Empirical per-company trial-balance net on stored CurrentBalance. Both
tenants imbalanced, but from pre-existing data, not this session:
- Demo: $89.5k opening-balance-without-equity (demo artifact) + $3,153.63 postings.
- SCP: $3,079.52, all postings. Forensics: AR reconciles (invoices−payments),
but Revenue has $0 GL movement (24 header-only invoices, 0 line items → no
per-item revenue credit) and payment-side bank debit never posted. One-sided
postings from imported/header-only docs + null offset accounts skipped by
AccountBalanceService — same class as O2/O6/O7/O8.
Conclusion: this session's changes did not introduce the imbalance and in
fact prevent the bug class going forward. Remediation options documented
(not auto-applied — SCP is live data).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Item 1 — server-side guard (defense in depth) on payment-source / deposit
/ reconcilable account selections. New AccountGuard.IsValidMoneyAccountAsync
checks the submitted account is active, company-owned, and an Asset or
Liability before any GL posting, at: bill RecordPayment, bill Create
(payNow), bill EditPayment, BankReconciliation.Create, and deposit Record.
The dropdowns already constrain normal users; this rejects tampered/stale
POSTs. Per the "trust the operator" decision it still allows A/R etc.
(any Asset/Liability) — it only blocks non-money types.
Item 2 — account AccountType is now derived from the chosen AccountSubType
on create/edit via the new AccountClassification.TypeForSubType (single
source of truth, also used by the Create pre-select). The two can no longer
disagree, so the sub-type-based debit/credit sign convention is always
consistent with the account's type. A read-only sweep of the dev DB found
0 existing mismatches, so no repair tool was built.
Audit doc updated: both backlog items marked resolved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit of this session's accounting changes (sub-type→type dropdowns,
deposit account picker, default GL accounts) found no ledger-drift bugs.
Two fixes applied:
- Default revenue account now requires IsActive (mirrors the 4000
fallback), so a deactivated default isn't silently posted to.
- DepositsController.Record blocks recording when the 2300 Customer
Deposits liability exists but no deposit/bank account resolves — that
would post a one-sided entry. When 2300 doesn't exist (no accounting),
nothing posts, so the deposit is still allowed.
ACCOUNTING_AUDIT.md updated: O9 footgun surface widened by the default-
accounts feature (now mitigated/documented), plus the 2026-06-20 review
notes and the resolved deposit-imbalance item.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Repeatable post-deploy (and periodic) check that proves the books are consistent
against real data: per company, Trial Balance debits==credits, Balance Reconciliation
shows no drift, then Recalculate Balances and re-check. Includes the read-only
pre-deploy migration preview, the two pending migrations in order, account spot-checks
for the audit-touched accounts, and the inventory/sales-tax policy reminders.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner decision: this is a service business that uses materials to deliver a service
rather than selling inventory, so powder/consumables are expensed at purchase (bill
to a COGS/expense account) and inventory is not capitalized on the Balance Sheet.
No code change required — the Balance Sheet already behaves this way, and the
perpetual consumption-COGS path (O6) is opt-in via item account mappings (set only
via CSV import) and stays dormant under this policy. Documents the double-count
footgun (do not both expense at purchase and map item COGS/Inventory accounts) and
locks the periodic choice into the ledger-refactor plan's Phase 5.
All accounting audit findings O1-O9 now resolved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Captures the phased plan to make JournalEntry lines the single source of truth for
all GL balances/reports, retiring the parallel re-derivation in LedgerService and
FinancialReportService. Resolves the O2/O6/O7/O8 bug class structurally and folds in
O9 (inventory capitalization) at Phase 5. Proposed only — not started.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
O6: inventory consumed on jobs posts DR COGS / CR Inventory, but neither recompute
engine reflected it — so reports understated COGS / overstated inventory and a
"Recalculate Balances" wiped the effect. The COGS posting fires only for JobUsage
and Waste transaction types, which are created only at the two COGS-posting sites,
so the consumption is exactly identifiable from InventoryTransaction:
- both posting sites now record consumption at the effective (weighted-average)
unit cost so TotalCost equals the COGS posted (the recompute reads TotalCost)
- LedgerService: new section (dated rows + prior balance) crediting Inventory /
debiting COGS from JobUsage/Waste rows on items with both accounts mapped
- FinancialReportService: Trial Balance + accrual P&L include consumption COGS
This reads existing transactions, so historical data is covered with no backfill.
The Balance Sheet inventory line is intentionally left alone — it does not track
inventory purchases either (periodic), so relieving it for consumption alone would
unbalance it; tracked as O9 (inventory capitalization policy).
O8: the write-off already creates a balanced posted JournalEntry (both engines read
it via their JE-line sections). The real defect was 4 "Status != WrittenOff" filters
in FinancialReportService that excluded pre-write-off payments from AR credits and
bank debits — leaving the paid portion dangling as open AR and understating the bank.
Removed those filters; AR now nets to zero for written-off invoices and the trial
balance balances. No backfill needed.
Adds a LedgerService regression test for inventory consumption. Build clean; 293
unit tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ApplyGiftCertificate posts DR 2500 Gift Certificate Liability / CR AR, but the AR
recompute only subtracted payments and credit-memo applications — so the redemption's
2500 debit was recomputed while its AR credit was not, leaving the Trial Balance out
of balance by the total gift-certificate amount redeemed and overstating AR on the
Balance Sheet.
Subtract GC redemptions from AR in both recompute engines:
- FinancialReportService: Balance Sheet (gcRedeemedBs) and Trial Balance (gcRedeemedTb)
- LedgerService: AR section (dated rows) and ComputePriorBalanceAsync (prior balance)
AR Aging was already correct (uses BalanceDue, which includes GiftCertificateRedeemed).
Adds a LedgerService regression test. Build clean; 292 unit tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the read-path defense-in-depth pass flagged in the accounting audit:
every Accounts lookup in a controller now carries an explicit CompanyId predicate,
matching the standing rule in CLAUDE.md ("every FindAsync/GetAllAsync must include
an explicit CompanyId"). ~19 lookups across 12 controllers:
- Tier 1 (write-path): AccountsController duplicate account-number check (Create/Edit)
- Tier 2 (dropdowns/lists): Accounts (Index/year-end/parent), BankReconciliations,
Bills (bank list + receipt scan + suggest), Budgets, CatalogItems, Expenses,
FixedAssets, Inventory, JournalEntries chart dropdown, Vendors
- Tier 3 (accountIds.Contains display maps): JournalEntries/Reports/VendorCredits
detail views, scoped via the in-scope entity's CompanyId for uniformity
companyId source per controller: _tenantContext where available, else the in-scope
entity's CompanyId, else the current user. Build clean; 291 unit tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Account 2500 is resolved by number as the GC liability (GiftCertificatesController),
but the per-tenant seeder never created it — so tenants onboarded after the
AccountingGapsPhase2 migration had no GC liability account and gift-certificate GL
postings silently no-op'd. The default-company seeder also created 2500 as
"Long-Term Loan", mislabeling that company's GC obligations.
- SeedDataService.Accounts: seed 2500 "Gift Certificate Liability" (IsSystem)
- SeedData: seed 2500 as GC liability; move long-term loan to 2900
- EnsureSystemAccountsAsync: self-heal — rename a 2500 still named "Long-Term Loan"
(preserving user renames) and ensure a 2500 exists
- migration FixGiftCertificateLiabilityAccount: move long-term loan to 2900 where a
2500="Long-Term Loan" exists without a 2900, relabel the mislabeled 2500, and
safety-net insert a 2500 for any company lacking one
Non-destructive: no account Id/number/balance is changed (same pattern as O1).
Verified on dev: existing GC-liability rows preserved, no spurious accounts added.
All audit findings O1-O5 resolved. Build clean; 291 unit tests pass; migration applied.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
O3: defense-in-depth on the write/posting path. Finding #7 scoped the report
(read) path; this scopes every GL posting-path account lookup that determines
where money lands, so a SuperAdmin acting in a company context can never post to
another tenant's account:
- InvoicesController: all account-resolver helpers (checking, customer deposits,
sales returns, customer credits, AR, bad debt, sales tax, sales discount, GC
liability) plus the bank-account and write-off expense dropdowns
- CreditMemosController: Create/Apply/Void GL lookups (scoped via the in-scope
customer/invoice/memo)
- GiftCertificatesController: Create/BulkCreate/Void GL lookups + GC liability helper
- BillsController: AP/expense account resolution that pre-fills APAccountId
DepositsController and JournalEntriesController.SalesTaxPayment were already scoped.
O4: SalesTaxPayment now rejects a remittance greater than the outstanding Sales
Tax Payable balance (0.005 rounding tolerance), so a typo can no longer drive
2200 into an abnormal debit balance.
Remaining pure read-path dropdown lookups (app-wide, lower risk) are documented
in docs/ACCOUNTING_AUDIT.md as a separate follow-up. All audit findings O1-O4 are
now resolved. Build clean; 291 unit tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
O1: account 2300 has always been used by the deposit GL code as the Customer
Deposits liability (resolved by number), but it was seeded/named "Payroll
Liabilities" for tenants the AccountingDepositsGL migration's NOT EXISTS guard
skipped — so the liability was mislabeled on the balance sheet. Rename 2300 to
"Customer Deposits" (IsSystem) and move payroll to a new 2400 account:
- both seed paths (SeedDataService.Accounts, SeedData)
- EnsureSystemAccountsAsync self-heal (renames only where still default-named,
preserving user renames; ensures 2400 exists)
- migration RenameDepositsAccountAddPayroll for existing tenants
Account number 2300 is unchanged, so the deposit posting code needs no changes.
O2: LedgerService never recomputed 4950 Sales Discounts, so "Recalculate
Balances" wiped it to JE-only and the Balance Reconciliation report showed false
drift. Add a 4950 section to GetAccountLedgerAsync and ComputePriorBalanceAsync
that reproduces the actual postings (invoice discounts DR + credit-memo issuance
DR, less the unapplied remainder of voided memos CR), matching AccountBalanceService.
Adds a LedgerService regression test for 4950. Documents both fixes plus the
remaining open findings (O3, O4) in docs/ACCOUNTING_AUDIT.md so the audit is no
longer lost. Build clean; 291 unit tests pass; migration applied.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deletes the committed dotnet publish output folder (434 files: DLLs,
bundled static assets) plus 73 stray root files (old *_FIX/*_SUMMARY
docs, .bak files, loose .sql scripts, deploy.zip, screenshots) and a
few scripts/. Repo housekeeping to reclaim disk space; no src/ or
wwwroot/ files touched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Quotes Create/Edit: hide 'Send via email' checkbox when customer has no
email; show badge 'send via SMS from details' or 'SMS consent required'
when customer has a mobile number. JS responds to customer dropdown change.
- Quotes Details: hide 'Send Quote via Email' button and approval email
checkbox; hide SMS button when no mobile; show consent-required note.
- Jobs Details (Mark Complete modal): hide email checkbox; show
'SMS notification will be sent' badge or consent-required note.
- Jobs Index (status modal): hide email row when customer has no email.
- Jobs Edit: hide 'Notify customer if status changes' when no email.
- Invoices Details: hide Send/Re-send buttons when no email (vs. disabled).
DTOs: added CustomerEmail + CustomerNotifyByEmail to JobDto/JobListDto;
added CustomerNotifyByEmail/CustomerMobilePhone/CustomerNotifyBySms to
QuoteDto. Mapped in JobProfile and QuotesController customer blocks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Invoice-basis report showing taxable vs non-taxable sales, tax billed
by GL account, monthly trend table/chart, and full invoice detail grid.
Non-taxable invoice rows shaded grey for easy scanning. Quick-preset
date buttons (This Month, Last Month, YTD, Last Year) for common filing
periods. CSV export formatted for accountants and tax-filing software.
Gated behind AllowAccounting() like other financial reports.
- SalesTaxReportDto + 3 supporting DTOs in FinancialReportDtos.cs
- GetSalesTaxReportAsync on IFinancialReportService + implementation
- GenerateSalesTaxReportPdfAsync on IPdfService + QuestPDF implementation
- SalesTax / SalesTaxPdf / SalesTaxCsv actions in ReportsController
- Views/Reports/SalesTax.cshtml with Chart.js monthly trend chart
- Landing page card added to Finance section
- HelpKnowledgeBase and Help/Reports.cshtml updated with full docs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 3 — eliminated ApplicationDbContext from all non-exempt controllers,
routing all data access through IUnitOfWork. Added IPlainRepository<T> for
the four platform entities (Announcement, BannedIp, DashboardTip, ReleaseNote)
that intentionally don't extend BaseEntity and therefore can't use the
constrained IRepository<T>. Added permanent-exception comments to the 18
controllers that legitimately retain direct DbContext access (Identity infra,
cross-tenant platform ops, bulk streaming exports).
Phase 4 — added EnforceDataAccessArchitecture() to Program.cs, a startup
gate that reflects over every Controller subclass and throws at boot if any
non-exempt controller injects ApplicationDbContext. The app cannot start with
a violation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Defines the target architecture for eliminating direct ApplicationDbContext
injection from controllers. Documents the three-tier model (generic repo,
typed domain repos, read services), the 6 typed repository interfaces to
build, the 2 reporting service interfaces to build, permanent exceptions,
and the 4-phase migration roadmap with per-controller checklist.
CLAUDE.md updated with the hard rule and tier quick-reference so every
session and every team member sees the constraint immediately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>