#nullable enable using Baya.Application.Contracts.Persistence; using Baya.Application.Models.Booking; using Baya.Application.Models.Common; using Baya.Application.Models.Payments; using Baya.Domain.Entities.Booking; using Baya.Domain.Entities.Identity; using Baya.Infrastructure.Persistence.Repositories.Common; using Microsoft.EntityFrameworkCore; namespace Baya.Infrastructure.Persistence.Repositories; internal sealed class BookingRepository : BaseAsyncRepository, IBookingRepository { public BookingRepository(ApplicationDbContext dbContext) : base(dbContext) { } public Task AddAsync(Booking booking, CancellationToken cancellationToken) => base.AddAsync(booking); public Task GetBookingIdByRequestIdAsync(long bookingRequestId, CancellationToken cancellationToken) => TableNoTracking .Where(b => b.BookingRequestId == bookingRequestId) .Select(b => (long?)b.Id) .FirstOrDefaultAsync(cancellationToken); public Task GetLedgerAmountsAsync(long id, CancellationToken cancellationToken) => TableNoTracking .Where(b => b.Id == id) .Select(b => new BookingLedgerAmounts(b.NurseId, b.GrossPriceIrr, b.BalinyaarCommissionIrr, b.NursePayoutAmount)) .FirstOrDefaultAsync(cancellationToken); public async Task GetDetailAsync(long id, CancellationToken cancellationToken) { var row = await ( from b in TableNoTracking where b.Id == id join n in DbContext.Set() on b.NurseId equals n.Id join p in DbContext.Set() on b.PatientId equals p.Id select new { Booking = b, NurseName = n.User.Name, NurseFamily = n.User.FamilyName, PatientName = p.DisplayName, Sessions = b.Sessions .OrderBy(s => s.SessionIndex) .Select(s => new BookingSessionProjection( s.Id, s.SessionIndex, s.ScheduledDate, s.ScheduledTimeStart, s.ScheduledTimeEnd, s.Status, s.VisitPayoutAmount, s.PayoutEligibleAt, s.Verification == null ? null : s.Verification.Status, s.Verification == null ? null : s.Verification.CheckInAt, s.Verification == null ? null : s.Verification.CheckOutAt, s.Verification == null ? null : s.Verification.CheckInAddressMatch)) .ToList() }) .FirstOrDefaultAsync(cancellationToken); if (row is null) return null; var bk = row.Booking; return new BookingDetailProjection( bk.Id, bk.BookingRequestId, bk.Status, bk.CustomerId, bk.NurseId, ComposeName(row.NurseName, row.NurseFamily), bk.PatientId, row.PatientName, bk.VariantId, bk.VariantSnapshotJson, bk.CustomerAddressId, bk.AddressSnapshotJson, bk.GrossPriceIrr, bk.BalinyaarCommissionIrr, bk.PlatformFeeRate, bk.NursePayoutAmount, bk.PspFeeAmount, bk.SessionCount, bk.ScheduledDate, bk.ScheduledTimeStart, bk.ScheduledTimeEnd, bk.ConfirmedAt, bk.CompletedAt, bk.CancelledAt, bk.CancelledBy, bk.CancellationReason, bk.CancellationPolicyCode, bk.CancellationRefundPercentage, bk.RefundableAmountIrr, bk.DisputeWindowEndsAt, bk.CreatedAt, row.Sessions); } public Task> ListForCustomerAsync(long customerId, string? status, int page, int pageSize, CancellationToken cancellationToken) => ListAsync(TableNoTracking.Where(b => b.CustomerId == customerId), status, forNurse: false, page, pageSize, cancellationToken); public Task> ListForNurseAsync(long nurseId, string? status, int page, int pageSize, CancellationToken cancellationToken) => ListAsync(TableNoTracking.Where(b => b.NurseId == nurseId), status, forNurse: true, page, pageSize, cancellationToken); public Task> ListAllAsync(string? status, int page, int pageSize, CancellationToken cancellationToken) => ListAsync(TableNoTracking, status, forNurse: false, page, pageSize, cancellationToken); private async Task> ListAsync( IQueryable source, string? status, bool forNurse, int page, int pageSize, CancellationToken cancellationToken) { if (!string.IsNullOrWhiteSpace(status)) source = source.Where(b => b.Status == status); var total = await source.CountAsync(cancellationToken); var rows = await ( from b in source join n in DbContext.Set() on b.NurseId equals n.Id join p in DbContext.Set() on b.PatientId equals p.Id orderby b.Id descending select new { b.Id, b.Status, NurseName = n.User.Name, NurseFamily = n.User.FamilyName, PatientName = p.DisplayName, b.ScheduledDate, b.SessionCount, b.GrossPriceIrr, b.NursePayoutAmount, b.DisputeWindowEndsAt, b.CreatedAt }) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(cancellationToken); var items = rows .Select(r => new BookingListItemDto( r.Id, r.Status, forNurse ? r.PatientName : ComposeName(r.NurseName, r.NurseFamily), r.ScheduledDate, r.SessionCount, (forNurse ? r.NursePayoutAmount : r.GrossPriceIrr).ToString(), r.DisputeWindowEndsAt, r.CreatedAt)) .ToList(); return new PagedResult(items, total, page, pageSize); } public Task GetParticipantsAsync(long bookingId, CancellationToken cancellationToken) => ( from b in TableNoTracking where b.Id == bookingId join c in DbContext.Set() on b.CustomerId equals c.Id join n in DbContext.Set() on b.NurseId equals n.Id select new BookingParticipants(c.UserId, n.UserId)) .FirstOrDefaultAsync(cancellationToken); public Task GetTrackedWithSessionsAsync(long id, CancellationToken cancellationToken) => Table.Include(b => b.Sessions).FirstOrDefaultAsync(b => b.Id == id, cancellationToken); public Task GetTrackedWithCareAsync(long id, CancellationToken cancellationToken) => Table.Include(b => b.CareInstructions).FirstOrDefaultAsync(b => b.Id == id, cancellationToken); public async Task GetTrackedBookingBySessionAsync(long sessionId, CancellationToken cancellationToken) { var bookingId = await DbContext.Set() .Where(s => s.Id == sessionId) .Select(s => (long?)s.BookingId) .FirstOrDefaultAsync(cancellationToken); if (bookingId is not { } bid) return null; return await Table .Include(b => b.Sessions).ThenInclude(s => s.Verification) .FirstOrDefaultAsync(b => b.Id == bid, cancellationToken); } public Task GetCareInstructionsGateAsync(long bookingId, CancellationToken cancellationToken) => TableNoTracking .Where(b => b.Id == bookingId) .Select(b => new CareInstructionsGate( b.Status, b.NurseId, b.CustomerId, b.CareInstructions == null ? null : new CareInstructionsDto( b.Id, b.CareInstructions.CurrentConditions, b.CareInstructions.Medications, b.CareInstructions.Allergies, b.CareInstructions.SpecialInstructions, b.CareInstructions.EmergencyContactName, b.CareInstructions.EmergencyContactPhone))) .FirstOrDefaultAsync(cancellationToken); public async Task> ListSessionsForNurseAsync( long nurseId, DateOnly? date, int page, int pageSize, CancellationToken cancellationToken) { var query = DbContext.Set().AsNoTracking() .Where(s => s.Booking.NurseId == nurseId); if (date is { } d) query = query.Where(s => s.ScheduledDate == d); var total = await query.CountAsync(cancellationToken); var items = await ( from s in query join p in DbContext.Set() on s.Booking.PatientId equals p.Id orderby s.ScheduledDate, s.ScheduledTimeStart select new BookingSessionListItemDto( s.Id, s.BookingId, s.SessionIndex, p.DisplayName, s.ScheduledDate, s.ScheduledTimeStart, s.ScheduledTimeEnd, s.Status, s.Verification == null ? VisitVerificationStatus.Pending : s.Verification.Status)) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(cancellationToken); return new PagedResult(items, total, page, pageSize); } public Task GetEvvForSessionAsync(long sessionId, CancellationToken cancellationToken) => DbContext.Set().AsNoTracking() .Where(s => s.Id == sessionId) .Select(s => new EvvGate( s.Booking.NurseId, s.Booking.CustomerId, s.Verification == null ? null : new VisitVerificationDto( s.Verification.Id, s.Id, s.Verification.Status, s.Verification.CheckInAt, s.Verification.CheckInLat, s.Verification.CheckInLng, s.Verification.CheckOutAt, s.Verification.CheckOutLat, s.Verification.CheckOutLng, s.Verification.CheckInAddressMatch, s.Verification.CheckInDistanceMeters))) .FirstOrDefaultAsync(cancellationToken); public async Task> ListAdminEvvAsync(string type, int page, int pageSize, CancellationToken cancellationToken) { var query = DbContext.Set().AsNoTracking().AsQueryable(); // The two admin review lanes: an advisory location mismatch, or a no-show (missed) session. query = type == "no_show" ? query.Where(s => s.Status == BookingSessionStatus.Missed) : query.Where(s => s.Verification != null && s.Verification.CheckInAddressMatch == false); var total = await query.CountAsync(cancellationToken); var items = await query .OrderByDescending(s => s.Id) .Skip((page - 1) * pageSize) .Take(pageSize) .Select(s => new AdminEvvItemDto( s.Id, s.BookingId, s.Booking.NurseId, s.Status, s.ScheduledDate, s.ScheduledTimeStart, s.Verification == null ? null : s.Verification.CheckInAt, s.Verification == null ? null : s.Verification.CheckInAddressMatch, s.Verification == null ? null : s.Verification.CheckInDistanceMeters)) .ToListAsync(cancellationToken); return new PagedResult(items, total, page, pageSize); } public async Task> GetNoShowCandidatesAsync(DateOnly today, int batchSize, CancellationToken cancellationToken) => await DbContext.Set() .Include(s => s.Booking) .Where(s => s.Status == BookingSessionStatus.Scheduled && s.ScheduledDate <= today) .OrderBy(s => s.Id) .Take(batchSize) .ToListAsync(cancellationToken); private static string ComposeName(string? name, string? familyName) => string.Join(' ', new[] { name, familyName }.Where(s => !string.IsNullOrWhiteSpace(s))).Trim(); }