feat: add supplier payment and allocation models, API, and frontend components
- 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.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using Distribution.Models;
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Features.Payment;
|
||||
|
||||
@@ -6,8 +6,14 @@ public record CreatePaymentRequest(
|
||||
decimal Amount,
|
||||
PaymentMethod Method,
|
||||
EntryMode EntryMode = EntryMode.RealTime,
|
||||
// Cash: required. Cheque: optional (used as the deposit account hint if provided).
|
||||
Guid? BankAccountId = null,
|
||||
// Cheque: required.
|
||||
string? ChequeNumber = null,
|
||||
DateTime? ChequeDate = null,
|
||||
// Cheque: optional metadata forwarded to the auto-created ReceivedCheque.
|
||||
string? DrawerBankName = null,
|
||||
string? AccountHolderName = null,
|
||||
Guid? CollectedByUserId = null);
|
||||
|
||||
public record PaymentResponse(
|
||||
@@ -16,6 +22,8 @@ public record PaymentResponse(
|
||||
decimal Amount,
|
||||
PaymentMethod Method,
|
||||
EntryMode EntryMode,
|
||||
Guid? BankAccountId,
|
||||
Guid? ReceivedChequeId,
|
||||
string? ChequeNumber,
|
||||
DateTime? ChequeDate,
|
||||
DateTime CollectedAt,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Distribution.Features.Invoice;
|
||||
using Distribution.Features.BankAccount;
|
||||
using Distribution.Features.Invoice;
|
||||
using Distribution.Features.ReceivedCheque;
|
||||
using Distribution.Infra.Authorization;
|
||||
using Distribution.Infra.UoW;
|
||||
using Distribution.Models;
|
||||
@@ -8,11 +10,15 @@ 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.ChequeNumber, p.ChequeDate,
|
||||
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)
|
||||
@@ -31,10 +37,26 @@ public class PaymentService(
|
||||
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;
|
||||
@@ -50,6 +72,7 @@ public class PaymentService(
|
||||
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
|
||||
@@ -58,6 +81,30 @@ public class PaymentService(
|
||||
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);
|
||||
@@ -70,6 +117,11 @@ public class PaymentService(
|
||||
: 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; }
|
||||
|
||||
Reference in New Issue
Block a user