Files

523 lines
22 KiB
C#

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<ApiResponse> 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<string> 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<string>();
for (var n = start; n <= end; n++)
{
result.Add(n.ToString().PadLeft(width, '0'));
}
return result;
}
public async Task<ApiResponse> 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<ApiResponse> 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<ApiResponse> 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<ChequePayeeType>(request.PayeeType, ignoreCase: true, out var payeeType))
{
throw new BadRequestException(
$"Invalid payeeType '{request.PayeeType}'. Must be one of: {string.Join(", ", Enum.GetNames<ChequePayeeType>())}.");
}
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<ApiResponse> UpdateChequePageStatusAsync(string chequeNo, Dictionary<string, JsonElement> payload)
{
var rawAction = DataExtractor.GetRequiredString(payload, "action");
if (!Enum.TryParse<ChequePageAction>(rawAction, ignoreCase: true, out var action))
{
throw new BadRequestException(
$"Invalid action '{rawAction}'. Must be one of: {string.Join(", ", Enum.GetNames<ChequePageAction>())}.");
}
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<ApiResponse> 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<ApiResponse> 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<ApiResponse> 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<ReceivedChequeFromType>(request.ReceivedFromType, ignoreCase: true, out var receivedFromType))
{
throw new BadRequestException(
$"Invalid receivedFromType '{request.ReceivedFromType}'. Must be one of: {string.Join(", ", Enum.GetNames<ReceivedChequeFromType>())}.");
}
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<ApiResponse> UpdateReceivedChequeStatusAsync(long receivedChequeId, Dictionary<string, JsonElement> payload)
{
var rawAction = DataExtractor.GetRequiredString(payload, "action");
if (!Enum.TryParse<ReceivedChequeAction>(rawAction, ignoreCase: true, out var action))
{
throw new BadRequestException(
$"Invalid action '{rawAction}'. Must be one of: {string.Join(", ", Enum.GetNames<ReceivedChequeAction>())}.");
}
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<ApiResponse> 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<ApiResponse> 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 });
}
}
}