#nullable enable using Baya.Application.Contracts.Persistence; using Baya.Application.Models.Common; using Baya.Application.Models.Refunds; using Baya.Domain.Entities.Booking; using Baya.Domain.Entities.Identity; using Baya.Domain.Entities.Payments; using Baya.Domain.Entities.Refunds; using Baya.Infrastructure.Persistence.Repositories.Common; using Microsoft.EntityFrameworkCore; namespace Baya.Infrastructure.Persistence.Repositories; internal sealed class RefundRepository : BaseAsyncRepository, IRefundRepository { public RefundRepository(ApplicationDbContext dbContext) : base(dbContext) { } public Task GetRefundContextAsync(long bookingId, CancellationToken cancellationToken) => (from t in DbContext.Set().AsNoTracking() where t.BookingId == bookingId && t.Status == PaymentTransactionStatus.Succeeded join b in DbContext.Set() on t.BookingId equals b.Id join c in DbContext.Set() on b.CustomerId equals c.Id join g in DbContext.Set() on t.GatewayId equals g.Id select new RefundMoneyContext( b.Id, b.CustomerId, c.UserId, b.NurseId, b.GrossPriceIrr, b.BalinyaarCommissionIrr, b.NursePayoutAmount, b.CancellationPolicyCode, b.CancellationRefundPercentage, b.RefundableAmountIrr, t.Id, t.GatewayReferenceCode, t.Amount, g.Type)) .FirstOrDefaultAsync(cancellationToken); public async Task GetRefundedSumForTransactionAsync(long paymentTransactionId, CancellationToken cancellationToken) => await TableNoTracking .Where(r => r.PaymentTransactionId == paymentTransactionId && r.Status != RefundStatus.Failed && r.Status != RefundStatus.Rejected) .SumAsync(r => (long?)r.Amount, cancellationToken) ?? 0; public Task AddRefundAsync(Refund refund, CancellationToken cancellationToken) => base.AddAsync(refund); public Task AddClawbackAsync(NurseClawback clawback, CancellationToken cancellationToken) => DbContext.Set().AddAsync(clawback, cancellationToken).AsTask(); public Task GetTrackedClawbackByIdAsync(long id, CancellationToken cancellationToken) => DbContext.Set().FirstOrDefaultAsync(c => c.Id == id, cancellationToken); public Task GetTrackedRefundByIdAsync(long id, CancellationToken cancellationToken) => Table.FirstOrDefaultAsync(r => r.Id == id, cancellationToken); public Task GetProcessingRefundIdForTransactionAsync(long paymentTransactionId, CancellationToken cancellationToken) => TableNoTracking .Where(r => r.PaymentTransactionId == paymentTransactionId && r.Status == RefundStatus.Processing) .OrderByDescending(r => r.Id) .Select(r => (long?)r.Id) .FirstOrDefaultAsync(cancellationToken); public async Task> ListAsync(long? bookingId, string? status, int page, int pageSize, CancellationToken cancellationToken) { var query = TableNoTracking; if (bookingId is { } bid) query = query.Where(r => r.BookingId == bid); if (!string.IsNullOrWhiteSpace(status)) query = query.Where(r => r.Status == status); var total = await query.CountAsync(cancellationToken); var rows = await query .OrderByDescending(r => r.Id) .Skip((page - 1) * pageSize) .Take(pageSize) .Select(r => new { r.Id, r.BookingId, r.PaymentTransactionId, r.Amount, r.PlatformFeeRefundedIrr, r.NursePayoutRefundedIrr, r.RefundChannel, r.Status, r.RefundPercentage, r.ReasonCategory, r.CancellationPolicyCode, r.RefundPercentageApplied, r.ExpectedCustomerRefundEta, r.GatewayRefundReference, r.ExternalRevertReference, r.ProcessedAt, r.CreatedAt }) .ToListAsync(cancellationToken); var items = rows .Select(r => new RefundListItemDto( r.Id, r.BookingId, r.PaymentTransactionId, r.Amount.ToString(), r.PlatformFeeRefundedIrr.ToString(), r.NursePayoutRefundedIrr.ToString(), r.RefundChannel, r.Status, r.RefundPercentage, r.ReasonCategory, r.CancellationPolicyCode, r.RefundPercentageApplied, r.ExpectedCustomerRefundEta, r.GatewayRefundReference, r.ExternalRevertReference, r.ProcessedAt, r.CreatedAt)) .ToList(); return new PagedResult(items, total, page, pageSize); } public Task GetStatusAsync(long id, CancellationToken cancellationToken) => StatusProjection(r => r.Id == id, cancellationToken); public Task GetStatusByBookingAsync(long bookingId, CancellationToken cancellationToken) // Newest refund for the booking (a re-refund is rare, but keep it deterministic). => StatusProjection(r => r.BookingId == bookingId, cancellationToken, latestFirst: true); private async Task StatusProjection( System.Linq.Expressions.Expression> predicate, CancellationToken cancellationToken, bool latestFirst = false) { var query = from r in TableNoTracking.Where(predicate) join b in DbContext.Set() on r.BookingId equals b.Id join c in DbContext.Set() on b.CustomerId equals c.Id select new { c.UserId, r.Id, r.BookingId, r.Status, r.RefundChannel, r.Amount, r.ExpectedCustomerRefundEta, r.GatewayRefundReference, r.ExternalRevertReference, r.PlatformFeeRefundedIrr, r.NursePayoutRefundedIrr, r.RefundPercentageApplied, r.CancellationPolicyCode, r.CreatedAt, r.ProcessedAt }; var row = await (latestFirst ? query.OrderByDescending(x => x.Id) : query).FirstOrDefaultAsync(cancellationToken); if (row is null) return null; var reference = Mask(row.GatewayRefundReference ?? row.ExternalRevertReference); var dto = new RefundStatusDto( row.Id, row.BookingId, row.Status, row.RefundChannel, row.Amount.ToString(), row.ExpectedCustomerRefundEta, reference, row.PlatformFeeRefundedIrr.ToString(), row.NursePayoutRefundedIrr.ToString(), row.RefundPercentageApplied, row.CancellationPolicyCode, row.CreatedAt, row.ProcessedAt); return new RefundStatusProjection(row.UserId, dto); } public Task GetExternalRevertReferenceAsync(long refundId, CancellationToken cancellationToken) => TableNoTracking .Where(r => r.Id == refundId) .Select(r => r.ExternalRevertReference) .FirstOrDefaultAsync(cancellationToken); // Show only the last 4 characters of an external reference to the customer — never the full PSP/BNPL id. private static string? Mask(string? reference) { if (string.IsNullOrEmpty(reference)) return reference; return reference.Length <= 4 ? new string('•', reference.Length) : $"{new string('•', reference.Length - 4)}{reference[^4..]}"; } }