523 lines
22 KiB
C#
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 });
|
|
}
|
|
}
|
|
}
|