first commit

This commit is contained in:
Dhananjaya99
2026-09-28 11:44:26 +05:30
parent 721743ae6b
commit bffc9b1686
424 changed files with 46474 additions and 0 deletions
+691
View File
@@ -0,0 +1,691 @@
using Fitness_core_BE.Exceptions;
using Fitness_core_BE.Helpers;
using Fitness_core_BE.Infra.UoW;
using Fitness_core_BE.Interfaces;
using Fitness_core_BE.Interfaces.Services;
using Fitness_core_BE.Models;
using Fitness_core_BE.Models.Enums;
namespace Fitness_core_BE.Services.Payment
{
public class PaymentService : IPaymentService
{
private readonly IPaymentRepository _paymentRepository;
private readonly ICustomerInvoiceRepository _customerInvoiceRepository;
private readonly IClientInvoiceRepository _clientInvoiceRepository;
private readonly ICustomerSubscriptionRepository _customerSubscriptionRepository;
private readonly ICustomerRepository _customerRepository;
private readonly IReceiptService _receiptService;
private readonly IThirdPartyService _thirdPartyService;
private readonly IAccessDeviceService _accessDeviceService;
private readonly InvoicePaymentTokenHelper _invoicePaymentTokenHelper;
private readonly IConfiguration _config;
private readonly UserRoleHelper _userRoleHelper;
private readonly IUnitOfWork _uow;
public PaymentService(
IPaymentRepository paymentRepository,
ICustomerInvoiceRepository customerInvoiceRepository,
IClientInvoiceRepository clientInvoiceRepository,
ICustomerSubscriptionRepository customerSubscriptionRepository,
ICustomerRepository customerRepository,
IReceiptService receiptService,
IThirdPartyService thirdPartyService,
IAccessDeviceService accessDeviceService,
InvoicePaymentTokenHelper invoicePaymentTokenHelper,
IConfiguration config,
UserRoleHelper userRoleHelper,
IUnitOfWork uow)
{
_paymentRepository = paymentRepository;
_customerInvoiceRepository = customerInvoiceRepository;
_clientInvoiceRepository = clientInvoiceRepository;
_customerSubscriptionRepository = customerSubscriptionRepository;
_customerRepository = customerRepository;
_receiptService = receiptService;
_thirdPartyService = thirdPartyService;
_accessDeviceService = accessDeviceService;
_invoicePaymentTokenHelper = invoicePaymentTokenHelper;
_config = config;
_userRoleHelper = userRoleHelper;
_uow = uow;
}
public async Task<object> CreateCustomerPayment(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
Guid customerInvoiceId = DataExtractor.GetRequiredGuid(data, "customerInvoiceId");
decimal payAmount = DataExtractor.GetRequiredDecimal(data, "payAmount");
var invoice = await _customerInvoiceRepository.GetCustomerInvoiceById(customerInvoiceId)
?? throw new NotFoundException($"Customer invoice {customerInvoiceId} not found");
Guid? cardId = null;
var cardIdStr = DataExtractor.GetOptionalString(data, "cardId");
if (!string.IsNullOrEmpty(cardIdStr) && Guid.TryParse(cardIdStr, out var parsedCardId))
cardId = parsedCardId;
var attempt = new PaymentTransaction
{
PayDate = DateTime.UtcNow,
PayStatus = "Pending",
PayAmount = payAmount,
PayNote = DataExtractor.GetOptionalString(data, "payNote"),
FK_CardId = cardId,
FK_CustomerInvoiceId = customerInvoiceId,
FK_ClientId = invoice.FKClientId,
FK_CustomerId = invoice.CustomerSubscription?.FKCustomerId,
BeneficiaryType = BeneficiaryType.Customer,
RefType = DataExtractor.GetOptionalString(data, "refType") ?? "invoice",
BankRefNo = DataExtractor.GetOptionalString(data, "bankRefNo"),
PaymentReferenceId = DataExtractor.GetOptionalString(data, "paymentReferenceId"),
IsActive = true,
IsApproved = false
};
var result = await _paymentRepository.CreatePaymentAttemptAsync(attempt);
await _uow.CommitAsync();
return new { success = true, message = "Payment attempt created", payment = result };
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> CreateClientPayment(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
Guid clientInvoiceId = DataExtractor.GetRequiredGuid(data, "clientInvoiceId");
decimal payAmount = DataExtractor.GetRequiredDecimal(data, "payAmount");
var invoice = await _clientInvoiceRepository.GetClientInvoiceById(clientInvoiceId)
?? throw new NotFoundException($"Client invoice {clientInvoiceId} not found");
Guid? cardId = null;
var cardIdStr = DataExtractor.GetOptionalString(data, "cardId");
if (!string.IsNullOrEmpty(cardIdStr) && Guid.TryParse(cardIdStr, out var parsedCardId))
cardId = parsedCardId;
var attempt = new PaymentTransaction
{
PayDate = DateTime.UtcNow,
PayStatus = "Pending",
PayAmount = payAmount,
PayNote = DataExtractor.GetOptionalString(data, "payNote"),
FK_CardId = cardId,
FK_ClientInvoiceId = clientInvoiceId,
FK_ClientId = invoice.ClientSubscription?.FKClientId,
BeneficiaryType = BeneficiaryType.Client,
RefType = DataExtractor.GetOptionalString(data, "refType") ?? "invoice",
BankRefNo = DataExtractor.GetOptionalString(data, "bankRefNo"),
PaymentReferenceId = DataExtractor.GetOptionalString(data, "paymentReferenceId"),
IsActive = true,
IsApproved = false
};
var result = await _paymentRepository.CreatePaymentAttemptAsync(attempt);
await _uow.CommitAsync();
return new { success = true, message = "Payment attempt created", payment = result };
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> GetPaymentById(object payload, HttpContext httpContext)
{
try
{
var data = DeserializePayload.Deserialize(payload);
Guid attemptId = DataExtractor.GetRequiredGuid(data, "attemptId");
var payment = await _paymentRepository.GetPaymentByIdAsync(attemptId)
?? throw new NotFoundException($"Payment {attemptId} not found");
return payment;
}
catch
{
throw;
}
}
public async Task<object> GetPaymentsByCustomerInvoice(object payload, HttpContext httpContext)
{
try
{
var data = DeserializePayload.Deserialize(payload);
Guid customerInvoiceId = DataExtractor.GetRequiredGuid(data, "customerInvoiceId");
var payments = await _paymentRepository.GetPaymentsByCustomerInvoiceIdAsync(customerInvoiceId);
return payments;
}
catch
{
throw;
}
}
public async Task<object> GetPaymentsByClientInvoice(object payload, HttpContext httpContext)
{
try
{
var data = DeserializePayload.Deserialize(payload);
Guid clientInvoiceId = DataExtractor.GetRequiredGuid(data, "clientInvoiceId");
var payments = await _paymentRepository.GetPaymentsByClientInvoiceIdAsync(clientInvoiceId);
return payments;
}
catch
{
throw;
}
}
public async Task<object> ApprovePayment(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
Guid attemptId = DataExtractor.GetRequiredGuid(data, "attemptId");
var payment = await _paymentRepository.GetPaymentByIdAsync(attemptId)
?? throw new NotFoundException($"Payment {attemptId} not found");
payment.IsApproved = true;
payment.PayStatus = "Approved";
await _paymentRepository.UpdatePaymentAttemptAsync(payment);
if (payment.FK_CustomerInvoiceId.HasValue)
{
// Roll the invoice status up from the approved total (Pending/Partial/Paid)
// and re-allocate the schedule, rather than blindly marking "Paid".
await _uow.SaveChangesAsync();
await RollupCustomerInvoiceAsync(payment.FK_CustomerInvoiceId.Value);
}
else if (payment.FK_ClientInvoiceId.HasValue)
{
await _clientInvoiceRepository.UpdateClientInvoiceStatus(payment.FK_ClientInvoiceId.Value, "Paid");
}
await _uow.CommitAsync();
if (payment.FK_ClientInvoiceId.HasValue)
{
// Post-commit, best-effort: email/SMS the gym owner a receipt PDF for their
// platform-fee payment. Never let a notification failure affect the approval.
try
{
var receipt = await _receiptService.BuildForClientInvoiceAsync(payment.FK_ClientInvoiceId.Value, payment.PayAmount);
await _receiptService.SendReceiptAsync(receipt);
}
catch (Exception ex)
{
Console.WriteLine($"Client invoice receipt notification failed (non-critical): {ex.Message}");
}
}
return new { success = true, message = "Payment approved", payment };
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
/// <summary>
/// Owner records a counter payment against a subscription. Immediately approved.
/// Fills the earliest unpaid installments; non-split plans must be settled in full.
/// </summary>
public async Task<object> RecordSubscriptionPayment(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
Guid subscriptionId = DataExtractor.GetRequiredGuid(data, "subscriptionId");
decimal amount = DataExtractor.GetRequiredDecimal(data, "amount");
string? note = DataExtractor.GetOptionalString(data, "note");
string paymentMethod = DataExtractor.GetRequiredString(data, "paymentMethod");
string? bankRefNo = DataExtractor.GetOptionalString(data, "bankRefNo");
if (paymentMethod != "Cash" && paymentMethod != "Bank")
throw new ArgumentException("paymentMethod must be 'Cash' or 'Bank'");
if (amount <= 0)
throw new ArgumentException("Payment amount must be greater than zero");
var subscription = await _customerSubscriptionRepository.GetCustomerSubscriptionById(subscriptionId)
?? throw new NotFoundException("Subscription not found");
var plan = subscription.CustomerPlan
?? throw new NotFoundException("Subscription plan not found");
// Authorization: the subscription's plan must belong to the caller's gym.
Guid clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
if (plan.FKClientId != clientId)
throw new UnauthorizedException("This subscription does not belong to your gym");
var invoice = await _customerInvoiceRepository.GetInvoiceBySubscriptionIdAsync(subscriptionId);
if (invoice == null)
{
// GetInvoiceBySubscriptionIdAsync also returns null while a renewal invoice
// is awaiting approval (its only invoices are unapproved and/or RolledOver
// right now) -- that is not a legacy subscription, and self-healing here
// would silently create a duplicate, uncarried-forward invoice. Only
// self-heal a genuine "never had an invoice" subscription.
if (await _customerInvoiceRepository.HasPendingApprovalInvoiceAsync(subscriptionId))
throw new BadRequestException(
"This member's renewal is awaiting approval. Approve, change plan, or reject it before recording a payment.");
// Self-heal legacy subscriptions that predate automatic invoicing.
invoice = await _customerInvoiceRepository.CreateCustomerInvoiceFromSubscription(subscriptionId);
await _uow.SaveChangesAsync();
}
var alreadyPaid = await _paymentRepository.GetApprovedPaidAmountAsync(invoice.CustomerInvoiceId);
var balance = invoice.Amount - alreadyPaid;
if (amount > balance)
throw new ArgumentException($"Amount exceeds the outstanding balance ({balance:0.##})");
if (!plan.AllowSplitPayments && amount != balance)
throw new ArgumentException("This plan must be paid in full; split payments are not enabled");
var attempt = new PaymentTransaction
{
PayDate = DateTime.UtcNow,
PayStatus = "Approved",
PayAmount = amount,
PayNote = note,
FK_CustomerInvoiceId = invoice.CustomerInvoiceId,
FK_ClientId = invoice.FKClientId,
FK_CustomerId = subscription.FKCustomerId,
BeneficiaryType = BeneficiaryType.Customer,
RefType = "manual",
PaymentMethod = paymentMethod,
BankRefNo = paymentMethod == "Bank" ? bankRefNo : null,
IsActive = true,
IsApproved = true
};
var result = await _paymentRepository.CreatePaymentAttemptAsync(attempt);
await _uow.SaveChangesAsync();
var summary = await RollupCustomerInvoiceAsync(invoice.CustomerInvoiceId);
await _uow.CommitAsync();
// Post-commit, best-effort: text the member and email them a receipt PDF.
// Never let a notification failure affect the recorded payment.
try
{
var receipt = await _receiptService.BuildForInvoiceAsync(invoice.CustomerInvoiceId, amount);
await _receiptService.SendReceiptAsync(receipt);
}
catch (Exception ex)
{
Console.WriteLine($"Receipt notification failed (non-critical): {ex.Message}");
}
// Post-commit, best-effort: once the invoice is fully settled, undo whatever
// overdue lock put the member's fingerprint access on hold -- the owner just
// collected the money, they shouldn't also have to remember to hit the separate
// unlock endpoint. SetCustomerAccessAsync already carries this onto every
// co-member on a shared group/couple subscription, not just the payer.
if (summary.Status == "Paid")
{
try
{
var customer = await _customerRepository.GetCustomerById(subscription.FKCustomerId);
if (customer != null)
{
var accessStatus = await _accessDeviceService.GetAccessStatusAsync(clientId, customer.FKUserId);
if (accessStatus?.DesiredAccessState == "Disabled")
await _accessDeviceService.SetCustomerAccessAsync(clientId, subscription.FKCustomerId, enabled: true, reason: null);
}
}
catch (Exception ex)
{
Console.WriteLine($"Auto-unlock after payment failed (non-critical): {ex.Message}");
}
}
return new
{
success = true,
invoiceId = invoice.CustomerInvoiceId,
invoiceAmount = summary.InvoiceAmount,
paidAmount = summary.Paid,
balance = summary.Balance,
status = summary.Status,
payment = result
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> GetSubscriptionPayments(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
Guid subscriptionId = DataExtractor.GetRequiredGuid(data, "subscriptionId");
var subscription = await _customerSubscriptionRepository.GetCustomerSubscriptionById(subscriptionId)
?? throw new NotFoundException("Subscription not found");
Guid clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
if (subscription.CustomerPlan == null || subscription.CustomerPlan.FKClientId != clientId)
throw new UnauthorizedException("This subscription does not belong to your gym");
var invoice = await _customerInvoiceRepository.GetInvoiceBySubscriptionIdAsync(subscriptionId);
decimal invoiceAmount = invoice?.Amount ??
subscription.CustomerPlan.PlanPrice +
(subscription.CustomerPlan.HasOnboardingFee ? subscription.CustomerPlan.OnboardingFeeAmount : 0m);
var payments = invoice != null
? await _paymentRepository.GetPaymentsByCustomerInvoiceIdAsync(invoice.CustomerInvoiceId)
: new List<PaymentTransaction>();
var installments = await _customerSubscriptionRepository.GetInstallmentsAsync(subscriptionId);
decimal paid = payments.Where(p => p.IsApproved == true).Sum(p => p.PayAmount);
return new
{
invoiceId = invoice?.CustomerInvoiceId,
invoiceAmount,
paidAmount = paid,
balance = invoiceAmount - paid,
status = paid >= invoiceAmount && invoiceAmount > 0 ? "Paid" : paid > 0 ? "Partial" : "Pending",
installments = installments.Select(i => new
{
i.SubscriptionInstallmentId,
i.InstallmentNo,
i.DueDate,
i.Amount,
i.PaidAmount,
i.Status
}),
payments = payments.Select(p => new
{
p.AttemptId,
p.PayDate,
p.PayStatus,
p.PayAmount,
p.PayNote,
p.RefType,
p.PaymentMethod,
p.BankRefNo,
p.IsApproved
})
};
}
/// <summary>
/// Owner-triggered "Charge now" against the member's consented auto-charge card.
/// No live payment gateway is wired up anywhere in this backend yet (see
/// ActivationService's own deferred platform-billing charge) — this records the attempt
/// and leaves it Pending, exactly like the rest of the app defers real settlement. Replace
/// the body of this method with a real PayHere/Stripe charge call once a gateway exists;
/// until then the owner falls back to SendPaymentLink or RecordSubscriptionPayment.
/// </summary>
public async Task<object> AttemptAutoCharge(Guid customerInvoiceId, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var invoice = await _customerInvoiceRepository.GetCustomerInvoiceById(customerInvoiceId)
?? throw new NotFoundException("Invoice not found");
Guid clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
if (invoice.FKClientId != clientId)
throw new UnauthorizedException("This invoice does not belong to your gym");
var subscription = await _customerSubscriptionRepository.GetCustomerSubscriptionById(invoice.FKCustomerSubscriptionId)
?? throw new NotFoundException("Subscription not found");
if (!subscription.AutoChargeEnabled || subscription.FKAutoChargeCardId == null)
throw new BadRequestException("Auto-deduct isn't enabled for this member. Send a payment link or mark as paid instead.");
var card = await _customerRepository.GetCardByIdAsync(subscription.FKAutoChargeCardId.Value);
if (card == null || !card.ConsentGiven)
throw new BadRequestException("This member hasn't consented to auto-deduct. Send a payment link or mark as paid instead.");
var alreadyPaid = await _paymentRepository.GetApprovedPaidAmountAsync(invoice.CustomerInvoiceId);
var balance = invoice.Amount - alreadyPaid;
if (balance <= 0)
throw new BadRequestException("This invoice is already fully paid.");
var attempt = new PaymentTransaction
{
PayDate = DateTime.UtcNow,
PayStatus = "Pending",
PayAmount = balance,
FK_CardId = card.JobId,
FK_CustomerInvoiceId = invoice.CustomerInvoiceId,
FK_ClientId = invoice.FKClientId,
FK_CustomerId = subscription.FKCustomerId,
BeneficiaryType = BeneficiaryType.Customer,
RefType = "auto",
IsActive = true,
IsApproved = false
};
var result = await _paymentRepository.CreatePaymentAttemptAsync(attempt);
await _uow.CommitAsync();
return new
{
success = true,
status = "Pending",
message = "Charge attempt recorded — pending confirmation.",
payment = result
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
/// <summary>
/// Emails/texts the member a link to the public pay-by-link page so they can pay this
/// invoice online themselves — the other fallback alongside AttemptAutoCharge/RecordSubscriptionPayment.
/// </summary>
public async Task<object> SendPaymentLink(Guid customerInvoiceId, HttpContext httpContext)
{
Guid clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var model = await _receiptService.BuildForInvoiceAsync(customerInvoiceId, requireClientId: clientId);
if (model.Balance <= 0)
throw new BadRequestException("This invoice is already fully paid.");
if (string.IsNullOrWhiteSpace(model.MemberEmail) && string.IsNullOrWhiteSpace(model.MemberPhone))
throw new BadRequestException("This member has no phone or email on file to send the link to.");
string token = _invoicePaymentTokenHelper.Generate(customerInvoiceId);
string baseUrl = (_config["App:FrontendBaseUrl"] ?? string.Empty).TrimEnd('/');
string link = $"{baseUrl}/pay/{token}";
bool emailSent = false;
bool smsSent = false;
if (!string.IsNullOrWhiteSpace(model.MemberEmail))
{
try
{
string html =
$"<p>Hi {model.MemberName},</p>" +
$"<p>You have an outstanding balance of <b>{model.Currency} {model.Balance:N2}</b> for <b>{model.PlanName}</b> at {model.GymName}.</p>" +
$"<p><a href=\"{link}\">Pay now</a></p>" +
$"<p>— {model.GymName}<br/><small>powered by GYM OS</small></p>";
await _thirdPartyService.SendEmailAsync(model.MemberEmail!, $"Payment link — {model.GymName}", html);
emailSent = true;
}
catch (Exception ex)
{
Console.WriteLine($"Payment link email failed (non-critical): {ex.Message}");
}
}
if (!string.IsNullOrWhiteSpace(model.MemberPhone))
{
try
{
string sms = $"Pay {model.Currency} {model.Balance:N2} for {model.PlanName} at {model.GymName}: {link}";
var smsResult = await _thirdPartyService.SendSmsAsync(model.MemberPhone!, message: sms);
smsSent = smsResult.StatusCode is >= 200 and < 300;
}
catch (Exception ex)
{
Console.WriteLine($"Payment link SMS failed (non-critical): {ex.Message}");
}
}
if (!emailSent && !smsSent)
throw new BadRequestException("Couldn't send the payment link by email or SMS. Check that notifications are configured.");
string via = emailSent && smsSent ? "email and SMS" : emailSent ? "email" : "SMS";
return new { success = true, message = $"Payment link sent via {via}" };
}
public async Task<object> GetInvoiceByPaymentToken(string token)
{
var invoiceId = _invoicePaymentTokenHelper.ValidateAndGetInvoiceId(token)
?? throw new NotFoundException("This payment link is invalid or has expired.");
var model = await _receiptService.BuildForInvoiceAsync(invoiceId);
return new
{
memberName = model.MemberName,
gymName = model.GymName,
planName = model.PlanName,
amount = model.InvoiceAmount,
balance = model.Balance,
currency = model.Currency
};
}
/// <summary>
/// Public, one-time self-serve payment from the pay-by-link page. Since the member is
/// completing this live (not a background auto-charge), it's approved immediately — same
/// treatment as RecordSubscriptionPayment. cardToken is a mock value today; this is the
/// seam where a real gateway checkout replaces the stub later.
/// </summary>
public async Task<object> ChargeInvoiceByPaymentToken(string token, object payload)
{
var invoiceId = _invoicePaymentTokenHelper.ValidateAndGetInvoiceId(token)
?? throw new NotFoundException("This payment link is invalid or has expired.");
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
DataExtractor.GetRequiredString(data, "cardToken");
var invoice = await _customerInvoiceRepository.GetCustomerInvoiceById(invoiceId)
?? throw new NotFoundException("Invoice not found");
var alreadyPaid = await _paymentRepository.GetApprovedPaidAmountAsync(invoice.CustomerInvoiceId);
var balance = invoice.Amount - alreadyPaid;
if (balance <= 0)
throw new BadRequestException("This invoice is already fully paid.");
var attempt = new PaymentTransaction
{
PayDate = DateTime.UtcNow,
PayStatus = "Approved",
PayAmount = balance,
PayNote = "Paid online via payment link",
FK_CustomerInvoiceId = invoice.CustomerInvoiceId,
FK_ClientId = invoice.FKClientId,
FK_CustomerId = invoice.CustomerSubscription?.FKCustomerId,
BeneficiaryType = BeneficiaryType.Customer,
RefType = "link",
IsActive = true,
IsApproved = true
};
await _paymentRepository.CreatePaymentAttemptAsync(attempt);
await _uow.SaveChangesAsync();
var summary = await RollupCustomerInvoiceAsync(invoice.CustomerInvoiceId);
await _uow.CommitAsync();
try
{
var receipt = await _receiptService.BuildForInvoiceAsync(invoice.CustomerInvoiceId, balance);
await _receiptService.SendReceiptAsync(receipt);
}
catch (Exception ex)
{
Console.WriteLine($"Receipt notification failed (non-critical): {ex.Message}");
}
return new
{
success = true,
invoiceId = invoice.CustomerInvoiceId,
paidAmount = summary.Paid,
balance = summary.Balance,
status = summary.Status
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
/// <summary>
/// Recomputes an invoice's status from its approved payment total and
/// re-allocates the installment schedule (idempotent — always derived from the total).
/// </summary>
private async Task<(decimal InvoiceAmount, decimal Paid, decimal Balance, string Status)> RollupCustomerInvoiceAsync(Guid customerInvoiceId)
{
var invoice = await _customerInvoiceRepository.GetCustomerInvoiceById(customerInvoiceId)
?? throw new NotFoundException("Invoice not found");
decimal paid = await _paymentRepository.GetApprovedPaidAmountAsync(customerInvoiceId);
string status = paid >= invoice.Amount && invoice.Amount > 0
? "Paid"
: paid > 0 ? "Partial" : "Pending";
await _customerInvoiceRepository.UpdateCustomerInvoiceStatus(customerInvoiceId, status);
// Distribute the total across installments in order.
var installments = await _customerSubscriptionRepository.GetInstallmentsAsync(invoice.FKCustomerSubscriptionId);
decimal remaining = paid;
foreach (var installment in installments)
{
decimal fill = Math.Min(remaining, installment.Amount);
installment.PaidAmount = fill;
installment.Status = fill >= installment.Amount
? "Paid"
: fill > 0 ? "Partial" : "Pending";
remaining -= fill;
}
return (invoice.Amount, paid, invoice.Amount - paid, status);
}
}
}