using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Payments.Models; using LiteCharms.Features.MidrandBooks.Postgres; namespace LiteCharms.Features.MidrandBooks.Payments; public sealed class PaymentService(IDbContextFactory contextFactory) : IService { public async ValueTask> GetRefundAsync(long refundId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var refund = await context.Refunds.AsNoTracking() .FirstOrDefaultAsync(r => r.Id == refundId, cancellationToken); return refund is not null ? Result.Ok(refund.ToModel()) : Result.Fail("Could not find refund"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask UpdateRefundAsync(long refundId, UpdateRefund request, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken)) return Result.Fail("Order not found"); var updatedRows = await context.Refunds .Where(r => r.Id == refundId && r.OrderId == request.OrderId) .ExecuteUpdateAsync(setters => setters .SetProperty(r => r.Status, request.Status) .SetProperty(r => r.Reason, request.Reason) .SetProperty(r => r.UpdatedAt, DateTime.UtcNow) .SetProperty(r => r.Amount, request.Amount), cancellationToken); return updatedRows > 0 ? Result.Ok() : Result.Fail("Failed to update refund"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> CreateRefundAsync(CreateRefund request, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var order = await context.Orders.AsNoTracking() .FirstOrDefaultAsync(o => o.Id == request.OrderId && o.Status == OrderStatus.Completed, cancellationToken); if (order is null) return Result.Fail("Order not found"); if (request.Amount > order.Total) return Result.Fail("Refund amount cannot be greater than order total"); var totalRefundsPaid = await context.Refunds .Where(r => r.OrderId == request.OrderId) .SumAsync(r => r.Amount, cancellationToken); if (request.Amount > (order.Total - totalRefundsPaid)) return Result.Fail("Refund amount exceeds amount available for refund"); var refund = context.Refunds.Add(new Entities.Refund { Amount = request.Amount, CreatedAt = DateTime.UtcNow, OrderId = request.OrderId, Reason = request.Reason, Status = request.Status, Type = request.Type, }); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok(refund.Entity.Id) : Result.Fail("Failed to create refund"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask WriteLedgerEntryAsync(CreateLedgerEntry request, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken)) return Result.Fail("Order not found"); if (!await context.Customers.AnyAsync(o => o.Id == request.CustomerId, cancellationToken)) return Result.Fail("Customer not found"); if (!await context.Orders.AnyAsync(oc => oc.Id == request.OrderId && oc.CustomerId == request.CustomerId, cancellationToken)) return Result.Fail("Customer does not match the order"); if (!await context.Payments.AnyAsync(o => o.Id == request.PaymentId && o.OrderId == request.OrderId, cancellationToken)) return Result.Fail("Payment not found"); if (request.PaymentGatewayId is not null) if (!await context.Gateways.AnyAsync(o => o.Id == request.PaymentGatewayId, cancellationToken)) return Result.Fail("Gateway not found"); context.Ledger.Add(new Entities.PaymentLedger { CreatedAt = DateTime.UtcNow, CustomerId = request.CustomerId, OrderId = request.OrderId, PaymentGatewayId = request.PaymentGatewayId, PaymentGatewayReference = request.PaymentGatewayReference, PaymentId = request.PaymentId, Status = request.Status, }); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok() : Result.Fail("Failed to create ledger entry"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> GetPaymentGatewayAsync(long paymentGatewayId, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var gateway = await context.Gateways.AsNoTracking().FirstOrDefaultAsync(g => g.Id == paymentGatewayId, cancellationToken); return gateway is not null ? Result.Ok(gateway.ToModel()) : Result.Fail("Could not find gateway"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> CreatePaymentGatewayAsync(CreatePaymentGateway request, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (await context.Gateways.AnyAsync(g => g.MerchantId == request.MerchantId && g.MerchantKey == request.MerchantKey, cancellationToken)) return Result.Fail("A gateway with the same credentials already exists"); var gateway = context.Gateways.Add(new Entities.PaymentGateway { CreatedAt = DateTime.UtcNow, Enabled = true, IsSandbox = request.IsSandbox, MerchantId = request.MerchantId, MerchantKey = request.MerchantKey, Name = request.Name, Website = request.Website, }); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok(gateway.Entity.Id) : Result.Fail("Failed to create payment gateway"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask CompletePaymentAsync(long paymentId, PaymentStatuses status, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (status == PaymentStatuses.NotPaid) return Result.Fail("Cannot finalise a payment using NotPaid status"); var updatedRecords = await context.Payments .Where(p => p.Id == paymentId && p.Status != PaymentStatuses.Paid && p.Status != status) .ExecuteUpdateAsync(setters => setters .SetProperty(u => u.Status, status) .SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken); return updatedRecords > 0 ? Result.Ok() : Result.Fail("Failed to update payment"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask UpdatePaymentAsync(long paymentId, decimal amount, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var updatedRecords = await context.Payments .Where(p => p.Id == paymentId && p.Status == PaymentStatuses.NotPaid) .ExecuteUpdateAsync(setters => setters .SetProperty(u => u.Amount, amount) .SetProperty(u => u.Status, PaymentStatuses.NotPaid) .SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken); return updatedRecords > 0 ? Result.Ok() : Result.Fail("Failed to update payment"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> CreatePaymentAsync(decimal amount, long orderId, string reference, CancellationToken cancellationToken = default) { try { await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (await context.Payments.AnyAsync(p => p.OrderId == orderId && p.Amount == amount && p.Status != PaymentStatuses.Paid, cancellationToken)) return Result.Fail("An order with the same amount already exists in the system"); var payment = context.Payments.Add(new Entities.Payment { CreatedAt = DateTime.UtcNow, Amount = amount, OrderId = orderId, Reference = reference, Status = PaymentStatuses.NotPaid, }); return await context.SaveChangesAsync(cancellationToken) > 0 ? Result.Ok(payment.Entity.Id) : Result.Fail("Failed to make payment"); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } }