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; } }