Distribution_GL Initialization
This commit is contained in:
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user