using System.Text.Json; using General_Ledger_Service.DTOs.Cheques; using General_Ledger_Service.DTOs.Common; using General_Ledger_Service.Enums; using General_Ledger_Service.Exceptions; using General_Ledger_Service.Helpers; using General_Ledger_Service.Infra.UoW; using General_Ledger_Service.Interfaces.Repositories; using General_Ledger_Service.Interfaces.Services; using General_Ledger_Service.Models; namespace General_Ledger_Service.Services { public class ChequeService( IChequeRepository chequeRepository, IBankRepository bankRepository, IAuditLogRepository auditLogRepository, IUnitOfWork unitOfWork) : IChequeService { private readonly IChequeRepository _chequeRepository = chequeRepository; private readonly IBankRepository _bankRepository = bankRepository; private readonly IAuditLogRepository _auditLogRepository = auditLogRepository; private readonly IUnitOfWork _unitOfWork = unitOfWork; public async Task CreateChequeBookAsync(CreateChequeBookRequest request) { _ = await _bankRepository.GetByIdAsync(request.BankAccountId) ?? throw new NotFoundException("Bank account not found"); if (string.IsNullOrWhiteSpace(request.ChequeBookNo)) { throw new BadRequestException("chequeBookNo is required"); } if (await _chequeRepository.ChequeBookNoExistsAsync(request.ChequeBookNo)) { throw new BadRequestException($"Cheque book number '{request.ChequeBookNo}' already exists"); } if (request.TotalLeaves <= 0) { throw new BadRequestException("totalLeaves must be greater than 0"); } var chequeNumbers = BuildChequeNoRange(request.StartChequeNo, request.EndChequeNo, request.TotalLeaves); var chequeBook = new ChequeBook { BranchId = request.BranchId, BankAccountId = request.BankAccountId, ChequeBookNo = request.ChequeBookNo, StartChequeNo = request.StartChequeNo, EndChequeNo = request.EndChequeNo, TotalLeaves = request.TotalLeaves, ReceivedDate = request.ReceivedDate, Status = ChequeBookStatus.Active, Description = request.Description, IsActive = true, CreatedBy = request.CreatedBy }; await _unitOfWork.BeginAsync(); try { await _chequeRepository.AddChequeBookAsync(chequeBook); await _unitOfWork.SaveChangesAsync(); var pages = chequeNumbers.Select(chequeNo => new ChequePage { ChequeBookId = chequeBook.ChequeBookId, ChequeNo = chequeNo, IssueStatus = ChequeIssueStatus.Unused, CreatedBy = request.CreatedBy }); await _chequeRepository.AddChequePagesAsync(pages); await _unitOfWork.SaveChangesAsync(); await _auditLogRepository.AddAsync(new AuditLog { Category = AuditCategory.Administrative, EntityName = "ChequeBook", EntityId = chequeBook.ChequeBookId.ToString(), Action = AuditAction.Create, Description = $"Cheque book {chequeBook.ChequeBookNo} created with {chequeBook.TotalLeaves} leaves", PerformedBy = request.CreatedBy }); await _unitOfWork.SaveChangesAsync(); await _unitOfWork.CommitAsync(); } catch { await _unitOfWork.RollbackAsync(); throw; } return ResponseHelpers.Created("Cheque book created successfully", chequeBook); } // Cheque numbers are printed sequentially on the physical book, so the range is generated // by numeric increment, preserving the digit-width (leading zeros) of StartChequeNo. private static List BuildChequeNoRange(string startChequeNo, string endChequeNo, int totalLeaves) { if (!long.TryParse(startChequeNo, out var start) || !long.TryParse(endChequeNo, out var end)) { throw new BadRequestException("startChequeNo and endChequeNo must be numeric"); } if (end < start) { throw new BadRequestException("endChequeNo must not be less than startChequeNo"); } var count = end - start + 1; if (count != totalLeaves) { throw new BadRequestException( $"totalLeaves ({totalLeaves}) does not match the range startChequeNo-endChequeNo ({count} cheques)"); } var width = startChequeNo.Length; var result = new List(); for (var n = start; n <= end; n++) { result.Add(n.ToString().PadLeft(width, '0')); } return result; } public async Task GetChequeBookByNoAsync(string chequeBookNo, string? expand) { var includePages = string.Equals(expand, "pages", StringComparison.OrdinalIgnoreCase); var chequeBook = await _chequeRepository.GetChequeBookByNoAsync(chequeBookNo, includePages) ?? throw new NotFoundException("Cheque book not found"); return ResponseHelpers.Ok("Cheque book retrieved successfully", chequeBook); } public async Task ListChequeBooksAsync(ListChequeBooksRequest request) { var page = request.Page is > 0 ? request.Page : null; var pageSize = request.PageSize is > 0 ? request.PageSize : null; var (items, totalCount) = await _chequeRepository.ListChequeBooksAsync( request.BankAccountId, request.BranchId, request.Status, page, pageSize); return ResponseHelpers.Ok("Cheque books retrieved successfully", new ChequeBookListResponse { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize }); } public async Task IssueChequePageAsync(string chequeNo, IssueChequePageRequest request) { var page = await _chequeRepository.GetChequePageByNoAsync(chequeNo) ?? throw new NotFoundException("Cheque page not found"); if (page.IssueStatus != ChequeIssueStatus.Unused) { throw new BadRequestException($"Cheque page must be Unused to be issued (currently {page.IssueStatus})"); } if (string.IsNullOrWhiteSpace(request.PayeeName)) { throw new BadRequestException("payeeName is required"); } if (request.Amount <= 0) { throw new BadRequestException("amount must be greater than 0"); } if (!Enum.TryParse(request.PayeeType, ignoreCase: true, out var payeeType)) { throw new BadRequestException( $"Invalid payeeType '{request.PayeeType}'. Must be one of: {string.Join(", ", Enum.GetNames())}."); } page.PayeeType = payeeType; page.PayeeId = request.PayeeId; page.PayeeName = request.PayeeName; page.IssueDate = request.IssueDate; page.Amount = request.Amount; page.CurrencyCode = string.IsNullOrWhiteSpace(request.CurrencyCode) ? "LKR" : request.CurrencyCode; page.VoucherId = request.VoucherId; page.ReferenceNo = request.ReferenceNo; page.Purpose = request.Purpose; page.IsCrossCheque = request.IsCrossCheque; page.IsAccountPayee = request.IsAccountPayee; page.IsPostDated = request.IsPostDated; page.Notes = request.Notes; page.PrintedBy = request.PrintedBy; page.PrintedAt = DateTime.UtcNow; page.IssueStatus = ChequeIssueStatus.Issued; await _unitOfWork.BeginAsync(); try { await _unitOfWork.SaveChangesAsync(); await _auditLogRepository.AddAsync(new AuditLog { Category = AuditCategory.Administrative, EntityName = "ChequePage", EntityId = page.ChequePageId.ToString(), Action = AuditAction.Update, Description = $"Cheque {page.ChequeNo} issued to {page.PayeeName} for {page.Amount}", PerformedBy = request.PrintedBy }); await _unitOfWork.SaveChangesAsync(); await _unitOfWork.CommitAsync(); } catch { await _unitOfWork.RollbackAsync(); throw; } return ResponseHelpers.Ok("Cheque page issued successfully", page); } public async Task UpdateChequePageStatusAsync(string chequeNo, Dictionary payload) { var rawAction = DataExtractor.GetRequiredString(payload, "action"); if (!Enum.TryParse(rawAction, ignoreCase: true, out var action)) { throw new BadRequestException( $"Invalid action '{rawAction}'. Must be one of: {string.Join(", ", Enum.GetNames())}."); } var page = await _chequeRepository.GetChequePageByNoAsync(chequeNo) ?? throw new NotFoundException("Cheque page not found"); string description; switch (action) { case ChequePageAction.Clear: if (page.IssueStatus != ChequeIssueStatus.Issued) { throw new BadRequestException($"Cheque must be Issued to be cleared (currently {page.IssueStatus})"); } page.ClearedDate = DataExtractor.GetRequiredDateOnly(payload, "clearedDate"); page.ClearedByBank = true; page.IssueStatus = ChequeIssueStatus.Cleared; description = $"Cheque {page.ChequeNo} cleared"; break; case ChequePageAction.Bounce: if (page.IssueStatus != ChequeIssueStatus.Issued) { throw new BadRequestException($"Cheque must be Issued to be bounced (currently {page.IssueStatus})"); } page.IssueStatus = ChequeIssueStatus.Bounced; description = $"Cheque {page.ChequeNo} bounced"; break; case ChequePageAction.Cancel: if (page.IssueStatus != ChequeIssueStatus.Unused && page.IssueStatus != ChequeIssueStatus.Issued) { throw new BadRequestException($"Cheque cannot be cancelled from status {page.IssueStatus}"); } page.CancelReason = DataExtractor.GetRequiredString(payload, "cancelReason"); page.IssueStatus = ChequeIssueStatus.Cancelled; description = $"Cheque {page.ChequeNo} cancelled"; break; case ChequePageAction.Void: if (page.IssueStatus != ChequeIssueStatus.Unused) { throw new BadRequestException($"Only an Unused cheque can be voided (currently {page.IssueStatus})"); } page.IssueStatus = ChequeIssueStatus.Void; description = $"Cheque {page.ChequeNo} voided"; break; default: throw new BadRequestException($"Invalid action '{rawAction}'."); } var performedBy = DataExtractor.GetOptionalString(payload, "performedBy"); await _unitOfWork.BeginAsync(); try { await _unitOfWork.SaveChangesAsync(); await _auditLogRepository.AddAsync(new AuditLog { Category = AuditCategory.Administrative, EntityName = "ChequePage", EntityId = page.ChequePageId.ToString(), Action = AuditAction.Update, Description = description, PerformedBy = performedBy }); await _unitOfWork.SaveChangesAsync(); await _unitOfWork.CommitAsync(); } catch { await _unitOfWork.RollbackAsync(); throw; } return ResponseHelpers.Ok("Cheque page status updated successfully", page); } public async Task GetChequePageByNoAsync(string chequeNo) { var page = await _chequeRepository.GetChequePageByNoAsync(chequeNo) ?? throw new NotFoundException("Cheque page not found"); return ResponseHelpers.Ok("Cheque page retrieved successfully", page); } public async Task ListChequePagesAsync(ListChequePagesRequest request) { var page = request.Page is > 0 ? request.Page : null; var pageSize = request.PageSize is > 0 ? request.PageSize : null; var (items, totalCount) = await _chequeRepository.ListChequePagesAsync( request.ChequeBookNo, request.IssueStatus, request.PayeeType, request.PayeeId, page, pageSize); return ResponseHelpers.Ok("Cheque pages retrieved successfully", new ChequePageListResponse { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize }); } public async Task CreateReceivedChequeAsync(CreateReceivedChequeRequest request) { if (string.IsNullOrWhiteSpace(request.ChequeNo)) { throw new BadRequestException("chequeNo is required"); } if (string.IsNullOrWhiteSpace(request.ReceivedFromName)) { throw new BadRequestException("receivedFromName is required"); } if (request.Amount <= 0) { throw new BadRequestException("amount must be greater than 0"); } if (!Enum.TryParse(request.ReceivedFromType, ignoreCase: true, out var receivedFromType)) { throw new BadRequestException( $"Invalid receivedFromType '{request.ReceivedFromType}'. Must be one of: {string.Join(", ", Enum.GetNames())}."); } var receivedCheque = new ReceivedCheque { CompanyId = request.CompanyId, BranchId = request.BranchId, ReceivedFromType = receivedFromType, ReceivedFromId = request.ReceivedFromId, ReceivedFromName = request.ReceivedFromName, DrawerBankName = request.DrawerBankName, DrawerBankBranch = request.DrawerBankBranch, AccountHolderName = request.AccountHolderName, ChequeNo = request.ChequeNo, ChequeDate = request.ChequeDate, Amount = request.Amount, ReceivedDate = request.ReceivedDate, Status = ReceivedChequeStatus.Received, ReferenceType = request.ReferenceType, ReferenceId = request.ReferenceId, Notes = request.Notes, CreatedBy = request.CreatedBy }; await _unitOfWork.BeginAsync(); try { await _chequeRepository.AddReceivedChequeAsync(receivedCheque); await _unitOfWork.SaveChangesAsync(); await _auditLogRepository.AddAsync(new AuditLog { Category = AuditCategory.Administrative, EntityName = "ReceivedCheque", EntityId = receivedCheque.ReceivedChequeId.ToString(), Action = AuditAction.Create, Description = $"Received cheque {receivedCheque.ChequeNo} from {receivedCheque.ReceivedFromName}", PerformedBy = request.CreatedBy }); await _unitOfWork.SaveChangesAsync(); await _unitOfWork.CommitAsync(); } catch { await _unitOfWork.RollbackAsync(); throw; } return ResponseHelpers.Created("Received cheque recorded successfully", receivedCheque); } public async Task UpdateReceivedChequeStatusAsync(long receivedChequeId, Dictionary payload) { var rawAction = DataExtractor.GetRequiredString(payload, "action"); if (!Enum.TryParse(rawAction, ignoreCase: true, out var action)) { throw new BadRequestException( $"Invalid action '{rawAction}'. Must be one of: {string.Join(", ", Enum.GetNames())}."); } var receivedCheque = await _chequeRepository.GetReceivedChequeByIdAsync(receivedChequeId) ?? throw new NotFoundException("Received cheque not found"); string description; switch (action) { case ReceivedChequeAction.Deposit: if (receivedCheque.Status != ReceivedChequeStatus.Received) { throw new BadRequestException($"Received cheque must be in Received status to be deposited (currently {receivedCheque.Status})"); } var depositBankAccountId = payload.TryGetValue("depositBankAccountId", out var depositElement) && depositElement.ValueKind != JsonValueKind.Null ? depositElement.GetInt64() : throw new BadRequestException("Missing required field 'depositBankAccountId'."); _ = await _bankRepository.GetByIdAsync(depositBankAccountId) ?? throw new NotFoundException("Deposit bank account not found"); receivedCheque.DepositBankAccountId = depositBankAccountId; receivedCheque.DepositDate = DataExtractor.GetRequiredDateOnly(payload, "depositDate"); receivedCheque.Status = ReceivedChequeStatus.Deposited; description = $"Received cheque {receivedCheque.ChequeNo} deposited"; break; case ReceivedChequeAction.Clear: if (receivedCheque.Status != ReceivedChequeStatus.Deposited) { throw new BadRequestException($"Received cheque must be Deposited to be cleared (currently {receivedCheque.Status})"); } receivedCheque.Status = ReceivedChequeStatus.Cleared; description = $"Received cheque {receivedCheque.ChequeNo} cleared"; break; case ReceivedChequeAction.Return: if (receivedCheque.Status != ReceivedChequeStatus.Deposited) { throw new BadRequestException($"Received cheque must be Deposited to be returned (currently {receivedCheque.Status})"); } receivedCheque.Status = ReceivedChequeStatus.Returned; description = $"Received cheque {receivedCheque.ChequeNo} returned"; break; case ReceivedChequeAction.Cancel: if (receivedCheque.Status != ReceivedChequeStatus.Received) { throw new BadRequestException($"Received cheque can only be cancelled from Received status (currently {receivedCheque.Status})"); } receivedCheque.Status = ReceivedChequeStatus.Cancelled; description = $"Received cheque {receivedCheque.ChequeNo} cancelled"; break; default: throw new BadRequestException($"Invalid action '{rawAction}'."); } var notes = DataExtractor.GetOptionalString(payload, "notes"); if (notes is not null) { receivedCheque.Notes = notes; } var performedBy = DataExtractor.GetOptionalString(payload, "performedBy"); await _unitOfWork.BeginAsync(); try { await _unitOfWork.SaveChangesAsync(); await _auditLogRepository.AddAsync(new AuditLog { Category = AuditCategory.Administrative, EntityName = "ReceivedCheque", EntityId = receivedCheque.ReceivedChequeId.ToString(), Action = AuditAction.Update, Description = description, PerformedBy = performedBy }); await _unitOfWork.SaveChangesAsync(); await _unitOfWork.CommitAsync(); } catch { await _unitOfWork.RollbackAsync(); throw; } return ResponseHelpers.Ok("Received cheque status updated successfully", receivedCheque); } public async Task GetReceivedChequeByIdAsync(long receivedChequeId) { var receivedCheque = await _chequeRepository.GetReceivedChequeByIdAsync(receivedChequeId) ?? throw new NotFoundException("Received cheque not found"); return ResponseHelpers.Ok("Received cheque retrieved successfully", receivedCheque); } public async Task ListReceivedChequesAsync(ListReceivedChequesRequest request) { var page = request.Page is > 0 ? request.Page : null; var pageSize = request.PageSize is > 0 ? request.PageSize : null; var (items, totalCount) = await _chequeRepository.ListReceivedChequesAsync( request.CompanyId, request.BranchId, request.Status, request.ReceivedFromType, page, pageSize); return ResponseHelpers.Ok("Received cheques retrieved successfully", new ReceivedChequeListResponse { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize }); } } }