using Distribution.Features.BankAccount; using Distribution.Features.Invoice; using Distribution.Features.ReceivedCheque; using Distribution.Infra.Authorization; using Distribution.Infra.UoW; using Distribution.Models; namespace Distribution.Features.Payment; public class PaymentService( IPaymentRepository repo, IInvoiceRepository invoiceRepo, IBankAccountRepository bankAccountRepo, IReceivedChequeRepository receivedChequeRepo, IUnitOfWork uow, ICurrentUserService currentUser) { private static PaymentResponse ToResponse(Models.Payment p) => new(p.Id, p.FK_InvoiceId, p.Amount, p.Method, p.EntryMode, p.FK_BankAccountId, p.FK_ReceivedChequeId, p.ChequeNumber, p.ChequeDate, p.CollectedAt, p.CollectedBy, p.RecordedBy, p.CreatedAt); public async Task?> GetByInvoiceIdAsync(Guid invoiceId) { var invoice = await invoiceRepo.GetByIdAsync(invoiceId); if (invoice is null || !invoice.IsActive) return null; var payments = await repo.GetByInvoiceIdAsync(invoiceId); return payments.Select(ToResponse).ToList(); } public async Task CreateAsync(Guid invoiceId, CreatePaymentRequest request, CancellationToken ct = default) { var invoice = await invoiceRepo.GetByIdAsync(invoiceId); if (invoice is null || !invoice.IsActive) return null; if (invoice.Status == InvoiceStatus.Finalized) throw new InvalidOperationException("Cannot record a payment against a finalized invoice"); if (request.Amount <= 0) throw new InvalidOperationException("Amount must be greater than zero"); if (request.Method == PaymentMethod.Cheque && (string.IsNullOrWhiteSpace(request.ChequeNumber) || request.ChequeDate is null)) throw new InvalidOperationException("ChequeNumber and ChequeDate are required when Method is Cheque"); if (request.Method == PaymentMethod.Cash && request.BankAccountId is null) throw new InvalidOperationException("BankAccountId is required when Method is Cash"); Models.BankAccount? bankAccount = null; if (request.BankAccountId is not null) { bankAccount = await bankAccountRepo.GetByIdAsync(request.BankAccountId.Value); if (bankAccount is null) throw new InvalidOperationException("BankAccountId does not reference a valid account"); if (bankAccount.IsActive is false) throw new InvalidOperationException("The selected account is inactive"); } var recorderId = currentUser.UserId ?? request.CollectedByUserId ?? throw new InvalidOperationException("CollectedByUserId is required while auth is unavailable"); var collectedBy = request.CollectedByUserId ?? recorderId; var recordedBy = request.EntryMode == EntryMode.DayEndManual && collectedBy != recorderId ? recorderId : (Guid?)null; var payment = new Models.Payment { FK_InvoiceId = invoiceId, Amount = request.Amount, Method = request.Method, EntryMode = request.EntryMode, ChequeNumber = request.Method == PaymentMethod.Cheque ? request.ChequeNumber : null, ChequeDate = request.Method == PaymentMethod.Cheque ? request.ChequeDate : null, FK_BankAccountId = request.BankAccountId, CollectedAt = DateTime.Now, CollectedBy = collectedBy, RecordedBy = recordedBy }; await uow.BeginAsync(ct); try { // Auto-create a ReceivedCheque for the physical cheque. Status starts at Received; // bookkeeping of the deposit/clear transitions happens through the Received Cheques screen. if (request.Method == PaymentMethod.Cheque) { var cheque = new Models.ReceivedCheque { CompanyId = null, BranchId = null, ReceivedFromType = ReceivedFromType.Customer, ReceivedFromId = null, ReceivedFromName = $"Invoice {invoice.Id}", DrawerBankName = request.DrawerBankName, AccountHolderName = request.AccountHolderName, ChequeNo = request.ChequeNumber!, ChequeDate = request.ChequeDate!.Value, Amount = request.Amount, ReceivedDate = DateTime.Now, ReferenceType = "Invoice", Status = ReceivedChequeStatus.Received }; await receivedChequeRepo.AddAsync(cheque); payment.FK_ReceivedChequeId = cheque.Id; } await repo.AddAsync(payment); var allPayments = await repo.GetByInvoiceIdAsync(invoiceId); var total = allPayments.Sum(p => p.Amount) + request.Amount; invoice.PaymentStatus = total == 0 ? PaymentStatus.Pending : total < invoice.TotalAmount ? PaymentStatus.PartiallyPaid : PaymentStatus.Paid; invoiceRepo.Update(invoice); // TODO ledger: when accounting is wired in, post a journal entry here — // Debit Cash/Bank (BankAccount) OR Cheques-in-hand (for Cheque payments) // Credit Accounts Receivable (invoice.Store) await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return ToResponse(payment); } }