Skip to content

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-code T-122 UID comments were updated to T-126 to match. Commit messages stay historical T-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:

  1. extractCounterpartyId — the real helper checks transaction.counterpartyDirectoryId first, then falls back to gl.<code>.payeeId (the per-allocation payee stored on the gl block — used by WOPC payouts and director-current movements). The inlined copy fell back to expenseMetadata.payeeId, a field most relevant txs don't carry. Result: every WOPC-paid recipient came up empty.
  2. 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.tsextractCounterpartyId and extractExpenseGlCode are now export 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 (tip 26330aac); 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.tsx no longer renders the animated RollingDisplayName or its keyframes. The Description cell now shows a one-line ellipsized stored displayName, and the tooltip always exposes both full values: displayName and the original bank-statement description.
  • Verification: git diff --check clean; NODE_OPTIONS=--max-old-space-size=4096 npx tsc --noEmit clean.
  • 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.ts now exposes IR56M_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.ts imports that same set, so the modal transaction list and schedule total still cannot drift.
  • Regression: added __tests__/lib/accounting/ir56mServerEligibility.test.ts to 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.