using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PowderCoating.Application.DTOs.Accounting;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Enums;
using PowderCoating.Infrastructure.Data;
namespace PowderCoating.Infrastructure.Services;
///
/// Builds chronological ledger reports for individual accounts by querying every transaction
/// table that can touch a given account (Payments, Expenses, BillPayments, InvoiceItems, Bills,
/// AR, AP) and computing a running balance. Also drives
/// by returning the authoritative closing balance.
///
public class LedgerService : ILedgerService
{
private readonly ApplicationDbContext _context;
private readonly ILogger _logger;
///
/// Constructs the service with direct access.
/// Direct context access (rather than repositories) is intentional here: the ledger
/// must query multiple tables with heterogeneous filters and EF navigation properties,
/// which would require many separate repository calls that would be slower and less readable.
///
public LedgerService(ApplicationDbContext context, ILogger logger)
{
_context = context;
_logger = logger;
}
///
/// Assembles a full ledger report for an account over the given date range, including
/// nine distinct transaction sources: customer payments deposited, expenses paid from,
/// bill payments paid from, invoice revenue line items, sales tax, direct expense
/// categorizations, bill line-item categorizations, AR movements, and AP movements.
/// Returns null when the account does not exist or is filtered out by the global
/// query filters (company isolation / soft delete).
/// The OpeningBalance in the returned DTO represents the account balance on the day
/// before , computed by .
///
public async Task GetAccountLedgerAsync(int accountId, DateTime from, DateTime to)
{
// Use FirstOrDefaultAsync so global query filters (company isolation, soft delete) apply
var account = await _context.Accounts.FirstOrDefaultAsync(a => a.Id == accountId);
if (account == null) return null;
var fromDate = from.Date;
var toDate = to.Date.AddDays(1).AddTicks(-1); // inclusive end of day
var entries = new List();
// ── 1. Customer payments deposited INTO this account (DEBIT) ──────────
// e.g. Checking/Savings account receives a customer payment
var depositedPayments = await _context.Payments
.Include(p => p.Invoice)
.Where(p => p.DepositAccountId == accountId
&& p.PaymentDate >= fromDate && p.PaymentDate <= toDate)
.ToListAsync();
foreach (var p in depositedPayments)
entries.Add(new LedgerEntryDto
{
Date = p.PaymentDate,
Reference = p.Invoice?.InvoiceNumber ?? $"PMT-{p.Id}",
Source = "Customer Payment",
Description = p.Notes ?? p.Reference,
Debit = p.Amount,
Credit = 0,
LinkController = "Invoices",
LinkId = p.InvoiceId
});
// Customer deposits recorded to this account (DEBIT — cash received at deposit time)
var depositedDeposits = await _context.Deposits
.Where(d => d.DepositAccountId == accountId
&& d.ReceivedDate >= fromDate && d.ReceivedDate <= toDate)
.ToListAsync();
foreach (var d in depositedDeposits)
entries.Add(new LedgerEntryDto
{
Date = d.ReceivedDate,
Reference = d.ReceiptNumber,
Source = "Customer Deposit",
Description = d.Notes ?? d.Reference,
Debit = d.Amount,
Credit = 0,
LinkController = "Jobs",
LinkId = d.JobId
});
// Refunds paid FROM this account (CREDIT — cash leaves)
var refundsPaidFrom = await _context.Refunds
.Include(r => r.Invoice)
.Where(r => r.DepositAccountId == accountId
&& r.RefundDate >= fromDate && r.RefundDate <= toDate)
.ToListAsync();
foreach (var r in refundsPaidFrom)
entries.Add(new LedgerEntryDto
{
Date = r.RefundDate,
Reference = r.Reference ?? $"REF-{r.Id}",
Source = "Refund",
Description = r.Reason,
Debit = 0,
Credit = r.Amount,
LinkController = "Invoices",
LinkId = r.InvoiceId
});
// ── 2. Direct expenses paid FROM this account (CREDIT) ────────────────
// e.g. Checking account used to pay an expense
var expensesPaidFrom = await _context.Expenses
.Include(e => e.Vendor)
.Where(e => e.PaymentAccountId == accountId
&& e.Date >= fromDate && e.Date <= toDate)
.ToListAsync();
foreach (var e in expensesPaidFrom)
entries.Add(new LedgerEntryDto
{
Date = e.Date,
Reference = e.ExpenseNumber,
Source = "Expense",
Description = e.Memo ?? e.Vendor?.CompanyName,
Debit = 0,
Credit = e.Amount,
LinkController = "Expenses",
LinkId = e.Id
});
// ── 3. Bill payments made FROM this account (CREDIT) ──────────────────
// e.g. Checking account used to pay a vendor bill
var billPaymentsPaidFrom = await _context.BillPayments
.Include(bp => bp.Bill)
.Where(bp => bp.BankAccountId == accountId
&& bp.PaymentDate >= fromDate && bp.PaymentDate <= toDate)
.ToListAsync();
foreach (var bp in billPaymentsPaidFrom)
entries.Add(new LedgerEntryDto
{
Date = bp.PaymentDate,
Reference = bp.PaymentNumber,
Source = "Bill Payment",
Description = bp.Memo ?? bp.Bill?.BillNumber,
Debit = 0,
Credit = bp.Amount,
LinkController = "Bills",
LinkId = bp.BillId
});
// ── 4. Invoice line items that post revenue to this account (CREDIT) ──
// e.g. Revenue account 4000 receives line-item revenue
var revenueItems = await _context.InvoiceItems
.Include(ii => ii.Invoice)
.Where(ii => ii.RevenueAccountId == accountId
&& ii.Invoice.Status != InvoiceStatus.Draft
&& ii.Invoice.Status != InvoiceStatus.Voided
&& ii.Invoice.InvoiceDate >= fromDate && ii.Invoice.InvoiceDate <= toDate)
.ToListAsync();
foreach (var item in revenueItems)
entries.Add(new LedgerEntryDto
{
Date = item.Invoice.InvoiceDate,
Reference = item.Invoice.InvoiceNumber,
Source = "Invoice",
Description = item.Description,
Debit = 0,
Credit = item.TotalPrice,
LinkController = "Invoices",
LinkId = item.InvoiceId
});
// ── 5. Sales tax collected to this account (CREDIT) ───────────────────
// e.g. Liability account 2200 receives sales tax from invoices
var taxInvoices = await _context.Invoices
.Where(i => i.SalesTaxAccountId == accountId
&& i.TaxAmount > 0
&& i.Status != InvoiceStatus.Draft
&& i.Status != InvoiceStatus.Voided
&& i.InvoiceDate >= fromDate && i.InvoiceDate <= toDate)
.ToListAsync();
foreach (var inv in taxInvoices)
entries.Add(new LedgerEntryDto
{
Date = inv.InvoiceDate,
Reference = inv.InvoiceNumber,
Source = "Sales Tax",
Description = $"Tax on {inv.InvoiceNumber}",
Debit = 0,
Credit = inv.TaxAmount,
LinkController = "Invoices",
LinkId = inv.Id
});
// ── 6. Direct expenses categorized to this account (DEBIT) ────────────
// e.g. Expense account 6200 receives direct expense entries
var expensesTo = await _context.Expenses
.Include(e => e.Vendor)
.Where(e => e.ExpenseAccountId == accountId
&& e.Date >= fromDate && e.Date <= toDate)
.ToListAsync();
foreach (var e in expensesTo)
entries.Add(new LedgerEntryDto
{
Date = e.Date,
Reference = e.ExpenseNumber,
Source = "Expense",
Description = e.Memo ?? e.Vendor?.CompanyName,
Debit = e.Amount,
Credit = 0,
LinkController = "Expenses",
LinkId = e.Id
});
// ── 7. Bill line items categorized to this account (DEBIT) ────────────
// e.g. Expense/COGS account receives costs from vendor bill line items
var billLineItems = await _context.BillLineItems
.Include(bli => bli.Bill)
.Where(bli => bli.AccountId == accountId
&& bli.Bill.Status != BillStatus.Draft
&& bli.Bill.Status != BillStatus.Voided
&& bli.Bill.BillDate >= fromDate && bli.Bill.BillDate <= toDate)
.ToListAsync();
foreach (var bli in billLineItems)
entries.Add(new LedgerEntryDto
{
Date = bli.Bill.BillDate,
Reference = bli.Bill.BillNumber,
Source = "Bill",
Description = bli.Description,
Debit = bli.Amount,
Credit = 0,
LinkController = "Bills",
LinkId = bli.BillId
});
// ── 8. Accounts Receivable ─────────────────────────────────────────────
// Invoice creation increases AR (DEBIT); customer payments reduce AR (CREDIT)
if (account.AccountSubType == AccountSubType.AccountsReceivable)
{
var arInvoices = await _context.Invoices
.Include(i => i.Customer)
.Where(i => i.Status != InvoiceStatus.Draft
&& i.Status != InvoiceStatus.Voided
&& i.InvoiceDate >= fromDate && i.InvoiceDate <= toDate)
.ToListAsync();
foreach (var inv in arInvoices)
{
var customerName = inv.Customer?.IsCommercial == true
? inv.Customer.CompanyName
: $"{inv.Customer?.ContactFirstName} {inv.Customer?.ContactLastName}".Trim();
entries.Add(new LedgerEntryDto
{
Date = inv.InvoiceDate,
Reference = inv.InvoiceNumber,
Source = "Invoice",
Description = customerName,
Debit = inv.Total,
Credit = 0,
LinkController = "Invoices",
LinkId = inv.Id
});
}
var arPayments = await _context.Payments
.Include(p => p.Invoice)
.Where(p => p.PaymentDate >= fromDate && p.PaymentDate <= toDate)
.ToListAsync();
foreach (var p in arPayments)
entries.Add(new LedgerEntryDto
{
Date = p.PaymentDate,
Reference = p.Invoice?.InvoiceNumber ?? $"PMT-{p.Id}",
Source = "Invoice Payment",
Description = $"Payment — {p.Invoice?.InvoiceNumber}",
Debit = 0,
Credit = p.Amount,
LinkController = "Invoices",
LinkId = p.InvoiceId
});
// Credit memo applications reduce open AR (CREDIT)
var arCreditMemos = await _context.CreditMemoApplications
.Include(a => a.Invoice)
.Include(a => a.CreditMemo)
.Where(a => a.AppliedDate >= fromDate && a.AppliedDate <= toDate
&& a.Invoice.Status != InvoiceStatus.Voided)
.ToListAsync();
foreach (var cm in arCreditMemos)
entries.Add(new LedgerEntryDto
{
Date = cm.AppliedDate,
Reference = cm.CreditMemo?.MemoNumber ?? $"CM-{cm.Id}",
Source = "Credit Memo",
Description = $"Credit applied to {cm.Invoice?.InvoiceNumber}",
Debit = 0,
Credit = cm.AmountApplied,
LinkController = "Invoices",
LinkId = cm.InvoiceId
});
// Refunds re-open AR (DEBIT — customer owes again after refund)
var arRefunds = await _context.Refunds
.Include(r => r.Invoice)
.Where(r => r.RefundDate >= fromDate && r.RefundDate <= toDate && !r.IsDeleted)
.ToListAsync();
foreach (var r in arRefunds)
entries.Add(new LedgerEntryDto
{
Date = r.RefundDate,
Reference = r.Reference ?? $"REF-{r.Id}",
Source = "Refund",
Description = r.Reason,
Debit = r.Amount,
Credit = 0,
LinkController = "Invoices",
LinkId = r.InvoiceId
});
}
// ── 9. Accounts Payable ────────────────────────────────────────────────
// Bill creation increases AP (CREDIT); bill payments reduce AP (DEBIT)
if (account.AccountSubType == AccountSubType.AccountsPayable)
{
var apBills = await _context.Bills
.Include(b => b.Vendor)
.Where(b => b.APAccountId == accountId
&& b.Status != BillStatus.Draft
&& b.Status != BillStatus.Voided
&& b.BillDate >= fromDate && b.BillDate <= toDate)
.ToListAsync();
foreach (var b in apBills)
entries.Add(new LedgerEntryDto
{
Date = b.BillDate,
Reference = b.BillNumber,
Source = "Bill",
Description = b.Vendor?.CompanyName ?? b.Memo,
Debit = 0,
Credit = b.Total,
LinkController = "Bills",
LinkId = b.Id
});
var apPayments = await _context.BillPayments
.Include(bp => bp.Bill)
.Where(bp => bp.Bill.APAccountId == accountId
&& bp.PaymentDate >= fromDate && bp.PaymentDate <= toDate)
.ToListAsync();
foreach (var bp in apPayments)
entries.Add(new LedgerEntryDto
{
Date = bp.PaymentDate,
Reference = bp.PaymentNumber,
Source = "Bill Payment",
Description = bp.Memo ?? bp.Bill?.BillNumber,
Debit = bp.Amount,
Credit = 0,
LinkController = "Bills",
LinkId = bp.BillId
});
// Vendor credit applications reduce AP (DEBIT — offset against what we owe)
var apVendorCredits = await _context.VendorCreditApplications
.Include(vca => vca.VendorCredit)
.Include(vca => vca.Bill)
.Where(vca => vca.VendorCredit.APAccountId == accountId
&& vca.AppliedDate >= fromDate && vca.AppliedDate <= toDate)
.ToListAsync();
foreach (var vca in apVendorCredits)
entries.Add(new LedgerEntryDto
{
Date = vca.AppliedDate,
Reference = vca.VendorCredit?.CreditNumber ?? $"VC-{vca.VendorCreditId}",
Source = "Vendor Credit",
Description = $"Credit applied to {vca.Bill?.BillNumber}",
Debit = vca.Amount,
Credit = 0,
LinkController = "VendorCredits",
LinkId = vca.VendorCreditId
});
}
// ── 11. Gift Certificate Liability (account 2500) ─────────────────────
// CR when GC is issued; DR when redeemed or voided with remaining balance.
if (account.AccountNumber == "2500")
{
var gcIssued = await _context.GiftCertificates
.Where(gc => !gc.IsDeleted && gc.IssueDate >= fromDate && gc.IssueDate <= toDate)
.ToListAsync();
foreach (var gc in gcIssued)
entries.Add(new LedgerEntryDto
{
Date = gc.IssueDate, Reference = gc.CertificateCode,
Source = "Gift Certificate", Description = "GC issued",
Debit = 0, Credit = gc.OriginalAmount,
LinkController = "GiftCertificates", LinkId = gc.Id
});
var gcRedemptions = await _context.GiftCertificateRedemptions
.Include(r => r.GiftCertificate)
.Where(r => !r.IsDeleted && r.RedeemedDate >= fromDate && r.RedeemedDate <= toDate)
.ToListAsync();
foreach (var r in gcRedemptions)
entries.Add(new LedgerEntryDto
{
Date = r.RedeemedDate, Reference = r.GiftCertificate?.CertificateCode ?? $"GC-{r.GiftCertificateId}",
Source = "GC Redemption", Description = "GC applied to invoice",
Debit = r.AmountRedeemed, Credit = 0,
LinkController = "GiftCertificates", LinkId = r.GiftCertificateId
});
var gcVoided = await _context.GiftCertificates
.Where(gc => !gc.IsDeleted && gc.Status == GiftCertificateStatus.Voided
&& gc.UpdatedAt >= fromDate && gc.UpdatedAt <= toDate
&& gc.OriginalAmount > gc.RedeemedAmount)
.ToListAsync();
foreach (var gc in gcVoided)
entries.Add(new LedgerEntryDto
{
Date = gc.UpdatedAt.GetValueOrDefault(), Reference = gc.CertificateCode,
Source = "GC Voided", Description = "Breakage income",
Debit = gc.OriginalAmount - gc.RedeemedAmount, Credit = 0,
LinkController = "GiftCertificates", LinkId = gc.Id
});
}
// ── 12. Customer Deposits liability (account 2300) ────────────────────
// CR when deposit is recorded; DR when deposit is applied to an invoice.
if (account.AccountNumber == "2300")
{
var depositsRecorded = await _context.Deposits
.Where(d => !d.IsDeleted && d.ReceivedDate >= fromDate && d.ReceivedDate <= toDate)
.ToListAsync();
foreach (var d in depositsRecorded)
entries.Add(new LedgerEntryDto
{
Date = d.ReceivedDate, Reference = d.ReceiptNumber,
Source = "Customer Deposit", Description = d.Notes ?? d.Reference,
Debit = 0, Credit = d.Amount,
LinkController = "Jobs", LinkId = d.JobId
});
var depositsApplied = await _context.Deposits
.Include(d => d.AppliedToInvoice)
.Where(d => !d.IsDeleted && d.AppliedToInvoiceId != null
&& d.AppliedDate >= fromDate && d.AppliedDate <= toDate)
.ToListAsync();
foreach (var d in depositsApplied)
entries.Add(new LedgerEntryDto
{
Date = d.AppliedDate!.Value, Reference = d.AppliedToInvoice?.InvoiceNumber ?? d.ReceiptNumber,
Source = "Deposit Applied", Description = $"Deposit {d.ReceiptNumber} applied to invoice",
Debit = d.Amount, Credit = 0,
LinkController = "Invoices", LinkId = d.AppliedToInvoiceId
});
}
// ── 10. Journal Entry lines touching this account ──────────────────
var jeLines = await _context.JournalEntryLines
.Include(l => l.JournalEntry)
.Where(l => l.AccountId == accountId
&& l.JournalEntry.Status == JournalEntryStatus.Posted
&& l.JournalEntry.EntryDate >= fromDate
&& l.JournalEntry.EntryDate <= toDate)
.ToListAsync();
foreach (var line in jeLines)
entries.Add(new LedgerEntryDto
{
Date = line.JournalEntry.EntryDate,
Reference = line.JournalEntry.EntryNumber,
Source = "Journal Entry",
Description = line.Description ?? line.JournalEntry.Description,
Debit = line.DebitAmount,
Credit = line.CreditAmount,
LinkController = "JournalEntries",
LinkId = line.JournalEntry.Id
});
// ── Sort and compute running balance ──────────────────────────────────
entries = entries
.OrderBy(e => e.Date)
.ThenBy(e => e.Reference)
.ToList();
// Derive normal-debit-balance flag from AccountSubType (more authoritative than AccountType,
// since users could misconfigure AccountType while SubType is picked from a constrained list).
bool normalDebitBalance = AccountingRules.IsNormalDebitBalance(account.AccountSubType);
// Compute the balance before the selected period
decimal priorBalance = await ComputePriorBalanceAsync(account, fromDate, to.Date, normalDebitBalance);
decimal runningBalance = priorBalance;
foreach (var entry in entries)
{
runningBalance += normalDebitBalance
? entry.Debit - entry.Credit
: entry.Credit - entry.Debit;
entry.RunningBalance = runningBalance;
}
return new AccountLedgerDto
{
Id = account.Id,
AccountNumber = account.AccountNumber,
Name = account.Name,
AccountType = account.AccountType,
AccountSubType = account.AccountSubType,
From = fromDate,
To = to.Date,
OpeningBalance = priorBalance,
PeriodDebits = entries.Sum(e => e.Debit),
PeriodCredits = entries.Sum(e => e.Credit),
ClosingBalance = runningBalance,
Entries = entries
};
}
///
/// Computes the account balance on the day immediately before
/// by summing all activity prior to that date across every transaction source and adding
/// the stored OpeningBalance. The opening balance is only included when its recorded
/// date is on or before — a future-dated opening balance (e.g.
/// from a mid-year chart-of-accounts migration) should not pollute earlier period reports.
/// A null OpeningBalanceDate means the balance predates all transactions and always applies.
/// The sign convention follows : debits increase debit-normal
/// accounts and credits increase credit-normal accounts.
///
private async Task ComputePriorBalanceAsync(
PowderCoating.Core.Entities.Account account, DateTime beforeDate, DateTime periodEnd, bool normalDebitBalance)
{
var accountId = account.Id;
decimal debits = 0;
decimal credits = 0;
// 1. Customer payments deposited INTO this account (DEBIT)
debits += await _context.Payments
.Where(p => p.DepositAccountId == accountId && p.PaymentDate < beforeDate)
.SumAsync(p => (decimal?)p.Amount) ?? 0;
// Customer deposits recorded to this account (DEBIT — cash received at deposit time)
debits += await _context.Deposits
.Where(d => !d.IsDeleted && d.DepositAccountId == accountId && d.ReceivedDate < beforeDate)
.SumAsync(d => (decimal?)d.Amount) ?? 0;
// Refunds paid FROM this account (CREDIT — cash leaves)
credits += await _context.Refunds
.Where(r => !r.IsDeleted && r.DepositAccountId == accountId && r.RefundDate < beforeDate)
.SumAsync(r => (decimal?)r.Amount) ?? 0;
// 2. Direct expenses paid FROM this account (CREDIT)
credits += await _context.Expenses
.Where(e => e.PaymentAccountId == accountId && e.Date < beforeDate)
.SumAsync(e => (decimal?)e.Amount) ?? 0;
// 3. Bill payments made FROM this account (CREDIT)
credits += await _context.BillPayments
.Where(bp => bp.BankAccountId == accountId && bp.PaymentDate < beforeDate)
.SumAsync(bp => (decimal?)bp.Amount) ?? 0;
// 4. Invoice line items posting revenue to this account (CREDIT)
credits += await _context.InvoiceItems
.Where(ii => ii.RevenueAccountId == accountId
&& ii.Invoice.Status != InvoiceStatus.Draft
&& ii.Invoice.Status != InvoiceStatus.Voided
&& ii.Invoice.InvoiceDate < beforeDate)
.SumAsync(ii => (decimal?)ii.TotalPrice) ?? 0;
// 5. Sales tax collected to this account (CREDIT)
credits += await _context.Invoices
.Where(i => i.SalesTaxAccountId == accountId
&& i.TaxAmount > 0
&& i.Status != InvoiceStatus.Draft
&& i.Status != InvoiceStatus.Voided
&& i.InvoiceDate < beforeDate)
.SumAsync(i => (decimal?)i.TaxAmount) ?? 0;
// 6. Direct expenses categorized to this account (DEBIT)
debits += await _context.Expenses
.Where(e => e.ExpenseAccountId == accountId && e.Date < beforeDate)
.SumAsync(e => (decimal?)e.Amount) ?? 0;
// 7. Bill line items categorized to this account (DEBIT)
debits += await _context.BillLineItems
.Where(bli => bli.AccountId == accountId
&& bli.Bill.Status != BillStatus.Draft
&& bli.Bill.Status != BillStatus.Voided
&& bli.Bill.BillDate < beforeDate)
.SumAsync(bli => (decimal?)bli.Amount) ?? 0;
// 8. Accounts Receivable
if (account.AccountSubType == AccountSubType.AccountsReceivable)
{
debits += await _context.Invoices
.Where(i => i.Status != InvoiceStatus.Draft
&& i.Status != InvoiceStatus.Voided
&& i.InvoiceDate < beforeDate)
.SumAsync(i => (decimal?)i.Total) ?? 0;
credits += await _context.Payments
.Where(p => p.PaymentDate < beforeDate)
.SumAsync(p => (decimal?)p.Amount) ?? 0;
credits += await _context.CreditMemoApplications
.Where(a => a.AppliedDate < beforeDate && a.Invoice.Status != InvoiceStatus.Voided)
.SumAsync(a => (decimal?)a.AmountApplied) ?? 0;
debits += await _context.Refunds
.Where(r => !r.IsDeleted && r.RefundDate < beforeDate)
.SumAsync(r => (decimal?)r.Amount) ?? 0;
}
// 9. Accounts Payable
if (account.AccountSubType == AccountSubType.AccountsPayable)
{
credits += await _context.Bills
.Where(b => b.APAccountId == accountId
&& b.Status != BillStatus.Draft
&& b.Status != BillStatus.Voided
&& b.BillDate < beforeDate)
.SumAsync(b => (decimal?)b.Total) ?? 0;
debits += await _context.BillPayments
.Where(bp => bp.Bill.APAccountId == accountId && bp.PaymentDate < beforeDate)
.SumAsync(bp => (decimal?)bp.Amount) ?? 0;
debits += await _context.VendorCreditApplications
.Where(vca => vca.VendorCredit.APAccountId == accountId && vca.AppliedDate < beforeDate)
.SumAsync(vca => (decimal?)vca.Amount) ?? 0;
}
// 11. GC Liability (account 2500)
if (account.AccountNumber == "2500")
{
credits += await _context.GiftCertificates
.Where(gc => !gc.IsDeleted && gc.IssueDate < beforeDate)
.SumAsync(gc => (decimal?)gc.OriginalAmount) ?? 0;
debits += await _context.GiftCertificateRedemptions
.Where(r => !r.IsDeleted && r.RedeemedDate < beforeDate)
.SumAsync(r => (decimal?)r.AmountRedeemed) ?? 0;
debits += await _context.GiftCertificates
.Where(gc => !gc.IsDeleted && gc.Status == GiftCertificateStatus.Voided
&& gc.UpdatedAt < beforeDate && gc.OriginalAmount > gc.RedeemedAmount)
.SumAsync(gc => (decimal?)(gc.OriginalAmount - gc.RedeemedAmount)) ?? 0;
}
// 12. Customer Deposits liability (account 2300)
if (account.AccountNumber == "2300")
{
credits += await _context.Deposits
.Where(d => !d.IsDeleted && d.ReceivedDate < beforeDate)
.SumAsync(d => (decimal?)d.Amount) ?? 0;
debits += await _context.Deposits
.Where(d => !d.IsDeleted && d.AppliedToInvoiceId != null && d.AppliedDate < beforeDate)
.SumAsync(d => (decimal?)d.Amount) ?? 0;
}
// 10. Posted journal entry lines touching this account (prior to period)
debits += await _context.JournalEntryLines
.Where(l => l.AccountId == accountId
&& l.JournalEntry.Status == JournalEntryStatus.Posted
&& l.JournalEntry.EntryDate < beforeDate)
.SumAsync(l => (decimal?)l.DebitAmount) ?? 0;
credits += await _context.JournalEntryLines
.Where(l => l.AccountId == accountId
&& l.JournalEntry.Status == JournalEntryStatus.Posted
&& l.JournalEntry.EntryDate < beforeDate)
.SumAsync(l => (decimal?)l.CreditAmount) ?? 0;
decimal netActivity = normalDebitBalance ? debits - credits : credits - debits;
// Apply the opening balance if it was established on or before the end of the viewed period.
// A null date means it predates all transactions and always applies.
decimal openingBalance = (account.OpeningBalanceDate == null || account.OpeningBalanceDate.Value.Date <= periodEnd)
? account.OpeningBalance
: 0;
return openingBalance + netActivity;
}
}