Distribution_GL Initialization

This commit is contained in:
2026-10-20 13:07:07 +05:30
commit 3f16e0cb44
194 changed files with 35659 additions and 0 deletions
+225
View File
@@ -0,0 +1,225 @@
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.");
}
}
}