177 lines
8.0 KiB
C#
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..]}";
|
|
}
|
|
}
|