Files
ERP_Distribution/Distribution/Features/Payment/PaymentService.cs
HarithaRandunu ca81e9bc42 feat: add General Ledger integration and financial reporting features
- Implemented General Ledger client component for managing GL integration.
- Created a dedicated page for General Ledger configuration.
- Developed Finance Report client component for generating various financial reports.
- Added report blocks for structured display of financial data.
- Introduced API endpoints for fetching GL accounts and financial reports.
- Updated navigation to include links for GL integration and financial reports.
- Enhanced permissions to manage GL and view financial reports.
- Updated appsettings to include General Ledger API configuration.
2026-10-20 18:54:30 +05:30

165 lines
6.9 KiB
C#

using Distribution.Features.BankAccount;
using Distribution.Features.GeneralLedger;
using Distribution.Features.Invoice;
using Distribution.Features.ReceivedCheque;
using Distribution.Features.StoreCredit;
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,
IStoreCreditMovementRepository creditRepo,
IUnitOfWork uow,
ICurrentUserService currentUser,
GlPostingService gl)
{
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 (request.Amount <= 0)
throw new InvalidOperationException("Amount must be greater than zero");
var existingPaid = (await repo.GetByInvoiceIdAsync(invoiceId)).Sum(p => p.Amount);
var remaining = invoice.TotalAmount - existingPaid;
if (request.Amount > remaining)
throw new InvalidOperationException(
$"Amount exceeds the outstanding balance of {remaining:0.##}");
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");
if (request.Method == PaymentMethod.Credit)
{
var store = invoice.ShopPO?.Store
?? throw new InvalidOperationException("Invoice is missing the owning store; cannot record a credit payment");
var currentOutstanding = await creditRepo.GetOutstandingForStoreAsync(store.Id);
if (currentOutstanding + request.Amount > store.CreditLimit)
{
var available = store.CreditLimit - currentOutstanding;
throw new InvalidOperationException(
$"Credit limit exceeded. Available credit: {available:0.##}");
}
}
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);
if (request.Method == PaymentMethod.Credit)
{
var movement = new StoreCreditMovement
{
FK_StoreId = invoice.ShopPO!.Store!.Id,
Direction = StoreCreditMovementDirection.Usage,
Amount = request.Amount,
FK_InvoiceId = invoice.Id,
FK_PaymentId = payment.Id,
OccurredAt = DateTime.Now,
RecordedBy = recorderId
};
await creditRepo.AddAsync(movement);
}
var total = existingPaid + request.Amount;
invoice.PaymentStatus = total == 0
? PaymentStatus.Pending
: total < invoice.TotalAmount
? PaymentStatus.PartiallyPaid
: PaymentStatus.Paid;
invoiceRepo.Update(invoice);
await uow.CommitAsync(ct);
}
catch { await uow.RollbackAsync(ct); throw; }
// Mirrors to the General Ledger after commit: Dr Cash/Bank (or Cheques-in-hand) / Cr Receivable. Never throws.
await gl.PostCustomerPaymentAsync(payment, ct);
return ToResponse(payment);
}
}