Distribution_GL Initialization
This commit is contained in:
@@ -0,0 +1,574 @@
|
||||
using ClosedXML.Excel;
|
||||
using General_Ledger_Service.DTOs.Bank;
|
||||
using General_Ledger_Service.DTOs.Common;
|
||||
using General_Ledger_Service.Enums;
|
||||
using General_Ledger_Service.Exceptions;
|
||||
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 BankService(
|
||||
IBankRepository bankRepository,
|
||||
IAccountRepository accountRepository,
|
||||
IJournalEntryRepository journalEntryRepository,
|
||||
IAuditLogRepository auditLogRepository,
|
||||
IUnitOfWork unitOfWork) : IBankService
|
||||
{
|
||||
private readonly IBankRepository _bankRepository = bankRepository;
|
||||
private readonly IAccountRepository _accountRepository = accountRepository;
|
||||
private readonly IJournalEntryRepository _journalEntryRepository = journalEntryRepository;
|
||||
private readonly IAuditLogRepository _auditLogRepository = auditLogRepository;
|
||||
private readonly IUnitOfWork _unitOfWork = unitOfWork;
|
||||
|
||||
// Retrofit 2026-07-31 - the caller no longer selects an existing GL account for a new
|
||||
// bank/cash account; this service auto-creates the whole backing GL structure instead:
|
||||
// Bank (root header, Asset, non-postable)
|
||||
// └─ <this bank account's own name> (leaf, postable) - one per BankAccount
|
||||
// Cash (root header, Asset, non-postable)
|
||||
// └─ <cash account type name, e.g. "Petty Cash"> (node, non-postable) - one per CashAccountType
|
||||
// └─ <this cash account's own name> (leaf, postable) - one per CashAccount
|
||||
// "Bank"/"Cash" and each type node are found-or-created by fixed name (GetOrCreateSystemRootAsync /
|
||||
// CashAccountType.GlAccountId), matching the user's explicit naming choice ("Bank"/"Cash",
|
||||
// not "Bank Accounts"/"Cash Accounts"). Depth here is fixed and shallow (2-3 levels,
|
||||
// well under COA_MAX_DEPTH's default of 6), so DepthGuard isn't invoked - it exists to
|
||||
// guard against unbounded user-driven nesting, not this fixed system structure.
|
||||
public async Task<ApiResponse> CreateBankAccountAsync(CreateBankAccountRequest request)
|
||||
{
|
||||
var currencyCode = string.IsNullOrWhiteSpace(request.CurrencyCode) ? "LKR" : request.CurrencyCode;
|
||||
|
||||
await _unitOfWork.BeginAsync();
|
||||
try
|
||||
{
|
||||
var bankRoot = await GetOrCreateSystemRootAsync("Bank");
|
||||
var glAccount = await CreateSystemChildAccountAsync(bankRoot, request.AccountName, isPostable: true, currencyCode);
|
||||
|
||||
var bankAccount = new BankAccount
|
||||
{
|
||||
AccountName = request.AccountName,
|
||||
BankName = request.BankName,
|
||||
AccountNumber = request.AccountNumber,
|
||||
GlAccountId = glAccount.AccountId,
|
||||
CurrencyCode = currencyCode
|
||||
};
|
||||
|
||||
await _bankRepository.AddAsync(bankAccount);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
await _auditLogRepository.AddAsync(new AuditLog
|
||||
{
|
||||
Category = AuditCategory.Administrative,
|
||||
EntityName = "BankAccount",
|
||||
EntityId = bankAccount.BankAccountId.ToString(),
|
||||
Action = AuditAction.Create,
|
||||
Description = $"Bank account {bankAccount.AccountName} created",
|
||||
PerformedBy = null
|
||||
});
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
return ResponseHelpers.Created("Bank account created successfully", bankAccount);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Finds an existing root Asset account with this exact name, or creates it - "Bank"/"Cash"
|
||||
// are system-owned singleton headers, always looked up by fixed name (never user-supplied
|
||||
// free text), so a collision would only ever be this same method racing itself.
|
||||
private async Task<Account> GetOrCreateSystemRootAsync(string name, short accountTypeId = 1)
|
||||
{
|
||||
var existing = await _accountRepository.GetByNameAndParentAsync(name, null);
|
||||
if (existing != null)
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var code = await AccountCodeGenerator.GenerateAsync(_accountRepository, accountTypeId, null);
|
||||
var account = new Account
|
||||
{
|
||||
AccountCode = code,
|
||||
AccountName = name,
|
||||
ParentAccountId = null,
|
||||
AccountTypeId = accountTypeId,
|
||||
IsControlAccount = true,
|
||||
IsPostable = false,
|
||||
CurrencyCode = "LKR",
|
||||
IsActive = true
|
||||
};
|
||||
|
||||
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} ({account.AccountName}) auto-created as a system GL header",
|
||||
PerformedBy = null
|
||||
});
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
// Always creates a new child account - used both for a type's own (non-postable) node
|
||||
// under "Cash" and for the (postable) leaf account backing one specific bank/cash account,
|
||||
// which is always a fresh 1:1 creation, never reused.
|
||||
private async Task<Account> CreateSystemChildAccountAsync(Account parent, string name, bool isPostable, string currencyCode)
|
||||
{
|
||||
var code = await AccountCodeGenerator.GenerateAsync(_accountRepository, parent.AccountTypeId, parent);
|
||||
var account = new Account
|
||||
{
|
||||
AccountCode = code,
|
||||
AccountName = name,
|
||||
ParentAccountId = parent.AccountId,
|
||||
AccountTypeId = parent.AccountTypeId,
|
||||
IsControlAccount = !isPostable,
|
||||
IsPostable = isPostable,
|
||||
CurrencyCode = currencyCode,
|
||||
IsActive = true
|
||||
};
|
||||
|
||||
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} ({account.AccountName}) auto-created as a system GL {(isPostable ? "account" : "header")}",
|
||||
PerformedBy = null
|
||||
});
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> ImportStatementLinesAsync(long bankAccountId, Stream fileStream)
|
||||
{
|
||||
var bankAccount = await _bankRepository.GetByIdAsync(bankAccountId)
|
||||
?? throw new NotFoundException("Bank account not found");
|
||||
|
||||
XLWorkbook workbook;
|
||||
IXLWorksheet worksheet;
|
||||
try
|
||||
{
|
||||
workbook = new XLWorkbook(fileStream);
|
||||
worksheet = workbook.Worksheets.First();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw new BadRequestException("Uploaded file is not a valid .xlsx spreadsheet");
|
||||
}
|
||||
|
||||
using (workbook)
|
||||
{
|
||||
return await ParseAndSaveAsync(bankAccount, worksheet);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ApiResponse> ParseAndSaveAsync(BankAccount bankAccount, IXLWorksheet worksheet)
|
||||
{
|
||||
var imported = new List<BankStatementLine>();
|
||||
var failedRows = new List<FailedRow>();
|
||||
|
||||
foreach (var row in worksheet.RowsUsed().Skip(1))
|
||||
{
|
||||
var rowNumber = row.RowNumber();
|
||||
|
||||
var txnDateCell = row.Cell(1);
|
||||
var descriptionCell = row.Cell(2);
|
||||
var amountCell = row.Cell(3);
|
||||
|
||||
if (!txnDateCell.TryGetValue(out DateTime txnDate))
|
||||
{
|
||||
failedRows.Add(new FailedRow { RowNumber = rowNumber, Reason = "txnDate is missing or not a valid date" });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!amountCell.TryGetValue(out decimal amount))
|
||||
{
|
||||
failedRows.Add(new FailedRow { RowNumber = rowNumber, Reason = "amount is missing or not a valid number" });
|
||||
continue;
|
||||
}
|
||||
|
||||
imported.Add(new BankStatementLine
|
||||
{
|
||||
BankAccountId = bankAccount.BankAccountId,
|
||||
TxnDate = DateOnly.FromDateTime(txnDate),
|
||||
Description = descriptionCell.IsEmpty() ? null : descriptionCell.GetString(),
|
||||
Amount = amount,
|
||||
IsReconciled = false
|
||||
});
|
||||
}
|
||||
|
||||
await _unitOfWork.BeginAsync();
|
||||
try
|
||||
{
|
||||
if (imported.Count > 0)
|
||||
{
|
||||
await _bankRepository.AddStatementLinesAsync(imported);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await _auditLogRepository.AddAsync(new AuditLog
|
||||
{
|
||||
Category = AuditCategory.Administrative,
|
||||
EntityName = "BankAccount",
|
||||
EntityId = bankAccount.BankAccountId.ToString(),
|
||||
Action = AuditAction.Create,
|
||||
Description = $"{imported.Count} statement line(s) imported, {failedRows.Count} row(s) failed",
|
||||
PerformedBy = null
|
||||
});
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
|
||||
return ResponseHelpers.Created("Bank statement import completed", new ImportStatementLinesResponse
|
||||
{
|
||||
Imported = imported,
|
||||
FailedRows = failedRows
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> ReconcileStatementLineAsync(long stmtLineId, ReconcileStatementLineRequest request)
|
||||
{
|
||||
var statementLine = await _bankRepository.GetStatementLineByIdAsync(stmtLineId)
|
||||
?? throw new NotFoundException("Bank statement line not found");
|
||||
|
||||
if (statementLine.IsReconciled)
|
||||
{
|
||||
throw new BadRequestException("Statement line is already reconciled");
|
||||
}
|
||||
|
||||
var journalEntry = await _journalEntryRepository.GetJournalEntryByJournalNoAsync(request.MatchedJournalNo)
|
||||
?? throw new NotFoundException("Journal entry not found");
|
||||
|
||||
if (!journalEntry.IsPosted)
|
||||
{
|
||||
throw new BadRequestException("Journal entry must be posted before it can be used for reconciliation");
|
||||
}
|
||||
|
||||
var bankAccount = await _bankRepository.GetByIdAsync(statementLine.BankAccountId)
|
||||
?? throw new NotFoundException("Bank account not found");
|
||||
|
||||
var netMovement = journalEntry.Lines
|
||||
.Where(l => l.AccountId == bankAccount.GlAccountId)
|
||||
.Sum(l => l.DebitAmount - l.CreditAmount);
|
||||
|
||||
if (netMovement != statementLine.Amount)
|
||||
{
|
||||
throw new BadRequestException(
|
||||
$"Statement line amount ({statementLine.Amount}) does not match the journal entry's movement on the bank's GL account ({netMovement})");
|
||||
}
|
||||
|
||||
statementLine.IsReconciled = true;
|
||||
statementLine.MatchedJournalId = journalEntry.JournalId;
|
||||
|
||||
await _unitOfWork.BeginAsync();
|
||||
try
|
||||
{
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
await _auditLogRepository.AddAsync(new AuditLog
|
||||
{
|
||||
Category = AuditCategory.Administrative,
|
||||
EntityName = "BankStatementLine",
|
||||
EntityId = statementLine.StmtLineId.ToString(),
|
||||
Action = AuditAction.Update,
|
||||
Description = $"Statement line {statementLine.StmtLineId} reconciled against journal entry {journalEntry.JournalNo}",
|
||||
PerformedBy = null
|
||||
});
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
|
||||
return ResponseHelpers.Ok("Statement line reconciled successfully", statementLine);
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> CreateCashAccountAsync(CreateCashAccountRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.CashAccountTypeName))
|
||||
{
|
||||
throw new BadRequestException("cashAccountTypeName is required");
|
||||
}
|
||||
|
||||
var currencyCode = string.IsNullOrWhiteSpace(request.CurrencyCode) ? "LKR" : request.CurrencyCode;
|
||||
var cashAccountType = await _bankRepository.GetCashAccountTypeByNameAsync(request.CashAccountTypeName.Trim());
|
||||
|
||||
var accountNumber = string.IsNullOrWhiteSpace(request.AccountNumber)
|
||||
? await GenerateCashAccountNumberAsync()
|
||||
: request.AccountNumber.Trim();
|
||||
|
||||
await _unitOfWork.BeginAsync();
|
||||
try
|
||||
{
|
||||
// "Other" flow: a cashAccountTypeName that doesn't match an existing row creates
|
||||
// one on the fly, so it appears in the type list for future selections.
|
||||
if (cashAccountType is null)
|
||||
{
|
||||
cashAccountType = await _bankRepository.AddCashAccountTypeAsync(new CashAccountType { Name = request.CashAccountTypeName.Trim() });
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var cashRoot = await GetOrCreateSystemRootAsync("Cash");
|
||||
|
||||
Account typeGlAccount;
|
||||
if (cashAccountType.GlAccountId is null)
|
||||
{
|
||||
// First time this type has ever backed a cash account - create its GL node
|
||||
// under "Cash" and persist the link on the type row itself, so every future
|
||||
// cash account of this type reuses the same node instead of creating a new one.
|
||||
typeGlAccount = await CreateSystemChildAccountAsync(cashRoot, cashAccountType.Name, isPostable: false, currencyCode: "LKR");
|
||||
cashAccountType.GlAccountId = typeGlAccount.AccountId;
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
typeGlAccount = await _accountRepository.GetByIdAsync(cashAccountType.GlAccountId.Value)
|
||||
?? throw new NotFoundException("Cash account type's GL header not found");
|
||||
}
|
||||
|
||||
var glAccount = await CreateSystemChildAccountAsync(typeGlAccount, request.AccountName, isPostable: true, currencyCode);
|
||||
|
||||
var cashAccount = new CashAccount
|
||||
{
|
||||
AccountName = request.AccountName,
|
||||
AccountNumber = accountNumber,
|
||||
CashAccountTypeId = cashAccountType.CashAccountTypeId,
|
||||
GlAccountId = glAccount.AccountId,
|
||||
CurrencyCode = currencyCode
|
||||
};
|
||||
|
||||
await _bankRepository.AddCashAccountAsync(cashAccount);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
await _auditLogRepository.AddAsync(new AuditLog
|
||||
{
|
||||
Category = AuditCategory.Administrative,
|
||||
EntityName = "CashAccount",
|
||||
EntityId = cashAccount.CashAccountId.ToString(),
|
||||
Action = AuditAction.Create,
|
||||
Description = $"Cash account {cashAccount.AccountName} created",
|
||||
PerformedBy = null
|
||||
});
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
return ResponseHelpers.Created("Cash account created successfully", cashAccount);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> GenerateCashAccountNumberAsync()
|
||||
{
|
||||
for (var seq = 1; seq <= 1000; seq++)
|
||||
{
|
||||
var candidate = $"CASH-{seq:D6}";
|
||||
if (!await _bankRepository.CashAccountNumberExistsAsync(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
throw new BadRequestException("Unable to auto-generate a unique cash account number - supply one explicitly");
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> ListCashAndBankAccountsAsync(string? accountType)
|
||||
{
|
||||
var filter = string.IsNullOrWhiteSpace(accountType) ? "Both" : accountType.Trim();
|
||||
if (!string.Equals(filter, "Cash", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(filter, "Bank", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(filter, "Both", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new BadRequestException($"Invalid accountType '{accountType}'. Must be one of: Cash, Bank, Both.");
|
||||
}
|
||||
|
||||
var combined = new List<CashAndBankAccountDto>();
|
||||
|
||||
if (!string.Equals(filter, "Cash", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var bankAccounts = await _bankRepository.GetAllAsync();
|
||||
combined.AddRange(bankAccounts.Select(b => new CashAndBankAccountDto
|
||||
{
|
||||
AccountType = "Bank",
|
||||
AccountId = b.BankAccountId,
|
||||
AccountName = b.AccountName,
|
||||
BankName = b.BankName,
|
||||
AccountNumber = b.AccountNumber,
|
||||
GlAccountId = b.GlAccountId,
|
||||
GlAccountCode = b.GlAccount.AccountCode,
|
||||
CurrencyCode = b.CurrencyCode,
|
||||
CreatedAt = b.CreatedAt
|
||||
}));
|
||||
}
|
||||
|
||||
if (!string.Equals(filter, "Bank", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var cashAccounts = await _bankRepository.GetAllCashAccountsAsync();
|
||||
combined.AddRange(cashAccounts.Select(c => new CashAndBankAccountDto
|
||||
{
|
||||
AccountType = "Cash",
|
||||
AccountId = c.CashAccountId,
|
||||
AccountName = c.AccountName,
|
||||
CashAccountTypeName = c.CashAccountType.Name,
|
||||
AccountNumber = c.AccountNumber,
|
||||
GlAccountId = c.GlAccountId,
|
||||
GlAccountCode = c.GlAccount.AccountCode,
|
||||
CurrencyCode = c.CurrencyCode,
|
||||
CreatedAt = c.CreatedAt,
|
||||
IsActive = c.IsActive,
|
||||
IsDefault = c.IsDefault
|
||||
}));
|
||||
}
|
||||
|
||||
return ResponseHelpers.Ok("Cash and bank accounts retrieved successfully", combined.OrderBy(a => a.AccountName).ToList());
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> ListCashAccountTypesAsync()
|
||||
{
|
||||
var types = await _bankRepository.GetAllCashAccountTypesAsync();
|
||||
return ResponseHelpers.Ok("Cash account types retrieved successfully", types);
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> UpdateCashAccountAsync(long cashAccountId, UpdateCashAccountRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.AccountName))
|
||||
{
|
||||
throw new BadRequestException("accountName is required");
|
||||
}
|
||||
|
||||
var cashAccount = await _bankRepository.GetCashAccountByIdAsync(cashAccountId)
|
||||
?? throw new NotFoundException("Cash account not found");
|
||||
|
||||
await _unitOfWork.BeginAsync();
|
||||
try
|
||||
{
|
||||
cashAccount.AccountName = request.AccountName.Trim();
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
await _auditLogRepository.AddAsync(new AuditLog
|
||||
{
|
||||
Category = AuditCategory.Administrative,
|
||||
EntityName = "CashAccount",
|
||||
EntityId = cashAccount.CashAccountId.ToString(),
|
||||
Action = AuditAction.Update,
|
||||
Description = $"Cash account renamed to {cashAccount.AccountName}",
|
||||
PerformedBy = null
|
||||
});
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
return ResponseHelpers.Ok("Cash account updated successfully", cashAccount);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> SetCashAccountStatusAsync(long cashAccountId, SetCashAccountStatusRequest request)
|
||||
{
|
||||
var cashAccount = await _bankRepository.GetCashAccountByIdAsync(cashAccountId)
|
||||
?? throw new NotFoundException("Cash account not found");
|
||||
|
||||
if (!request.IsActive && cashAccount.IsDefault)
|
||||
{
|
||||
throw new BadRequestException($"'{cashAccount.AccountName}' is the default cash account and cannot be deactivated.");
|
||||
}
|
||||
|
||||
await _unitOfWork.BeginAsync();
|
||||
try
|
||||
{
|
||||
cashAccount.IsActive = request.IsActive;
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
await _auditLogRepository.AddAsync(new AuditLog
|
||||
{
|
||||
Category = AuditCategory.Administrative,
|
||||
EntityName = "CashAccount",
|
||||
EntityId = cashAccount.CashAccountId.ToString(),
|
||||
Action = AuditAction.Update,
|
||||
Description = $"Cash account {cashAccount.AccountName} {(request.IsActive ? "activated" : "deactivated")}",
|
||||
PerformedBy = null
|
||||
});
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
return ResponseHelpers.Ok("Cash account status updated successfully", cashAccount);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> DeleteCashAccountAsync(long cashAccountId)
|
||||
{
|
||||
var cashAccount = await _bankRepository.GetCashAccountByIdAsync(cashAccountId)
|
||||
?? throw new NotFoundException("Cash account not found");
|
||||
|
||||
if (cashAccount.IsDefault)
|
||||
{
|
||||
throw new BadRequestException($"'{cashAccount.AccountName}' is the default cash account and cannot be deleted.");
|
||||
}
|
||||
|
||||
await _unitOfWork.BeginAsync();
|
||||
try
|
||||
{
|
||||
_bankRepository.RemoveCashAccount(cashAccount);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
await _auditLogRepository.AddAsync(new AuditLog
|
||||
{
|
||||
Category = AuditCategory.Administrative,
|
||||
EntityName = "CashAccount",
|
||||
EntityId = cashAccount.CashAccountId.ToString(),
|
||||
Action = AuditAction.Delete,
|
||||
Description = $"Cash account {cashAccount.AccountName} deleted",
|
||||
PerformedBy = null
|
||||
});
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
return ResponseHelpers.Ok("Cash account deleted successfully", null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user