Files
baya-monorepo/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/RefundRepository.cs
T
2026-07-13 17:03:45 +03:30

177 lines
8.0 KiB
C#

#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<Refund>, IRefundRepository
{
public RefundRepository(ApplicationDbContext dbContext) : base(dbContext)
{
}
public Task<RefundMoneyContext?> GetRefundContextAsync(long bookingId, CancellationToken cancellationToken)
=> (from t in DbContext.Set<PaymentTransaction>().AsNoTracking()
where t.BookingId == bookingId && t.Status == PaymentTransactionStatus.Succeeded
join b in DbContext.Set<Booking>() on t.BookingId equals b.Id
join c in DbContext.Set<CustomerProfile>() on b.CustomerId equals c.Id
join g in DbContext.Set<PaymentGateway>() 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<long> 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<NurseClawback>().AddAsync(clawback, cancellationToken).AsTask();
public Task<NurseClawback?> GetTrackedClawbackByIdAsync(long id, CancellationToken cancellationToken)
=> DbContext.Set<NurseClawback>().FirstOrDefaultAsync(c => c.Id == id, cancellationToken);
public Task<Refund?> GetTrackedRefundByIdAsync(long id, CancellationToken cancellationToken)
=> Table.FirstOrDefaultAsync(r => r.Id == id, cancellationToken);
public Task<long?> 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<PagedResult<RefundListItemDto>> 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<RefundListItemDto>(items, total, page, pageSize);
}
public Task<RefundStatusProjection?> GetStatusAsync(long id, CancellationToken cancellationToken)
=> StatusProjection(r => r.Id == id, cancellationToken);
public Task<RefundStatusProjection?> 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<RefundStatusProjection?> StatusProjection(
System.Linq.Expressions.Expression<Func<Refund, bool>> predicate, CancellationToken cancellationToken, bool latestFirst = false)
{
var query = from r in TableNoTracking.Where(predicate)
join b in DbContext.Set<Booking>() on r.BookingId equals b.Id
join c in DbContext.Set<CustomerProfile>() 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<string?> 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..]}";
}
}