Movement of Equity GL-consistent; wire reconciliation checks R3/R5 (+R6/R9)

The Movement of Equity derived net income from sub-ledgers (calcNetIncome over
invoices/expenses/bills), while the P&L and Balance Sheet use the GL. With any
direct bank-categorized income/expense the two disagreed — Ashley Manor's SCE
was off from the Balance Sheet equity by 9,257.44. Rebuild Movement of Equity
from the GL (current-year earnings + GL equity balances) so all three statements
tie by construction.

Complete the §9 reconciliation matrix: R3 (P&L net income == raw-GL period net
income — guards the P&L builder) and R5 (Movement of Equity ending == Balance
Sheet total equity) are now computed by cross-checking the report builders against
the raw GL; checks render in numeric order. R6 (GL == TB/BS, satisfied by
construction) and R9 (direct vs indirect CFO, only indirect built) shown as N/A
for matrix completeness. Adds 5 reconcile unit tests (12 total).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 12:52:05 -04:00
parent 96de47496a
commit db20226d62
3 changed files with 115 additions and 25 deletions
+22
View File
@@ -36,6 +36,19 @@ export interface ReconcileInput {
* Defaults to true.
*/
arApApplicable?: boolean;
/**
* Net income for the period as produced by the app's P&L builder. R3 cross-checks
* it against the raw-GL period net income computed here, catching a P&L
* classification/grouping bug. Omitted → R3 is skipped.
*/
reportPLNetIncome?: number;
/**
* Ending equity from the Movement-of-Equity builder and total equity from the
* Balance Sheet builder. R5 cross-checks that the two statements agree.
* Both omitted → R5 is skipped.
*/
sceEndingEquity?: number;
bsTotalEquity?: number;
}
export interface RecCheck { id: string; label: string; residual: number; pass: boolean }
@@ -90,6 +103,14 @@ export function reconcile(input: ReconcileInput): RecCheck[] {
{ id: "R2", label: "Balance Sheet — Assets = Liabilities + Equity (incl. net income)", residual: assets - (liab + equity + netIncomeCum) },
{ id: "R4", label: "Cash Flow — CFO+CFI+CFF = change in cash", residual: (periodNI + nonCashImpact) - deltaCash },
];
// R3: the P&L builder's net income must equal the raw-GL period net income.
if (input.reportPLNetIncome !== undefined) {
checks.push({ id: "R3", label: "P&L net income = period net income from the GL", residual: input.reportPLNetIncome - periodNI });
}
// R5: the Movement-of-Equity ending equity must equal the Balance Sheet total equity.
if (input.sceEndingEquity !== undefined && input.bsTotalEquity !== undefined) {
checks.push({ id: "R5", label: "Movement of Equity ending = Balance Sheet total equity", residual: input.sceEndingEquity - input.bsTotalEquity });
}
// R7/R8 (sub-ledger vs GL control) only apply to platform-managed companies.
if (input.arApApplicable !== false) {
checks.push(
@@ -97,5 +118,6 @@ export function reconcile(input: ReconcileInput): RecCheck[] {
{ id: "R8", label: "A/P = open bill balances (§1.5)", residual: apControl - input.openBills },
);
}
checks.sort((a, b) => Number(a.id.slice(1)) - Number(b.id.slice(1)));
return checks.map((c) => ({ ...c, pass: near(c.residual) }));
}