IR56M Recipient modal audit list — share counterparty/expense-GL helpers with the schedule generator so totals can't drift
Renumbered T-122 → T-126 (2026-06-25, renumber-on-merge). While this task was in flight on
claude/busy-dirac-QmUdM(PR #815), parallel agents grabbed T-122 / T-123 / T-124 / T-125 on main first. Per the AGENTS.md "whoever merges second renumbers" rule, this task bounced to the next-free T-126; in-codeT-122UID comments were updated toT-126to match. Commit messages stay historicalT-122. README next-free bumped to T-127.
Why (owner, 2026-06-25)¶
Owner opened the IR56M Recipient modal on a recipient with a Declared Total of HK$44,500 and saw the new audit page (T-121) come up empty:
"The list shows Declared Total as $44,500, but the modal says: No contributing transactions found … Audit total HK$0 differs from the Filings tab total HK$44,500 — investigate before declaring."
The drift Alert that T-121 built specifically to catch this kind of bug fired correctly — the audit endpoint and the schedule generator were returning different totals for the same recipient.
Root cause¶
T-121's endpoint inlined copies of two helpers from
lib/accounting/ir56m.server.ts rather than importing them, and the
copies diverged from the originals in two ways:
extractCounterpartyId— the real helper checkstransaction.counterpartyDirectoryIdfirst, then falls back togl.<code>.payeeId(the per-allocation payee stored on the gl block — used by WOPC payouts and director-current movements). The inlined copy fell back toexpenseMetadata.payeeId, a field most relevant txs don't carry. Result: every WOPC-paid recipient came up empty.extractExpenseGlCode— the real helper's reserved keys are['coaching', '4000']. The inlined copy used['coaching', 'coachingInvoices', 'receipts'], a different skip set that mismatched the schedule's expense-code resolution on edge cases.
The original T-121 docstring claimed the inline copy was kept "to avoid widening that module's export surface for one consumer" — a wrong tradeoff. Sharing the helper costs one extra export; copy-pasting two helpers costs every divergence-introducing maintenance edit. Exporting wins.
What shipped¶
lib/accounting/ir56m.server.ts—extractCounterpartyIdandextractExpenseGlCodeare nowexport function …(were module-private). Docstrings explain that the IR56M Recipient modal's audit endpoint shares the same body.pages/api/accounting/ir56m/recipient-payments.ts— replaced the two inlined copies with imports of the now-exported originals. Endpoint logic is otherwise unchanged (still applies the same five filters in the same order).docs/eop-tasks/tasks/T-126.md(this) — opened + closed in one pass.
Why it's built this way (decision that wasn't the obvious choice)¶
The fix is the boring one — export + import — but the discipline matters: any time two readers must agree on a per-row filter rule, they share a function, not a copy. The "small export surface" framing from T-121 was wrong because the inline copy's drift wasn't catchable by tsc; exporting moves the invariant into code, not folklore.
The T-121 drift Alert that surfaced this is staying — even after this fix, future changes to one path that miss the other would re-introduce the same bug, and the in-modal Alert is the cheapest tripwire. It also catches drift between Firestore data and the cached schedule (e.g. an operator edits a tx mid-modal) that this fix doesn't address.
Decision log¶
2026-06-25 — T-126 opened + shipped¶
- ✅ Attestation (Accounting (Diagnostics)): read
AGENTS.md(tip26330aac); board scan by scope — T-121 owns the audit feature; T-126 is its first-bug-fix follow-up, not a duplicate. - Source: Accounting (Diagnostics) · https://claude.ai/code/session_01G58Y71noihrYCDEDMexmea
- Owner direction (verbatim, 2026-06-25):
"The IR56M modal doesn't seem to be reading the FY dropdown and loading information well. The list shows Declared Total as $44,500, but the modal says: No contributing transactions found …"
Note: owner suspected the YA-dropdown wiring; the real issue was the
helper drift. The YA dropdown's periodEndIso is correctly being
passed to the modal — but with the wrong counterparty resolution the
endpoint was missing every WOPC-paid recipient regardless of YA.
- Tests + tsc: npx tsc --noEmit clean. No new unit test — the
endpoint now imports the helpers the schedule path already exercises.
Live-render verification couldn't run in-sandbox (same dependency
chain issue as T-121); owner to re-open the modal and confirm the list
populates + the totals tie.
- Blast radius: the two helpers are now exported but nothing else
imports them. Pure refactor + bug fix on the endpoint; schedule
generator behaviour is unchanged.
2026-06-28 — follow-up: remove the rolling display-name marquee¶
- ✅ Attestation (Codex local session): read
AGENTS.md; checked the board by scope, not UID — this is the same IR56M Recipient modal audit list scope, not a duplicate. Tracking T-126. - Source: Codex local session ·
/Users/gutchumi/dev/ArtifactoftheEstablisher-codex-wopc-pdfjs - Owner direction (verbatim, 2026-06-28):
"Also, the IR56M modal transaction name too long that rolls back and forth for delay is pretty, but inefficient. Let's actually take those away, and make both the full original description and the display name to be shown in the tool tip instead"
- What changed:
components/accounting/tax/IR56MRecipientModal.tsxno longer renders the animatedRollingDisplayNameor its keyframes. The Description cell now shows a one-line ellipsized storeddisplayName, and the tooltip always exposes both full values:displayNameand the original bank-statementdescription. - Verification:
git diff --checkclean;NODE_OPTIONS=--max-old-space-size=4096 npx tsc --noEmitclean. - Blast radius: IR56M Recipient modal audit table only. The endpoint and total tie-out logic are unchanged.
2026-06-29 — follow-up: restrict IR56M eligible GLs to remuneration¶
- ✅ Attestation (Codex local session): read
AGENTS.md; checked the board by scope, not UID — this remains the same IR56M schedule/modal tie-out scope. Tracking T-126. - Source: Codex local session ·
/Users/gutchumi/dev/ArtifactoftheEstablisher-codex-wopc-pdfjs - Owner direction (verbatim, 2026-06-29):
"The IR56M still included 6400 and 6520"
- Root cause: T-126 correctly made the schedule and audit endpoint
share helper code, but the shared eligible set was still built from
every COA account with
type === "expense". That admitted real expenses that are not IR56M remuneration — specifically 6400 Bank Charges & Fees and 6520 Staff Welfare & Events — whenever those txs also carried a directory payee/counterparty. - What changed:
lib/accounting/ir56m.server.tsnow exposesIR56M_ELIGIBLE_GL_CODES/buildIR56MEligibleGlCodeSet()and the live set is GL 5050 only, matching T-047's locked filing rule: declared total = GL 5050 sub-contractor fees posted to the recipient within the YA.pages/api/accounting/ir56m/recipient-payments.tsimports that same set, so the modal transaction list and schedule total still cannot drift. - Regression: added
__tests__/lib/accounting/ir56mServerEligibility.test.tsto assert that 6400/6520 return null while 5050 remains eligible. - Blast radius: IR56M candidate generation and IR56M Recipient modal audit rows. Broader accounting GL categorization is unchanged; 6400 and 6520 still remain valid expense accounts elsewhere.