Scope every FinancialReportService query by CompanyId (defense in depth)

Audit finding #7: most report queries relied on the global tenant query filter,
which is bypassed for SuperAdmin users — so a SuperAdmin (or any multi-company
account) running P&L / Balance Sheet / Trial Balance / aging / statements could
pull data across companies. The cash-flow method was the only one doing it right
(IgnoreQueryFilters + explicit CompanyId).

Adds an explicit `CompanyId == companyId` predicate to every DB query across
GetProfitAndLossAsync, GetBalanceSheetAsync, GetTrialBalanceAsync, GetArAgingAsync,
GetSalesAndIncomeAsync, GetBalanceReconciliationAsync, and the customer/vendor
statements (Sales Tax and AP aging already had it). The remaining in-memory
filters operate on collections already loaded with the predicate. Matches the
repo's standing rule (explicit CompanyId on every query, never the global filter
alone). Build clean; 284 unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 13:54:34 -04:00
parent 0c921ba180
commit 9532812b9f
@@ -89,7 +89,7 @@ public class FinancialReportService : IFinancialReportService
var isCash = accountingMethod == AccountingMethod.Cash;
var revenueAccounts = await _context.Accounts
.Where(a => a.AccountType == AccountType.Revenue && a.IsActive)
.Where(a => a.CompanyId == companyId && a.AccountType == AccountType.Revenue && a.IsActive)
.ToDictionaryAsync(a => a.Id);
var revenueLines = new List<FinancialReportLine>();
@@ -98,7 +98,7 @@ public class FinancialReportService : IFinancialReportService
{
// Cash basis: total payments received in period (not split by revenue account)
var cashRevenue = await _context.Payments
.Where(p => p.PaymentDate >= from && p.PaymentDate <= toEnd
.Where(p => p.CompanyId == companyId && p.PaymentDate >= from && p.PaymentDate <= toEnd
&& p.Invoice.Status != InvoiceStatus.Voided)
.SumAsync(p => (decimal?)p.Amount) ?? 0;
if (cashRevenue > 0)
@@ -106,7 +106,7 @@ public class FinancialReportService : IFinancialReportService
// Cash refunds are cash paid back out — they reduce cash-basis revenue.
var cashRefunds = await _context.Refunds
.Where(r => !r.IsDeleted && r.RefundMethod != PaymentMethod.StoreCredit
.Where(r => r.CompanyId == companyId && !r.IsDeleted && r.RefundMethod != PaymentMethod.StoreCredit
&& r.RefundDate >= from && r.RefundDate <= toEnd)
.SumAsync(r => (decimal?)r.Amount) ?? 0m;
if (cashRefunds > 0)
@@ -116,7 +116,8 @@ public class FinancialReportService : IFinancialReportService
{
// Accrual basis: revenue = invoice item amounts by invoice date
var accrualRevenue = await _context.InvoiceItems
.Where(ii => ii.RevenueAccountId != null
.Where(ii => ii.CompanyId == companyId
&& ii.RevenueAccountId != null
&& ii.Invoice.Status != InvoiceStatus.Draft
&& ii.Invoice.Status != InvoiceStatus.Voided
&& ii.Invoice.InvoiceDate >= from && ii.Invoice.InvoiceDate <= toEnd)
@@ -136,7 +137,8 @@ public class FinancialReportService : IFinancialReportService
.OrderBy(l => l.AccountNumber));
var unlinkedRevenue = await _context.InvoiceItems
.Where(ii => ii.RevenueAccountId == null
.Where(ii => ii.CompanyId == companyId
&& ii.RevenueAccountId == null
&& ii.Invoice.Status != InvoiceStatus.Draft
&& ii.Invoice.Status != InvoiceStatus.Voided
&& ii.Invoice.InvoiceDate >= from && ii.Invoice.InvoiceDate <= toEnd)
@@ -146,7 +148,7 @@ public class FinancialReportService : IFinancialReportService
// Contra-revenue: discounts granted and credit memos applied reduce gross revenue.
var periodDiscounts = await _context.Invoices
.Where(i => i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
.Where(i => i.CompanyId == companyId && i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
&& i.DiscountAmount > 0 && i.InvoiceDate >= from && i.InvoiceDate <= toEnd)
.SumAsync(i => (decimal?)i.DiscountAmount) ?? 0m;
// Credit-memo contra-revenue is recognized at issue (DR Sales Discounts). Net for the period =
@@ -171,7 +173,7 @@ public class FinancialReportService : IFinancialReportService
// Cash refunds reverse the sale — the revenue portion is contra-revenue (the tax portion
// relieves Sales Tax Payable, not revenue). Store-credit refunds are excluded (no GL posting).
var periodRefunds = await _context.Refunds
.Where(r => !r.IsDeleted && r.Invoice != null && r.RefundMethod != PaymentMethod.StoreCredit
.Where(r => r.CompanyId == companyId && !r.IsDeleted && r.Invoice != null && r.RefundMethod != PaymentMethod.StoreCredit
&& r.RefundDate >= from && r.RefundDate <= toEnd)
.Select(r => new { r.Amount, r.Invoice!.TaxAmount, r.Invoice.Total })
.ToListAsync();
@@ -186,7 +188,8 @@ public class FinancialReportService : IFinancialReportService
// GC sales are deferred to GC Liability at issuance; revenue is recognized on redemption.
var periodGcReclassified = await _context.InvoiceItems
.Where(ii => ii.IsGiftCertificate
.Where(ii => ii.CompanyId == companyId
&& ii.IsGiftCertificate
&& ii.Invoice.Status != InvoiceStatus.Draft
&& ii.Invoice.Status != InvoiceStatus.Voided
&& ii.Invoice.InvoiceDate >= from && ii.Invoice.InvoiceDate <= toEnd)
@@ -201,7 +204,7 @@ public class FinancialReportService : IFinancialReportService
// Voided GCs with remaining balance are breakage income (liability extinguished).
var periodGcBreakage = await _context.GiftCertificates
.Where(gc => !gc.IsDeleted && gc.Status == GiftCertificateStatus.Voided
.Where(gc => gc.CompanyId == companyId && !gc.IsDeleted && gc.Status == GiftCertificateStatus.Voided
&& gc.UpdatedAt >= from && gc.UpdatedAt <= toEnd
&& gc.OriginalAmount > gc.RedeemedAmount)
.SumAsync(gc => (decimal?)(gc.OriginalAmount - gc.RedeemedAmount)) ?? 0m;
@@ -220,7 +223,7 @@ public class FinancialReportService : IFinancialReportService
if (isCash)
{
var cashExpenses = await _context.Expenses
.Where(e => e.Date >= from && e.Date <= toEnd)
.Where(e => e.CompanyId == companyId && e.Date >= from && e.Date <= toEnd)
.GroupBy(e => e.ExpenseAccountId)
.Select(g => new { AccountId = g.Key, Amount = g.Sum(e => e.Amount) })
.ToListAsync();
@@ -229,7 +232,7 @@ public class FinancialReportService : IFinancialReportService
// Pro-rate paid bill line items by payment fraction (bill total may be partial)
var paidBillLines = await _context.BillPayments
.Where(bp => bp.PaymentDate >= from && bp.PaymentDate <= toEnd)
.Where(bp => bp.CompanyId == companyId && bp.PaymentDate >= from && bp.PaymentDate <= toEnd)
.Include(bp => bp.Bill).ThenInclude(b => b.LineItems)
.ToListAsync();
foreach (var bp in paidBillLines)
@@ -242,7 +245,7 @@ public class FinancialReportService : IFinancialReportService
else
{
var accrualExpenses = await _context.Expenses
.Where(e => e.Date >= from && e.Date <= toEnd)
.Where(e => e.CompanyId == companyId && e.Date >= from && e.Date <= toEnd)
.GroupBy(e => e.ExpenseAccountId)
.Select(g => new { AccountId = g.Key, Amount = g.Sum(e => e.Amount) })
.ToListAsync();
@@ -250,7 +253,8 @@ public class FinancialReportService : IFinancialReportService
expenseAmounts[e.AccountId] = expenseAmounts.GetValueOrDefault(e.AccountId) + e.Amount;
var accrualBillLines = await _context.BillLineItems
.Where(bli => bli.AccountId != null
.Where(bli => bli.CompanyId == companyId
&& bli.AccountId != null
&& bli.Bill.Status != BillStatus.Draft
&& bli.Bill.Status != BillStatus.Voided
&& bli.Bill.BillDate >= from && bli.Bill.BillDate <= toEnd)
@@ -262,7 +266,7 @@ public class FinancialReportService : IFinancialReportService
}
var expAccounts = await _context.Accounts
.Where(a => (a.AccountType == AccountType.Expense || a.AccountType == AccountType.CostOfGoods) && a.IsActive)
.Where(a => a.CompanyId == companyId && (a.AccountType == AccountType.Expense || a.AccountType == AccountType.CostOfGoods) && a.IsActive)
.ToDictionaryAsync(a => a.Id);
var cogsLines = new List<FinancialReportLine>();
@@ -302,7 +306,7 @@ public class FinancialReportService : IFinancialReportService
// Pre-compute balance contributions per account (batch GROUP BY queries avoid N+1)
var depositsByAcct = await _context.Payments
.Where(p => p.PaymentDate <= asOfEnd && p.DepositAccountId != null
.Where(p => p.CompanyId == companyId && p.PaymentDate <= asOfEnd && p.DepositAccountId != null
&& p.Invoice.Status != InvoiceStatus.Voided
&& p.Invoice.Status != InvoiceStatus.WrittenOff)
.GroupBy(p => p.DepositAccountId!.Value)
@@ -310,38 +314,38 @@ public class FinancialReportService : IFinancialReportService
.ToDictionaryAsync(g => g.Id, g => g.Amount);
var expFromByAcct = await _context.Expenses
.Where(e => e.Date <= asOfEnd)
.Where(e => e.CompanyId == companyId && e.Date <= asOfEnd)
.GroupBy(e => e.PaymentAccountId)
.Select(g => new { Id = g.Key, Amount = g.Sum(e => e.Amount) })
.ToDictionaryAsync(g => g.Id, g => g.Amount);
var bpFromByAcct = await _context.BillPayments
.Where(bp => bp.PaymentDate <= asOfEnd)
.Where(bp => bp.CompanyId == companyId && bp.PaymentDate <= asOfEnd)
.GroupBy(bp => bp.BankAccountId)
.Select(g => new { Id = g.Key, Amount = g.Sum(bp => bp.Amount) })
.ToDictionaryAsync(g => g.Id, g => g.Amount);
var billsByApAcct = await _context.Bills
.Where(b => b.Status != BillStatus.Draft && b.Status != BillStatus.Voided && b.BillDate <= asOfEnd)
.Where(b => b.CompanyId == companyId && b.Status != BillStatus.Draft && b.Status != BillStatus.Voided && b.BillDate <= asOfEnd)
.GroupBy(b => b.APAccountId)
.Select(g => new { Id = g.Key, Amount = g.Sum(b => b.Total) })
.ToDictionaryAsync(g => g.Id, g => g.Amount);
var bpByApAcct = await _context.BillPayments
.Where(bp => bp.PaymentDate <= asOfEnd)
.Where(bp => bp.CompanyId == companyId && bp.PaymentDate <= asOfEnd)
.GroupBy(bp => bp.Bill.APAccountId)
.Select(g => new { Id = g.Key, Amount = g.Sum(bp => bp.Amount) })
.ToDictionaryAsync(g => g.Id, g => g.Amount);
// AP: vendor credit applications reduce AP (DR side) when matched against specific bills.
var vcByApAcctBs = await _context.VendorCreditApplications
.Where(vca => vca.AppliedDate <= asOfEnd)
.Where(vca => vca.CompanyId == companyId && vca.AppliedDate <= asOfEnd)
.GroupBy(vca => vca.VendorCredit.APAccountId)
.Select(g => new { Id = g.Key, Amount = g.Sum(vca => vca.Amount) })
.ToDictionaryAsync(g => g.Id, g => g.Amount);
var taxByAcct = await _context.Invoices
.Where(i => i.SalesTaxAccountId != null && i.TaxAmount > 0
.Where(i => i.CompanyId == companyId && i.SalesTaxAccountId != null && i.TaxAmount > 0
&& i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
&& i.InvoiceDate <= asOfEnd)
.GroupBy(i => i.SalesTaxAccountId!.Value)
@@ -349,16 +353,16 @@ public class FinancialReportService : IFinancialReportService
.ToDictionaryAsync(g => g.Id, g => g.Amount);
var arDebits = await _context.Invoices
.Where(i => i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided && i.InvoiceDate <= asOfEnd)
.Where(i => i.CompanyId == companyId && i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided && i.InvoiceDate <= asOfEnd)
.SumAsync(i => (decimal?)i.Total) ?? 0;
var arCredits = await _context.Payments
.Where(p => p.PaymentDate <= asOfEnd
.Where(p => p.CompanyId == companyId && p.PaymentDate <= asOfEnd
&& p.Invoice.Status != InvoiceStatus.Voided
&& p.Invoice.Status != InvoiceStatus.WrittenOff)
.SumAsync(p => (decimal?)p.Amount) ?? 0;
// Credit memo applications reduce open AR (CR AR when a credit is applied to an invoice).
var cmAppliedBs = await _context.CreditMemoApplications
.Where(a => a.AppliedDate <= asOfEnd && a.Invoice.Status != InvoiceStatus.Voided)
.Where(a => a.CompanyId == companyId && a.AppliedDate <= asOfEnd && a.Invoice.Status != InvoiceStatus.Voided)
.SumAsync(a => (decimal?)a.AmountApplied) ?? 0;
arCredits += cmAppliedBs;
@@ -369,7 +373,7 @@ public class FinancialReportService : IFinancialReportService
.Where(m => m.CompanyId == companyId && m.Status != CreditMemoStatus.Voided && m.IssueDate <= asOfEnd)
.SumAsync(m => (decimal?)m.Amount) ?? 0m;
var cmAppliedNonVoidedBs = await _context.CreditMemoApplications
.Where(a => a.AppliedDate <= asOfEnd && a.Invoice.Status != InvoiceStatus.Voided
.Where(a => a.CompanyId == companyId && a.AppliedDate <= asOfEnd && a.Invoice.Status != InvoiceStatus.Voided
&& a.CreditMemo.Status != CreditMemoStatus.Voided)
.SumAsync(a => (decimal?)a.AmountApplied) ?? 0m;
var cmContraRevenueBs = cmIssuedNonVoidedBs + (cmAppliedBs - cmAppliedNonVoidedBs);
@@ -380,7 +384,7 @@ public class FinancialReportService : IFinancialReportService
// tax portion relieves Sales Tax Payable, cash leaves the bank (refundsByAcctBs). AR is untouched.
// Store-credit refunds post via CreditMemo, not the GL, so are excluded.
var saleReversingRefundsBs = await _context.Refunds
.Where(r => r.RefundDate <= asOfEnd && !r.IsDeleted && r.Invoice != null
.Where(r => r.CompanyId == companyId && r.RefundDate <= asOfEnd && !r.IsDeleted && r.Invoice != null
&& r.RefundMethod != PaymentMethod.StoreCredit)
.Select(r => new { r.Amount, r.Invoice!.TaxAmount, r.Invoice.Total, r.Invoice.SalesTaxAccountId })
.ToListAsync();
@@ -396,14 +400,14 @@ public class FinancialReportService : IFinancialReportService
// Refunds by bank account: money that left the account (CR to checking/bank).
var refundsByAcctBs = await _context.Refunds
.Where(r => r.RefundDate <= asOfEnd && !r.IsDeleted && r.DepositAccountId != null)
.Where(r => r.CompanyId == companyId && r.RefundDate <= asOfEnd && !r.IsDeleted && r.DepositAccountId != null)
.GroupBy(r => r.DepositAccountId!.Value)
.Select(g => new { Id = g.Key, Amount = g.Sum(r => r.Amount) })
.ToDictionaryAsync(g => g.Id, g => g.Amount);
// Deposits by bank account: cash received at deposit recording time (DR bank).
var depositsByAcctDepBs = await _context.Deposits
.Where(d => !d.IsDeleted && d.DepositAccountId != null && d.ReceivedDate <= asOfEnd)
.Where(d => d.CompanyId == companyId && !d.IsDeleted && d.DepositAccountId != null && d.ReceivedDate <= asOfEnd)
.GroupBy(d => d.DepositAccountId!.Value)
.Select(g => new { Id = g.Key, Amount = g.Sum(d => d.Amount) })
.ToDictionaryAsync(g => g.Id, g => g.Amount);
@@ -414,11 +418,11 @@ public class FinancialReportService : IFinancialReportService
.Select(a => (int?)a.Id).FirstOrDefaultAsync();
var custDepositsCreditsBs = custDepositsAcctIdBs.HasValue
? (await _context.Deposits
.Where(d => !d.IsDeleted && d.ReceivedDate <= asOfEnd)
.Where(d => d.CompanyId == companyId && !d.IsDeleted && d.ReceivedDate <= asOfEnd)
.SumAsync(d => (decimal?)d.Amount) ?? 0m) : 0m;
var custDepositsDebitsBs = custDepositsAcctIdBs.HasValue
? (await _context.Deposits
.Where(d => !d.IsDeleted && d.AppliedToInvoiceId != null && d.AppliedDate <= asOfEnd)
.Where(d => d.CompanyId == companyId && !d.IsDeleted && d.AppliedToInvoiceId != null && d.AppliedDate <= asOfEnd)
.SumAsync(d => (decimal?)d.Amount) ?? 0m) : 0m;
// Gift Certificate Liability (2500): balance driven by GC issuances, redemptions, and voids.
@@ -427,14 +431,14 @@ public class FinancialReportService : IFinancialReportService
.Select(a => (int?)a.Id).FirstOrDefaultAsync();
var gcLiabilityCreditsBs = gcLiabilityAcctIdBs.HasValue
? (await _context.GiftCertificates
.Where(gc => !gc.IsDeleted && gc.IssueDate <= asOfEnd)
.Where(gc => gc.CompanyId == companyId && !gc.IsDeleted && gc.IssueDate <= asOfEnd)
.SumAsync(gc => (decimal?)gc.OriginalAmount) ?? 0m) : 0m;
var gcLiabilityDebitsBs = gcLiabilityAcctIdBs.HasValue
? ((await _context.GiftCertificateRedemptions
.Where(r => !r.IsDeleted && r.RedeemedDate <= asOfEnd)
.Where(r => r.CompanyId == companyId && !r.IsDeleted && r.RedeemedDate <= asOfEnd)
.SumAsync(r => (decimal?)r.AmountRedeemed) ?? 0m)
+ (await _context.GiftCertificates
.Where(gc => !gc.IsDeleted && gc.Status == GiftCertificateStatus.Voided
.Where(gc => gc.CompanyId == companyId && !gc.IsDeleted && gc.Status == GiftCertificateStatus.Voided
&& gc.UpdatedAt <= asOfEnd && gc.OriginalAmount > gc.RedeemedAmount)
.SumAsync(gc => (decimal?)(gc.OriginalAmount - gc.RedeemedAmount)) ?? 0m)) : 0m;
@@ -443,21 +447,21 @@ public class FinancialReportService : IFinancialReportService
// plus (5) the net effect of any posted journal entries on revenue/expense/COGS accounts
// (accruals, depreciation, year-end closes, and other adjustments not in the tables above).
var lifetimeRevenue = await _context.InvoiceItems
.Where(ii => ii.Invoice.Status != InvoiceStatus.Draft && ii.Invoice.Status != InvoiceStatus.Voided && ii.Invoice.InvoiceDate <= asOfEnd)
.Where(ii => ii.CompanyId == companyId && ii.Invoice.Status != InvoiceStatus.Draft && ii.Invoice.Status != InvoiceStatus.Voided && ii.Invoice.InvoiceDate <= asOfEnd)
.SumAsync(ii => (decimal?)ii.TotalPrice) ?? 0;
var lifetimeDiscounts = isCash ? 0m
: (await _context.Invoices
.Where(i => i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
.Where(i => i.CompanyId == companyId && i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
&& i.DiscountAmount > 0 && i.InvoiceDate <= asOfEnd)
.SumAsync(i => (decimal?)i.DiscountAmount) ?? 0m);
// Credit memos are contra-revenue recognized at issue (DR Sales Discounts). Net revenue is
// reduced by the issued amount (active memos in full + applied portion of voided memos).
var lifetimeCreditMemos = isCash ? 0m : cmContraRevenueBs;
var lifetimeDirectExp = await _context.Expenses
.Where(e => e.Date <= asOfEnd)
.Where(e => e.CompanyId == companyId && e.Date <= asOfEnd)
.SumAsync(e => (decimal?)e.Amount) ?? 0;
var lifetimeBillCosts = await _context.BillLineItems
.Where(bli => bli.Bill.Status != BillStatus.Draft && bli.Bill.Status != BillStatus.Voided && bli.Bill.BillDate <= asOfEnd)
.Where(bli => bli.CompanyId == companyId && bli.Bill.Status != BillStatus.Draft && bli.Bill.Status != BillStatus.Voided && bli.Bill.BillDate <= asOfEnd)
.SumAsync(bli => (decimal?)bli.Amount) ?? 0;
// JE net effect on revenue accounts (positive = additional revenue recognised via manual JE)
@@ -489,14 +493,14 @@ public class FinancialReportService : IFinancialReportService
// GC items sold via invoices are reclassified to GC Liability and not yet earned income.
var lifetimeGcReclassified = await _context.InvoiceItems
.Where(ii => ii.IsGiftCertificate
.Where(ii => ii.CompanyId == companyId && ii.IsGiftCertificate
&& ii.Invoice.Status != InvoiceStatus.Draft
&& ii.Invoice.Status != InvoiceStatus.Voided
&& ii.Invoice.InvoiceDate <= asOfEnd)
.SumAsync(ii => (decimal?)ii.TotalPrice) ?? 0m;
// Voided GCs with remaining balance become breakage income (the liability is extinguished).
var lifetimeGcBreakage = await _context.GiftCertificates
.Where(gc => !gc.IsDeleted && gc.Status == GiftCertificateStatus.Voided
.Where(gc => gc.CompanyId == companyId && !gc.IsDeleted && gc.Status == GiftCertificateStatus.Voided
&& gc.UpdatedAt <= asOfEnd && gc.OriginalAmount > gc.RedeemedAmount)
.SumAsync(gc => (decimal?)(gc.OriginalAmount - gc.RedeemedAmount)) ?? 0m;
@@ -511,7 +515,7 @@ public class FinancialReportService : IFinancialReportService
- jeExpNet;
var accounts = await _context.Accounts
.Where(a => a.IsActive)
.Where(a => a.CompanyId == companyId && a.IsActive)
.OrderBy(a => a.AccountNumber)
.ToListAsync();
@@ -619,7 +623,8 @@ public class FinancialReportService : IFinancialReportService
var openInvoices = await _context.Invoices
.Include(i => i.Customer)
.Where(i => i.Status != InvoiceStatus.Draft
.Where(i => i.CompanyId == companyId
&& i.Status != InvoiceStatus.Draft
&& i.Status != InvoiceStatus.Voided
&& i.Status != InvoiceStatus.Paid
&& i.InvoiceDate <= asOfEnd
@@ -699,14 +704,15 @@ public class FinancialReportService : IFinancialReportService
var invoices = await _context.Invoices
.Include(i => i.Customer)
.Include(i => i.Payments)
.Where(i => i.Status != InvoiceStatus.Draft
.Where(i => i.CompanyId == companyId
&& i.Status != InvoiceStatus.Draft
&& i.Status != InvoiceStatus.Voided
&& i.InvoiceDate >= from && i.InvoiceDate <= toEnd)
.OrderBy(i => i.InvoiceDate)
.ToListAsync();
var collectedInPeriod = await _context.Payments
.Where(p => p.PaymentDate >= from && p.PaymentDate <= toEnd)
.Where(p => p.CompanyId == companyId && p.PaymentDate >= from && p.PaymentDate <= toEnd)
.SumAsync(p => (decimal?)p.Amount) ?? 0;
var byCustomer = invoices
@@ -971,7 +977,7 @@ public class FinancialReportService : IFinancialReportService
// Bank/cash: customer payments deposited here (DR)
var depositsByAcct = await _context.Payments
.Where(p => p.PaymentDate <= asOfEnd && p.DepositAccountId != null
.Where(p => p.CompanyId == companyId && p.PaymentDate <= asOfEnd && p.DepositAccountId != null
&& p.Invoice.Status != InvoiceStatus.Voided
&& p.Invoice.Status != InvoiceStatus.WrittenOff)
.GroupBy(p => p.DepositAccountId!.Value)
@@ -981,42 +987,42 @@ public class FinancialReportService : IFinancialReportService
// AP: vendor credit applications reduce AP (DR) — credits are applied when a vendor
// issues a credit note and it is matched against a specific bill.
var vcByApAcct = await _context.VendorCreditApplications
.Where(vca => vca.AppliedDate <= asOfEnd)
.Where(vca => vca.CompanyId == companyId && vca.AppliedDate <= asOfEnd)
.GroupBy(vca => vca.VendorCredit.APAccountId)
.Select(g => new { Id = g.Key, Amt = g.Sum(vca => vca.Amount) })
.ToDictionaryAsync(g => g.Id, g => g.Amt);
// Bank/cash: expenses paid from here (CR)
var expFromByAcct = await _context.Expenses
.Where(e => e.Date <= asOfEnd)
.Where(e => e.CompanyId == companyId && e.Date <= asOfEnd)
.GroupBy(e => e.PaymentAccountId)
.Select(g => new { Id = g.Key, Amt = g.Sum(e => e.Amount) })
.ToDictionaryAsync(g => g.Id, g => g.Amt);
// Bank/cash: bill payments made from here (CR)
var bpFromByAcct = await _context.BillPayments
.Where(bp => bp.PaymentDate <= asOfEnd)
.Where(bp => bp.CompanyId == companyId && bp.PaymentDate <= asOfEnd)
.GroupBy(bp => bp.BankAccountId)
.Select(g => new { Id = g.Key, Amt = g.Sum(bp => bp.Amount) })
.ToDictionaryAsync(g => g.Id, g => g.Amt);
// AP: bills increase AP (CR)
var billsByApAcct = await _context.Bills
.Where(b => b.Status != BillStatus.Draft && b.Status != BillStatus.Voided && b.BillDate <= asOfEnd)
.Where(b => b.CompanyId == companyId && b.Status != BillStatus.Draft && b.Status != BillStatus.Voided && b.BillDate <= asOfEnd)
.GroupBy(b => b.APAccountId)
.Select(g => new { Id = g.Key, Amt = g.Sum(b => b.Total) })
.ToDictionaryAsync(g => g.Id, g => g.Amt);
// AP: bill payments reduce AP (DR)
var bpByApAcct = await _context.BillPayments
.Where(bp => bp.PaymentDate <= asOfEnd)
.Where(bp => bp.CompanyId == companyId && bp.PaymentDate <= asOfEnd)
.GroupBy(bp => bp.Bill.APAccountId)
.Select(g => new { Id = g.Key, Amt = g.Sum(bp => bp.Amount) })
.ToDictionaryAsync(g => g.Id, g => g.Amt);
// Tax liability: sales tax collected (CR)
var taxByAcct = await _context.Invoices
.Where(i => i.SalesTaxAccountId != null && i.TaxAmount > 0
.Where(i => i.CompanyId == companyId && i.SalesTaxAccountId != null && i.TaxAmount > 0
&& i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
&& i.InvoiceDate <= asOfEnd)
.GroupBy(i => i.SalesTaxAccountId!.Value)
@@ -1025,7 +1031,7 @@ public class FinancialReportService : IFinancialReportService
// Revenue accounts: invoice line items (CR)
var revenueByAcct = await _context.InvoiceItems
.Where(ii => ii.RevenueAccountId != null
.Where(ii => ii.CompanyId == companyId && ii.RevenueAccountId != null
&& ii.Invoice.Status != InvoiceStatus.Draft
&& ii.Invoice.Status != InvoiceStatus.Voided
&& ii.Invoice.InvoiceDate <= asOfEnd)
@@ -1035,14 +1041,14 @@ public class FinancialReportService : IFinancialReportService
// Expense accounts: direct expenses (DR)
var expenseByAcct = await _context.Expenses
.Where(e => e.Date <= asOfEnd)
.Where(e => e.CompanyId == companyId && e.Date <= asOfEnd)
.GroupBy(e => e.ExpenseAccountId)
.Select(g => new { Id = g.Key, Amt = g.Sum(e => e.Amount) })
.ToDictionaryAsync(g => g.Id, g => g.Amt);
// Expense/COGS accounts: vendor bill line items (DR)
var billLinesByAcct = await _context.BillLineItems
.Where(bli => bli.AccountId != null
.Where(bli => bli.CompanyId == companyId && bli.AccountId != null
&& bli.Bill.Status != BillStatus.Draft
&& bli.Bill.Status != BillStatus.Voided
&& bli.Bill.BillDate <= asOfEnd)
@@ -1064,7 +1070,7 @@ public class FinancialReportService : IFinancialReportService
.FirstOrDefaultAsync();
var cmApplied = await _context.CreditMemoApplications
.Where(a => a.AppliedDate <= asOfEnd
.Where(a => a.CompanyId == companyId && a.AppliedDate <= asOfEnd
&& a.Invoice.Status != InvoiceStatus.Voided)
.SumAsync(a => (decimal?)a.AmountApplied) ?? 0m;
@@ -1076,7 +1082,7 @@ public class FinancialReportService : IFinancialReportService
.Where(m => m.CompanyId == companyId && m.Status != CreditMemoStatus.Voided && m.IssueDate <= asOfEnd)
.SumAsync(m => (decimal?)m.Amount) ?? 0m;
var cmAppliedNonVoided = await _context.CreditMemoApplications
.Where(a => a.AppliedDate <= asOfEnd
.Where(a => a.CompanyId == companyId && a.AppliedDate <= asOfEnd
&& a.Invoice.Status != InvoiceStatus.Voided
&& a.CreditMemo.Status != CreditMemoStatus.Voided)
.SumAsync(a => (decimal?)a.AmountApplied) ?? 0m;
@@ -1089,7 +1095,7 @@ public class FinancialReportService : IFinancialReportService
if (discountAcctId.HasValue)
{
var totalDiscounts = await _context.Invoices
.Where(i => i.DiscountAmount > 0
.Where(i => i.CompanyId == companyId && i.DiscountAmount > 0
&& i.Status != InvoiceStatus.Draft
&& i.Status != InvoiceStatus.Voided
&& i.InvoiceDate <= asOfEnd)
@@ -1100,14 +1106,14 @@ public class FinancialReportService : IFinancialReportService
// JE lines: posted entries debit/credit all account types
var jeDebitsByAcct = await _context.JournalEntryLines
.Where(l => l.JournalEntry.Status == JournalEntryStatus.Posted
.Where(l => l.CompanyId == companyId && l.JournalEntry.Status == JournalEntryStatus.Posted
&& l.JournalEntry.EntryDate <= asOfEnd)
.GroupBy(l => l.AccountId)
.Select(g => new { Id = g.Key, Amt = g.Sum(l => l.DebitAmount) })
.ToDictionaryAsync(g => g.Id, g => g.Amt);
var jeCreditsByAcct = await _context.JournalEntryLines
.Where(l => l.JournalEntry.Status == JournalEntryStatus.Posted
.Where(l => l.CompanyId == companyId && l.JournalEntry.Status == JournalEntryStatus.Posted
&& l.JournalEntry.EntryDate <= asOfEnd)
.GroupBy(l => l.AccountId)
.Select(g => new { Id = g.Key, Amt = g.Sum(l => l.CreditAmount) })
@@ -1117,11 +1123,11 @@ public class FinancialReportService : IFinancialReportService
// Credits include both cash payments and credit memo applications (which reduce open AR
// when a customer credit is applied against a specific invoice).
var arTotalDebits = await _context.Invoices
.Where(i => i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
.Where(i => i.CompanyId == companyId && i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
&& i.InvoiceDate <= asOfEnd)
.SumAsync(i => (decimal?)i.Total) ?? 0m;
var arTotalCredits = await _context.Payments
.Where(p => p.PaymentDate <= asOfEnd
.Where(p => p.CompanyId == companyId && p.PaymentDate <= asOfEnd
&& p.Invoice.Status != InvoiceStatus.Voided
&& p.Invoice.Status != InvoiceStatus.WrittenOff)
.SumAsync(p => (decimal?)p.Amount) ?? 0m;
@@ -1131,7 +1137,7 @@ public class FinancialReportService : IFinancialReportService
// DR Sales Tax Payable (relieves the liability), cash → CR bank (refundsByAcct below). They no
// longer touch AR. Store-credit refunds post via CreditMemo, not the GL, so are excluded.
var saleReversingRefunds = await _context.Refunds
.Where(r => r.RefundDate <= asOfEnd && !r.IsDeleted && r.Invoice != null
.Where(r => r.CompanyId == companyId && r.RefundDate <= asOfEnd && !r.IsDeleted && r.Invoice != null
&& r.RefundMethod != PaymentMethod.StoreCredit)
.Select(r => new { r.Amount, r.Invoice!.TaxAmount, r.Invoice.Total, r.Invoice.SalesTaxAccountId })
.ToListAsync();
@@ -1152,7 +1158,7 @@ public class FinancialReportService : IFinancialReportService
// Refunds by bank account: money leaving the account (CR to checking/bank).
var refundsByAcct = await _context.Refunds
.Where(r => r.RefundDate <= asOfEnd && !r.IsDeleted && r.DepositAccountId != null)
.Where(r => r.CompanyId == companyId && r.RefundDate <= asOfEnd && !r.IsDeleted && r.DepositAccountId != null)
.GroupBy(r => r.DepositAccountId!.Value)
.Select(g => new { Id = g.Key, Amt = g.Sum(r => r.Amount) })
.ToDictionaryAsync(g => g.Id, g => g.Amt);
@@ -1160,7 +1166,7 @@ public class FinancialReportService : IFinancialReportService
// Deposits by bank account: cash received at deposit recording time (DR bank).
// Deposit-sourced Payments have DepositAccountId = null, so there is no double-count with depositsByAcct.
var depositsByAcctDep = await _context.Deposits
.Where(d => !d.IsDeleted && d.DepositAccountId != null && d.ReceivedDate <= asOfEnd)
.Where(d => d.CompanyId == companyId && !d.IsDeleted && d.DepositAccountId != null && d.ReceivedDate <= asOfEnd)
.GroupBy(d => d.DepositAccountId!.Value)
.Select(g => new { Id = g.Key, Amt = g.Sum(d => d.Amount) })
.ToDictionaryAsync(g => g.Id, g => g.Amt);
@@ -1171,11 +1177,11 @@ public class FinancialReportService : IFinancialReportService
.Select(a => (int?)a.Id).FirstOrDefaultAsync();
var custDepositsCredits = custDepositsAcctId.HasValue
? (await _context.Deposits
.Where(d => !d.IsDeleted && d.ReceivedDate <= asOfEnd)
.Where(d => d.CompanyId == companyId && !d.IsDeleted && d.ReceivedDate <= asOfEnd)
.SumAsync(d => (decimal?)d.Amount) ?? 0m) : 0m;
var custDepositsDebits = custDepositsAcctId.HasValue
? (await _context.Deposits
.Where(d => !d.IsDeleted && d.AppliedToInvoiceId != null && d.AppliedDate <= asOfEnd)
.Where(d => d.CompanyId == companyId && !d.IsDeleted && d.AppliedToInvoiceId != null && d.AppliedDate <= asOfEnd)
.SumAsync(d => (decimal?)d.Amount) ?? 0m) : 0m;
// Gift Certificate Liability (2500): balance driven by GC issuances, redemptions, and voids.
@@ -1184,14 +1190,14 @@ public class FinancialReportService : IFinancialReportService
.Select(a => (int?)a.Id).FirstOrDefaultAsync();
var gcLiabilityCredits = gcLiabilityAcctId.HasValue
? (await _context.GiftCertificates
.Where(gc => !gc.IsDeleted && gc.IssueDate <= asOfEnd)
.Where(gc => gc.CompanyId == companyId && !gc.IsDeleted && gc.IssueDate <= asOfEnd)
.SumAsync(gc => (decimal?)gc.OriginalAmount) ?? 0m) : 0m;
var gcLiabilityDebits = gcLiabilityAcctId.HasValue
? ((await _context.GiftCertificateRedemptions
.Where(r => !r.IsDeleted && r.RedeemedDate <= asOfEnd)
.Where(r => r.CompanyId == companyId && !r.IsDeleted && r.RedeemedDate <= asOfEnd)
.SumAsync(r => (decimal?)r.AmountRedeemed) ?? 0m)
+ (await _context.GiftCertificates
.Where(gc => !gc.IsDeleted && gc.Status == GiftCertificateStatus.Voided
.Where(gc => gc.CompanyId == companyId && !gc.IsDeleted && gc.Status == GiftCertificateStatus.Voided
&& gc.UpdatedAt <= asOfEnd && gc.OriginalAmount > gc.RedeemedAmount)
.SumAsync(gc => (decimal?)(gc.OriginalAmount - gc.RedeemedAmount)) ?? 0m)) : 0m;
@@ -1337,17 +1343,17 @@ public class FinancialReportService : IFinancialReportService
// Opening balance: invoiced paid before period start
var preInvoiced = await _context.Invoices
.Where(i => i.CustomerId == customerId
.Where(i => i.CompanyId == companyId && i.CustomerId == customerId
&& i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
&& i.InvoiceDate < from)
.SumAsync(i => (decimal?)i.Total) ?? 0;
var prePaid = await _context.Payments
.Where(p => p.Invoice.CustomerId == customerId
.Where(p => p.CompanyId == companyId && p.Invoice.CustomerId == customerId
&& p.Invoice.Status != InvoiceStatus.Voided
&& p.PaymentDate < from)
.SumAsync(p => (decimal?)p.Amount) ?? 0;
var preCredits = await _context.CreditMemoApplications
.Where(a => a.Invoice.CustomerId == customerId && a.AppliedDate < from)
.Where(a => a.CompanyId == companyId && a.Invoice.CustomerId == customerId && a.AppliedDate < from)
.SumAsync(a => (decimal?)a.AmountApplied) ?? 0;
var openingBalance = preInvoiced - prePaid - preCredits;
@@ -1356,7 +1362,7 @@ public class FinancialReportService : IFinancialReportService
var lines = new List<StatementLineDto>();
var periodInvoices = await _context.Invoices
.Where(i => i.CustomerId == customerId
.Where(i => i.CompanyId == companyId && i.CustomerId == customerId
&& i.Status != InvoiceStatus.Draft && i.Status != InvoiceStatus.Voided
&& i.InvoiceDate >= from && i.InvoiceDate <= toEnd)
.AsNoTracking().ToListAsync();
@@ -1373,7 +1379,7 @@ public class FinancialReportService : IFinancialReportService
var periodPayments = await _context.Payments
.Include(p => p.Invoice)
.Where(p => p.Invoice.CustomerId == customerId
.Where(p => p.CompanyId == companyId && p.Invoice.CustomerId == customerId
&& p.Invoice.Status != InvoiceStatus.Voided
&& p.PaymentDate >= from && p.PaymentDate <= toEnd)
.AsNoTracking().ToListAsync();
@@ -1391,7 +1397,7 @@ public class FinancialReportService : IFinancialReportService
var periodCredits = await _context.CreditMemoApplications
.Include(a => a.Invoice)
.Include(a => a.CreditMemo)
.Where(a => a.Invoice.CustomerId == customerId
.Where(a => a.CompanyId == companyId && a.Invoice.CustomerId == customerId
&& a.AppliedDate >= from && a.AppliedDate <= toEnd)
.AsNoTracking().ToListAsync();
@@ -1442,15 +1448,15 @@ public class FinancialReportService : IFinancialReportService
// Opening balance: bills payments credits before period start
var preBills = await _context.Bills
.Where(b => b.VendorId == vendorId
.Where(b => b.CompanyId == companyId && b.VendorId == vendorId
&& b.Status != BillStatus.Draft && b.Status != BillStatus.Voided
&& b.BillDate < from)
.SumAsync(b => (decimal?)b.Total) ?? 0;
var prePayments = await _context.BillPayments
.Where(bp => bp.Bill.VendorId == vendorId && bp.PaymentDate < from)
.Where(bp => bp.CompanyId == companyId && bp.Bill.VendorId == vendorId && bp.PaymentDate < from)
.SumAsync(bp => (decimal?)bp.Amount) ?? 0;
var preVcApplied = await _context.VendorCreditApplications
.Where(vca => vca.Bill.VendorId == vendorId && vca.AppliedDate < from)
.Where(vca => vca.CompanyId == companyId && vca.Bill.VendorId == vendorId && vca.AppliedDate < from)
.SumAsync(vca => (decimal?)vca.Amount) ?? 0;
var openingBalance = preBills - prePayments - preVcApplied;
@@ -1458,7 +1464,7 @@ public class FinancialReportService : IFinancialReportService
var lines = new List<StatementLineDto>();
var periodBills = await _context.Bills
.Where(b => b.VendorId == vendorId
.Where(b => b.CompanyId == companyId && b.VendorId == vendorId
&& b.Status != BillStatus.Draft && b.Status != BillStatus.Voided
&& b.BillDate >= from && b.BillDate <= toEnd)
.AsNoTracking().ToListAsync();
@@ -1475,7 +1481,7 @@ public class FinancialReportService : IFinancialReportService
var periodPayments = await _context.BillPayments
.Include(bp => bp.Bill)
.Where(bp => bp.Bill.VendorId == vendorId
.Where(bp => bp.CompanyId == companyId && bp.Bill.VendorId == vendorId
&& bp.PaymentDate >= from && bp.PaymentDate <= toEnd)
.AsNoTracking().ToListAsync();
@@ -1492,7 +1498,7 @@ public class FinancialReportService : IFinancialReportService
var periodVcApplied = await _context.VendorCreditApplications
.Include(vca => vca.VendorCredit)
.Include(vca => vca.Bill)
.Where(vca => vca.Bill.VendorId == vendorId
.Where(vca => vca.CompanyId == companyId && vca.Bill.VendorId == vendorId
&& vca.AppliedDate >= from && vca.AppliedDate <= toEnd)
.AsNoTracking().ToListAsync();