Files
baya-monorepo/server/src/Tests/Baya.Test.Foundation/Booking/CreateBookingRequestHandlerTests.cs
T
2026-07-06 02:48:56 +03:30

193 lines
8.4 KiB
C#

using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Configuration;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Features.Booking.Commands.CreateBookingRequest;
using Baya.Application.Models.Booking;
using Baya.Application.Models.Identity;
using Baya.Domain.Entities.Booking;
using Baya.Domain.Entities.Catalog;
using Baya.Domain.Entities.Identity;
using Baya.Domain.Entities.User;
using NSubstitute;
namespace Baya.Test.Foundation.Booking;
public class CreateBookingRequestHandlerTests
{
private readonly ICurrentUser _currentUser = Substitute.For<ICurrentUser>();
private readonly IUnitOfWork _unitOfWork = Substitute.For<IUnitOfWork>();
private readonly IPlatformConfig _config = Substitute.For<IPlatformConfig>();
private readonly IDateTimeProvider _clock = Substitute.For<IDateTimeProvider>();
private readonly INotificationDispatcher _notifications = Substitute.For<INotificationDispatcher>();
private readonly ICustomerProfileRepository _customers = Substitute.For<ICustomerProfileRepository>();
private readonly IPatientRepository _patients = Substitute.For<IPatientRepository>();
private readonly ICustomerAddressRepository _addresses = Substitute.For<ICustomerAddressRepository>();
private readonly INurseServiceVariantRepository _variants = Substitute.For<INurseServiceVariantRepository>();
private readonly INurseProfileRepository _nurses = Substitute.For<INurseProfileRepository>();
private readonly IBookingRequestRepository _requests = Substitute.For<IBookingRequestRepository>();
private const int CustomerUserId = 7;
private const long CustomerId = 100;
private const long NurseId = 42;
private const int NurseUserId = 9;
private const long PatientId = 5;
private const long AddressId = 6;
private const long VariantId = 8;
private static readonly DateTimeOffset Now = new(2026, 7, 6, 10, 0, 0, TimeSpan.Zero);
public CreateBookingRequestHandlerTests()
{
_currentUser.UserId.Returns(CustomerUserId);
_currentUser.Roles.Returns([RoleNames.Customer]);
_clock.UtcNow.Returns(Now);
_unitOfWork.CustomerProfileRepository.Returns(_customers);
_unitOfWork.PatientRepository.Returns(_patients);
_unitOfWork.CustomerAddressRepository.Returns(_addresses);
_unitOfWork.NurseServiceVariantRepository.Returns(_variants);
_unitOfWork.NurseProfileRepository.Returns(_nurses);
_unitOfWork.BookingRequestRepository.Returns(_requests);
_customers.GetProfileIdByUserIdAsync(CustomerUserId, Arg.Any<CancellationToken>()).Returns(CustomerId);
_patients.GetOwnedAsync(PatientId, CustomerId, Arg.Any<CancellationToken>())
.Returns(new Patient { CustomerId = CustomerId, DisplayName = "پدر", Gender = "male" });
_addresses.GetOwnedAsync(AddressId, CustomerId, Arg.Any<CancellationToken>())
.Returns(new CustomerAddress { CustomerId = CustomerId });
_variants.GetOwnedAsync(VariantId, NurseId, Arg.Any<CancellationToken>())
.Returns(new NurseServiceVariant { NurseId = NurseId, IsActive = true });
_nurses.GetBookingContextByIdAsync(NurseId, Arg.Any<CancellationToken>())
.Returns(new NurseBookingContext(NurseUserId, "female", IsVerified: true, IsAcceptingBookings: true));
_config.GetConfig<int>("nurse_response_deadline_hours", Arg.Any<CancellationToken>()).Returns(24);
_requests.GetDetailAsync(Arg.Any<long>(), Arg.Any<CancellationToken>()).Returns(DetailStub());
}
private CreateBookingRequestCommandHandler Handler()
=> new(_currentUser, _unitOfWork, _config, _clock, _notifications);
private static CreateBookingRequestCommand Command(string gender = "female")
=> new(NurseId, VariantId, PatientId, AddressId,
new DateOnly(2026, 8, 1), new TimeOnly(9, 0), new TimeOnly(13, 0), gender, "Careful with the IV.");
[Fact]
public async Task Create_Valid_FreezesResponseDeadlineFromConfigAndNotifiesNurse()
{
var result = await Handler().Handle(Command(), CancellationToken.None);
Assert.True(result.IsSuccess);
await _requests.Received(1).AddAsync(
Arg.Is<BookingRequest>(r =>
r.CustomerId == CustomerId && r.NurseId == NurseId && r.PatientId == PatientId
&& r.Status == BookingRequestStatus.PendingNurseResponse
&& r.PaymentDeadlineAt == null
&& r.NurseResponseDeadlineAt == Now.AddHours(24).UtcDateTime),
Arg.Any<CancellationToken>());
await _unitOfWork.Received(1).CommitAsync();
await _notifications.Received(1).DispatchAsync(
Arg.Is<Notification>(n => n.RecipientUserId == NurseUserId && n.Type == "booking_request_received"),
Arg.Any<CancellationToken>());
}
[Fact]
public async Task Create_AnyGender_MatchesRegardlessOfNurseGender()
{
var result = await Handler().Handle(Command("any"), CancellationToken.None);
Assert.True(result.IsSuccess);
}
[Fact]
public async Task Create_SameGenderMismatch_FailsNamingTheConflict()
{
// Nurse is female; a male requirement must not match.
var result = await Handler().Handle(Command("male"), CancellationToken.None);
Assert.False(result.IsSuccess);
Assert.False(result.IsNotFound);
await _requests.DidNotReceive().AddAsync(Arg.Any<BookingRequest>(), Arg.Any<CancellationToken>());
}
[Fact]
public async Task Create_CrossCustomerPatient_IsNotFoundAndCreatesNothing()
{
_patients.GetOwnedAsync(PatientId, CustomerId, Arg.Any<CancellationToken>()).Returns((Patient)null);
var result = await Handler().Handle(Command(), CancellationToken.None);
Assert.True(result.IsNotFound);
await _requests.DidNotReceive().AddAsync(Arg.Any<BookingRequest>(), Arg.Any<CancellationToken>());
}
[Fact]
public async Task Create_AddressNotOwned_IsNotFound()
{
_addresses.GetOwnedAsync(AddressId, CustomerId, Arg.Any<CancellationToken>()).Returns((CustomerAddress)null);
var result = await Handler().Handle(Command(), CancellationToken.None);
Assert.True(result.IsNotFound);
}
[Fact]
public async Task Create_VariantNotThisNurses_IsNotFound()
{
_variants.GetOwnedAsync(VariantId, NurseId, Arg.Any<CancellationToken>()).Returns((NurseServiceVariant)null);
var result = await Handler().Handle(Command(), CancellationToken.None);
Assert.True(result.IsNotFound);
}
[Fact]
public async Task Create_InactiveVariant_Fails()
{
_variants.GetOwnedAsync(VariantId, NurseId, Arg.Any<CancellationToken>())
.Returns(new NurseServiceVariant { NurseId = NurseId, IsActive = false });
var result = await Handler().Handle(Command(), CancellationToken.None);
Assert.False(result.IsSuccess);
await _requests.DidNotReceive().AddAsync(Arg.Any<BookingRequest>(), Arg.Any<CancellationToken>());
}
[Fact]
public async Task Create_NurseNotAcceptingOrUnverified_Fails()
{
_nurses.GetBookingContextByIdAsync(NurseId, Arg.Any<CancellationToken>())
.Returns(new NurseBookingContext(NurseUserId, "female", IsVerified: true, IsAcceptingBookings: false));
var result = await Handler().Handle(Command(), CancellationToken.None);
Assert.False(result.IsSuccess);
}
[Fact]
public async Task Create_PastDate_Fails()
{
var command = Command() with { RequestedDate = new DateOnly(2026, 7, 5) };
var result = await Handler().Handle(command, CancellationToken.None);
Assert.False(result.IsSuccess);
}
[Fact]
public async Task Create_NonCustomer_IsForbidden()
{
_currentUser.Roles.Returns([RoleNames.Nurse]);
var result = await Handler().Handle(Command(), CancellationToken.None);
Assert.True(result.IsForbidden);
}
private static BookingRequestDetailProjection DetailStub()
=> new(
1, BookingRequestStatus.PendingNurseResponse, CustomerId, NurseId,
"پرستار", 0m, 0, PatientId, "پدر", VariantId, "خدمت", "per_day",
AddressId, "خانه", 1, "تهران", "Tehran", null, null, null,
"line", "postal", "recipient", "phone",
"female", new DateOnly(2026, 8, 1), new TimeOnly(9, 0), new TimeOnly(13, 0),
"notes", Now.AddHours(24).UtcDateTime, null, null, Now);
}