Scope GL posting account lookups by CompanyId; cap sales-tax remittance (audit O3, O4)
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>
This commit is contained in:
+26
-21
@@ -80,29 +80,34 @@ migration applied to the dev database successfully.
|
||||
|
||||
---
|
||||
|
||||
## Open findings (deferred — to do after O1/O2 per user)
|
||||
### O3 — Write-path account lookups omitted explicit `CompanyId` — **RESOLVED**
|
||||
- Added `CompanyId` to every **GL posting-path** account lookup that determines where money posts:
|
||||
- `InvoicesController` — all account-resolver helpers (`GetCheckingAccountIdAsync`,
|
||||
`GetCustomerDepositsAccountIdAsync`, `GetSalesReturnsAccountIdAsync`, `GetCustomerCreditsAccountIdAsync`,
|
||||
`GetArAccountIdAsync`, `GetBadDebtAccountIdAsync`, `ResolveSalesTaxAccountIdAsync`,
|
||||
`GetSalesDiscountAccountIdAsync`, `GetGcLiabilityAccountIdAsync`) plus the bank-account and write-off
|
||||
expense dropdowns (scoped via `_tenantContext`).
|
||||
- `CreditMemosController` — Create/Apply/Void GL lookups (scoped via the in-scope `customer`/`invoice`/`memo`).
|
||||
- `GiftCertificatesController` — Create, BulkCreate, Void GL lookups + `GetGcLiabilityAccountIdAsync`.
|
||||
- `BillsController` — AP/expense account resolution that pre-fills `APAccountId` (Create + CreateFromPO).
|
||||
- `DepositsController` and `JournalEntriesController.SalesTaxPayment` already scoped correctly.
|
||||
|
||||
### O3 — Write-path account lookups omit explicit `CompanyId` (defense-in-depth gap) — **LOW-MEDIUM**
|
||||
- Finding #7 added explicit `CompanyId` predicates to the **read** path, but several **posting** lookups
|
||||
still rely on the global filter alone, e.g. `CreditMemosController` (182-184, 268, 317-318) and
|
||||
`InvoicesController` refund/credit account lookups use `FirstOrDefaultAsync(a => a.AccountNumber == "2350" && a.IsActive)`
|
||||
with no `CompanyId`. `JournalEntriesController.SalesTaxPayment` (246) does it correctly.
|
||||
- **Impact:** low in practice (these run under a non-SuperAdmin company context where the global filter
|
||||
applies), but it violates the repo's standing rule and would post to the wrong tenant's account if ever
|
||||
executed in a SuperAdmin/multi-company context.
|
||||
- **Fix direction:** add `a.CompanyId == companyId` to every account lookup on the posting path.
|
||||
### O4 — Sales-tax remittance could over-remit (drive 2200 negative) — **RESOLVED**
|
||||
- `JournalEntriesController.SalesTaxPayment` (POST) now rejects any amount exceeding `taxAcct.CurrentBalance`
|
||||
(0.005 rounding tolerance), so a typo can no longer push Sales Tax Payable into an abnormal debit balance.
|
||||
|
||||
### O4 — Sales-tax remittance can over-remit (drive 2200 negative) — **LOW**
|
||||
- `SalesTaxPayment` validates amount > 0 but does **not** cap the payment at the outstanding 2200 balance.
|
||||
- **Impact:** a typo can push Sales Tax Payable negative (a debit-balance liability), which is an
|
||||
abnormal balance that then surfaces oddly on the balance sheet.
|
||||
- **Fix direction:** show the current 2200 balance (already shown on the GET form) and reject/warn when
|
||||
`amount` exceeds it.
|
||||
Verification of O3+O4: `dotnet build` clean; `dotnet test tests/PowderCoating.UnitTests` → **291 passed**.
|
||||
|
||||
---
|
||||
|
||||
## Suggested priority order
|
||||
1. **O1** (mislabeled/commingled liability — real balance-sheet correctness issue)
|
||||
2. **O2** (recalc corruption + reconciliation report reliability)
|
||||
3. **O3** (defense-in-depth on write path)
|
||||
4. **O4** (input guard)
|
||||
## Remaining (non-O1–O4) — known lower-risk follow-up
|
||||
Pure **read-path dropdown/display** account lookups still rely on the global tenant filter (correct for
|
||||
non-SuperAdmin; only a SuperAdmin acting inside a company could see another tenant's accounts in a picker).
|
||||
These are app-wide, not specific to the accounting posting path, e.g. `VendorCreditsController`
|
||||
PopulateDropdowns/Details, `BillsController` PopulateDropdowns and Index/edit account lists,
|
||||
`ExpensesController` filter + categorization pickers. Tracked here so they aren't mistaken for a regression;
|
||||
fold into a broader defense-in-depth pass if/when desired.
|
||||
|
||||
## Status
|
||||
All audit findings **O1–O4 are resolved** on `dev`. Original audit numbering #1–3/#5/#6/#8 remains
|
||||
unrecoverable (see top). The accounting batch has **not** been merged to `master`/production yet.
|
||||
|
||||
Reference in New Issue
Block a user