226 lines
10 KiB
C#
226 lines
10 KiB
C#
using System.Text.Json;
|
|
using General_Ledger_Service.DTOs.Accounts;
|
|
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 AccountService(
|
|
IAccountRepository accountRepository,
|
|
ISystemConfigRepository systemConfigRepository,
|
|
IAuditLogRepository auditLogRepository,
|
|
IUnitOfWork unitOfWork) : IAccountService
|
|
{
|
|
private readonly IAccountRepository _accountRepository = accountRepository;
|
|
private readonly ISystemConfigRepository _systemConfigRepository = systemConfigRepository;
|
|
private readonly IAuditLogRepository _auditLogRepository = auditLogRepository;
|
|
private readonly IUnitOfWork _unitOfWork = unitOfWork;
|
|
|
|
public async Task<ApiResponse> CreateAccountAsync(CreateAccountRequest request)
|
|
{
|
|
Account? parent = null;
|
|
|
|
if (request.ParentAccountId.HasValue)
|
|
{
|
|
parent = await _accountRepository.GetByIdAsync(request.ParentAccountId.Value) ?? throw new NotFoundException("Parent account not found");
|
|
var resultingDepth = await _accountRepository.GetAncestorDepthAsync(parent.AccountId);
|
|
await DepthGuard.EnsureWithinMaxDepthAsync(
|
|
_systemConfigRepository, "COA_MAX_DEPTH", "COA_ENFORCE_MAX_DEPTH", resultingDepth, "Maximum Chart of Accounts depth exceeded");
|
|
}
|
|
|
|
var accountCode = await AccountCodeGenerator.GenerateAsync(_accountRepository, request.AccountTypeId, parent);
|
|
|
|
var account = new Account
|
|
{
|
|
AccountCode = accountCode,
|
|
AccountName = request.AccountName,
|
|
ParentAccountId = request.ParentAccountId,
|
|
AccountTypeId = request.AccountTypeId,
|
|
IsControlAccount = request.IsControlAccount,
|
|
IsPostable = request.IsPostable,
|
|
CurrencyCode = string.IsNullOrWhiteSpace(request.CurrencyCode) ? "LKR" : request.CurrencyCode,
|
|
IsActive = true
|
|
};
|
|
|
|
await _unitOfWork.BeginAsync();
|
|
try
|
|
{
|
|
await _accountRepository.AddAsync(account);
|
|
await _unitOfWork.SaveChangesAsync();
|
|
|
|
await _auditLogRepository.AddAsync(new AuditLog
|
|
{
|
|
Category = AuditCategory.Ledger,
|
|
EntityName = "Account",
|
|
EntityId = account.AccountId.ToString(),
|
|
Action = AuditAction.Create,
|
|
Description = $"Account {account.AccountCode} created",
|
|
PerformedBy = null
|
|
});
|
|
await _unitOfWork.SaveChangesAsync();
|
|
|
|
await _unitOfWork.CommitAsync();
|
|
}
|
|
catch
|
|
{
|
|
await _unitOfWork.RollbackAsync();
|
|
throw;
|
|
}
|
|
|
|
return ResponseHelpers.Created("Account created successfully", account);
|
|
}
|
|
|
|
public async Task<ApiResponse> GetAccountByCodeAsync(string accountCode, string? expand)
|
|
{
|
|
var account = await _accountRepository.GetByCodeAsync(accountCode) ?? throw new NotFoundException("Account not found");
|
|
|
|
var breadcrumb = await _accountRepository.GetAncestorBreadcrumbAsync(account.AccountId);
|
|
|
|
List<AccountTreeNode>? descendantTree = null;
|
|
if (string.Equals(expand, "tree", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
descendantTree = await _accountRepository.GetDescendantTreeAsync(account.AccountId);
|
|
}
|
|
|
|
var data = new AccountDetailResponse
|
|
{
|
|
Account = account,
|
|
AncestorBreadcrumb = breadcrumb,
|
|
DescendantTree = descendantTree
|
|
};
|
|
|
|
return ResponseHelpers.Ok("Account retrieved successfully", data);
|
|
}
|
|
|
|
public async Task<ApiResponse> UpdateAccountAsync(string accountCode, Dictionary<string, JsonElement> payload)
|
|
{
|
|
var request = new UpdateAccountRequest
|
|
{
|
|
AccountName = DataExtractor.GetOptionalString(payload, "accountName"),
|
|
AccountTypeId = (short?)DataExtractor.GetNullableInt(payload, "accountTypeId"),
|
|
IsControlAccount = GetOptionalBool(payload, "isControlAccount"),
|
|
IsPostable = GetOptionalBool(payload, "isPostable"),
|
|
CurrencyCode = DataExtractor.GetOptionalString(payload, "currencyCode"),
|
|
CashFlowCategory = DataExtractor.GetOptionalString(payload, "cashFlowCategory"),
|
|
IncomeStatementSection = DataExtractor.GetOptionalString(payload, "incomeStatementSection"),
|
|
BalanceSheetClassification = DataExtractor.GetOptionalString(payload, "balanceSheetClassification"),
|
|
IsTaxDeductible = GetOptionalBool(payload, "isTaxDeductible"),
|
|
IsActive = GetOptionalBool(payload, "isActive"),
|
|
ParentAccountIdProvided = payload.ContainsKey("parentAccountId"),
|
|
ParentAccountId = payload.TryGetValue("parentAccountId", out var parentElement) && parentElement.ValueKind != JsonValueKind.Null
|
|
? parentElement.GetInt64()
|
|
: null
|
|
};
|
|
|
|
var account = await _accountRepository.GetByCodeAsync(accountCode) ?? throw new NotFoundException("Account not found");
|
|
|
|
account.AccountName = request.AccountName ?? account.AccountName;
|
|
account.AccountTypeId = request.AccountTypeId ?? account.AccountTypeId;
|
|
account.IsControlAccount = request.IsControlAccount ?? account.IsControlAccount;
|
|
account.IsPostable = request.IsPostable ?? account.IsPostable;
|
|
account.CurrencyCode = request.CurrencyCode ?? account.CurrencyCode;
|
|
account.CashFlowCategory = request.CashFlowCategory ?? account.CashFlowCategory;
|
|
account.IncomeStatementSection = request.IncomeStatementSection ?? account.IncomeStatementSection;
|
|
account.BalanceSheetClassification = request.BalanceSheetClassification ?? account.BalanceSheetClassification;
|
|
account.IsTaxDeductible = request.IsTaxDeductible ?? account.IsTaxDeductible;
|
|
account.IsActive = request.IsActive ?? account.IsActive;
|
|
|
|
var parentChanged = false;
|
|
|
|
if (request.ParentAccountIdProvided && request.ParentAccountId != account.ParentAccountId)
|
|
{
|
|
parentChanged = true;
|
|
var newParentId = request.ParentAccountId;
|
|
|
|
if (newParentId.HasValue)
|
|
{
|
|
if (newParentId.Value == account.AccountId)
|
|
{
|
|
throw new BadRequestException("An account cannot be its own parent");
|
|
}
|
|
|
|
_ = await _accountRepository.GetByIdAsync(newParentId.Value)
|
|
?? throw new NotFoundException("Parent account not found");
|
|
|
|
var wouldCycle = await _accountRepository.WouldCreateCycleAsync(account.AccountId, newParentId.Value);
|
|
if (wouldCycle)
|
|
{
|
|
throw new BadRequestException("Cannot re-parent: the new parent is a descendant of this account");
|
|
}
|
|
|
|
var resultingDepth = await _accountRepository.GetAncestorDepthAsync(newParentId.Value);
|
|
await DepthGuard.EnsureWithinMaxDepthAsync(
|
|
_systemConfigRepository, "COA_MAX_DEPTH", "COA_ENFORCE_MAX_DEPTH", resultingDepth, "Maximum Chart of Accounts depth exceeded");
|
|
}
|
|
|
|
account.ParentAccountId = newParentId;
|
|
}
|
|
|
|
await _unitOfWork.BeginAsync();
|
|
try
|
|
{
|
|
await _auditLogRepository.AddAsync(new AuditLog
|
|
{
|
|
Category = parentChanged ? AuditCategory.Ledger : AuditCategory.Administrative,
|
|
EntityName = "Account",
|
|
EntityId = account.AccountId.ToString(),
|
|
Action = AuditAction.Update,
|
|
Description = $"Account {account.AccountCode} updated",
|
|
PerformedBy = null
|
|
});
|
|
await _unitOfWork.SaveChangesAsync();
|
|
await _unitOfWork.CommitAsync();
|
|
}
|
|
catch
|
|
{
|
|
await _unitOfWork.RollbackAsync();
|
|
throw;
|
|
}
|
|
|
|
return ResponseHelpers.Ok("Account updated successfully", account);
|
|
}
|
|
|
|
public async Task<ApiResponse> ListAccountsAsync(ListAccountsRequest request)
|
|
{
|
|
// No default page/pageSize - paging is only applied when the caller explicitly
|
|
// supplies both (RepositoryHelpers.ApplyPaging); a non-positive value is treated as
|
|
// "not supplied" rather than silently substituted with one.
|
|
var page = request.Page is > 0 ? request.Page : null;
|
|
var pageSize = request.PageSize is > 0 ? request.PageSize : null;
|
|
var paged = page.HasValue && pageSize.HasValue;
|
|
|
|
var (items, totalCount) = await _accountRepository.SearchAsync(
|
|
request.AccountTypeId, request.IsActive, request.IsPostable, page, pageSize);
|
|
|
|
var data = new AccountListResponse
|
|
{
|
|
Items = items,
|
|
TotalCount = totalCount,
|
|
Page = paged ? page : null,
|
|
PageSize = paged ? pageSize : null
|
|
};
|
|
|
|
return ResponseHelpers.Ok("Accounts retrieved successfully", data);
|
|
}
|
|
|
|
private static bool? GetOptionalBool(Dictionary<string, JsonElement> data, string key)
|
|
{
|
|
if (!data.TryGetValue(key, out var element) || element.ValueKind == JsonValueKind.Null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (element.ValueKind == JsonValueKind.True) return true;
|
|
if (element.ValueKind == JsonValueKind.False) return false;
|
|
|
|
throw new BadRequestException($"Field '{key}' must be a valid boolean.");
|
|
}
|
|
}
|
|
}
|