b992d07689
- Created `SupplierPayment` and `SupplierPaymentGRN` models for managing supplier payments and their allocations. - Implemented API endpoints for listing and creating supplier payments. - Developed frontend components for displaying and recording supplier payments, including a dialog for payment creation. - Integrated GRN allocations into the payment creation process, ensuring total allocations match the payment amount. - Added pagination and loading states for the payments list.
132 lines
5.6 KiB
C#
132 lines
5.6 KiB
C#
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<List<PaymentResponse>?> 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<PaymentResponse?> 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);
|
|
}
|
|
}
|