Initial commit
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PowderCoating.Application.Interfaces;
|
||||
using PowderCoating.Core.Enums;
|
||||
using PowderCoating.Core.Interfaces;
|
||||
|
||||
namespace PowderCoating.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Atomic double-entry bookkeeping primitives used across InvoicesController, BillsController,
|
||||
/// and ExpensesController to keep the chart-of-accounts <see cref="PowderCoating.Core.Entities.Account.CurrentBalance"/>
|
||||
/// current after every financial transaction.
|
||||
/// </summary>
|
||||
public class AccountBalanceService : IAccountBalanceService
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILedgerService _ledgerService;
|
||||
private readonly ILogger<AccountBalanceService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs the service with the required unit-of-work, ledger service for balance
|
||||
/// recalculation, and logger for error reporting during batch recalculations.
|
||||
/// </summary>
|
||||
public AccountBalanceService(
|
||||
IUnitOfWork unitOfWork,
|
||||
ILedgerService ledgerService,
|
||||
ILogger<AccountBalanceService> logger)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_ledgerService = ledgerService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a debit against the specified account and persists the updated balance.
|
||||
/// For debit-normal accounts (Assets, Expenses, COGS) a debit increases the balance;
|
||||
/// for credit-normal accounts (Liabilities, Equity, Revenue) it decreases it.
|
||||
/// Silently no-ops when <paramref name="accountId"/> is null or <paramref name="amount"/> is zero
|
||||
/// so callers do not need null-guards for optional account mappings.
|
||||
/// </summary>
|
||||
public async Task DebitAsync(int? accountId, decimal amount)
|
||||
{
|
||||
if (accountId == null || amount == 0) return;
|
||||
|
||||
var account = await _unitOfWork.Accounts.GetByIdAsync(accountId.Value);
|
||||
if (account == null) return;
|
||||
|
||||
// Debit increases debit-normal accounts (Assets/Expenses/COGS)
|
||||
// Debit decreases credit-normal accounts (Liabilities/Equity/Revenue)
|
||||
account.CurrentBalance += IsNormalDebitBalance(account.AccountSubType) ? amount : -amount;
|
||||
await _unitOfWork.Accounts.UpdateAsync(account);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a credit against the specified account and persists the updated balance.
|
||||
/// For credit-normal accounts (Liabilities, Equity, Revenue) a credit increases the balance;
|
||||
/// for debit-normal accounts (Assets, Expenses, COGS) it decreases it.
|
||||
/// Silently no-ops when <paramref name="accountId"/> is null or <paramref name="amount"/> is zero.
|
||||
/// </summary>
|
||||
public async Task CreditAsync(int? accountId, decimal amount)
|
||||
{
|
||||
if (accountId == null || amount == 0) return;
|
||||
|
||||
var account = await _unitOfWork.Accounts.GetByIdAsync(accountId.Value);
|
||||
if (account == null) return;
|
||||
|
||||
// Credit decreases debit-normal accounts (Assets/Expenses/COGS)
|
||||
// Credit increases credit-normal accounts (Liabilities/Equity/Revenue)
|
||||
account.CurrentBalance += IsNormalDebitBalance(account.AccountSubType) ? -amount : amount;
|
||||
await _unitOfWork.Accounts.UpdateAsync(account);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recomputes and persists <c>CurrentBalance</c> for every active account in the company
|
||||
/// by replaying all ledger activity via <see cref="LedgerService.GetAccountLedgerAsync"/>.
|
||||
/// Uses the epoch date 2000-01-01 as the range start so that LedgerService treats
|
||||
/// <c>OpeningBalance</c> as a prior balance and all real transactions fall within the window.
|
||||
/// Errors on individual accounts are logged and swallowed so a single bad account does not
|
||||
/// abort the full recalculation.
|
||||
/// </summary>
|
||||
public async Task RecalculateAllAsync(int companyId)
|
||||
{
|
||||
var accounts = await _unitOfWork.Accounts.FindAsync(a => a.CompanyId == companyId && a.IsActive);
|
||||
|
||||
// Use a date range that covers all possible transactions.
|
||||
// LedgerService computes opening balance separately, so starting from 2000-01-01
|
||||
// means priorBalance = OpeningBalance + 0 (no transactions before that date),
|
||||
// and all real transactions fall inside the period window.
|
||||
var from = new DateTime(2000, 1, 1);
|
||||
var to = DateTime.UtcNow;
|
||||
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ledger = await _ledgerService.GetAccountLedgerAsync(account.Id, from, to);
|
||||
if (ledger != null)
|
||||
{
|
||||
account.CurrentBalance = ledger.ClosingBalance;
|
||||
await _unitOfWork.Accounts.UpdateAsync(account);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error recalculating balance for account {AccountId} ({AccountNumber})",
|
||||
account.Id, account.AccountNumber);
|
||||
}
|
||||
}
|
||||
|
||||
await _unitOfWork.CompleteAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <c>true</c> for account sub-types whose normal balance is a debit
|
||||
/// (Assets, COGS, Expenses). This mirrors the identical helper in <see cref="LedgerService"/>
|
||||
/// and is the single source of truth for how <see cref="DebitAsync"/> and <see cref="CreditAsync"/>
|
||||
/// decide the direction of the balance adjustment.
|
||||
/// </summary>
|
||||
private static bool IsNormalDebitBalance(AccountSubType subType) => subType switch
|
||||
{
|
||||
AccountSubType.Checking
|
||||
or AccountSubType.Savings
|
||||
or AccountSubType.AccountsReceivable
|
||||
or AccountSubType.Inventory
|
||||
or AccountSubType.FixedAsset
|
||||
or AccountSubType.OtherCurrentAsset
|
||||
or AccountSubType.OtherAsset => true,
|
||||
|
||||
AccountSubType.CostOfGoodsSold => true,
|
||||
|
||||
// Expense subtypes (enum values ≥ 50) → normal debit balance
|
||||
var st when (int)st >= 50 => true,
|
||||
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user