using Microsoft.Extensions.Logging; using PowderCoating.Application.Interfaces; using PowderCoating.Core.Enums; using PowderCoating.Core.Interfaces; namespace PowderCoating.Infrastructure.Services; /// /// Atomic double-entry bookkeeping primitives used across InvoicesController, BillsController, /// and ExpensesController to keep the chart-of-accounts /// current after every financial transaction. /// public class AccountBalanceService : IAccountBalanceService { private readonly IUnitOfWork _unitOfWork; private readonly ILedgerService _ledgerService; private readonly ILogger _logger; /// /// Constructs the service with the required unit-of-work, ledger service for balance /// recalculation, and logger for error reporting during batch recalculations. /// public AccountBalanceService( IUnitOfWork unitOfWork, ILedgerService ledgerService, ILogger logger) { _unitOfWork = unitOfWork; _ledgerService = ledgerService; _logger = logger; } /// /// 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 is null or is zero /// so callers do not need null-guards for optional account mappings. /// 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); } /// /// 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 is null or is zero. /// 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); } /// /// Recomputes and persists CurrentBalance for every active account in the company /// by replaying all ledger activity via . /// Uses the epoch date 2000-01-01 as the range start so that LedgerService treats /// OpeningBalance 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. /// 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(); } /// /// Returns true for account sub-types whose normal balance is a debit /// (Assets, COGS, Expenses). This mirrors the identical helper in /// and is the single source of truth for how and /// decide the direction of the balance adjustment. /// private static bool IsNormalDebitBalance(AccountSubType subType) => subType switch { AccountSubType.Cash or 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 }; }