IR56M Recipient modal — add a contributing-transactions audit page before the form fields
Why (owner, 2026-06-25)¶
The IR56M Recipient modal opens straight onto the auto-populated form fields (amounts split across sub-contracting / commission / writer / artiste / copyright / consultancy / service-fee buckets, plus the identity block from Contacts). The figures come from the schedule generator's per-payee aggregation — but the user has no audit-trail view of which transactions added up to those numbers before signing off on the declaration.
"before showing the auto-populated information that will be rendered to the form, can you have a page that shows a list of transaction that contributes to the recipient's total that is going to be declared on the IR56M form while adding a next button to proceed to the next page which is the current first page of the modal, with a cancel button that closes the modal"
What shipped¶
The modal is now 3 pages:
| Page | Was | Now | Purpose |
|---|---|---|---|
| 0 | (new) | Contributing transactions | Read-only audit list of every payment that summed into the auto-populated total. Footer: [Cancel] [Next →]. |
| 1 | Page 1 | Review / Edit | Form-field capture + signing-director picker. Footer: [Cancel] [← Back] [Preview →]. |
| 2 | Page 2 | Preview | Filled IR56M PDF preview. Footer: [Cancel] [← Back] [Request Signature]. |
Backend¶
pages/api/accounting/ir56m/recipient-payments.ts(new).GET ?payeeId=…&periodEndIso=YYYY-MM-DD&subsidiaryId=erl→{ rows, total, count }. Applies the same five filtersbuildIR56MScheduleuses (isDebit === true· valid date · counterpartyId matches ·amount > 0· GL is an expense code · date ∈ YA), so the audit-list total ties exactly to thetotalPaidthe Filings tab and the modal's existing hint already display.- The two helpers
extractCounterpartyId/extractExpenseGlCodeare inlined in the endpoint (rather than imported fromir56m.server.ts) to keep the export surface of that module small for one consumer. They are literal copies of the originals.
Frontend¶
components/accounting/tax/IR56MRecipientModal.tsx— page state widened from1 | 2to0 | 1 | 2. Modal always opens on page 0 now. The audit list (<RecipientAuditList>, defined at the bottom of the file) renders in a fixed-header scrolling antd<Table>with Date / Description / GL / Amount columns and aTotalsummary row showing the running sum + payment count. Drift between the audit-fetched total and the parent'stotalPaidHint(if any) raises an in-place warning Alert — that condition means the schedule generator and the audit endpoint disagree, which would be a real bug to investigate.- Cancelling from page 0 closes the modal without the unsaved-edits prompt — nothing has been touched yet, so there's nothing to discard. The existing dirty-tracking semantics on pages 1 and 2 are unchanged.
components/accounting/tax/IR56MFilingTab.tsx— passesperiodEndIsoandsubsidiaryId(read from the seed payload) to the modal so the audit endpoint can scope the fetch.
Why it's built this way (decisions that aren't the obvious choice)¶
- Audit list is fetched at modal-open time, not derived from a denormalized payload on the schedule. The schedule endpoint already returns per-payee aggregates; adding the full tx list to that response would bloat every Filings-tab table load for a feature only used after the user clicks a recipient. The per-recipient fetch is cheaper overall.
- Filter mirrored inline, not imported from
ir56m.server.ts. ExportingextractCounterpartyId/extractExpenseGlCodefrom a shared spot would have spread the IR56M filter rule across two readers; the inline copy keeps the schedule-generator a single source of truth that the endpoint reads-around. If the rule changes, both must change together — the docstring on the endpoint flags this. - Drift warning is in-modal, not silently corrected. If the audit total disagrees with the parent's
totalPaidHint, the user is the one who'd want to know — and the schedule-generator and endpoint should never disagree. Hiding the mismatch would mask a real bug.
Out of scope (deliberate)¶
- Per-row drill-through to the bank-tx in Accounting. The bank tx is identifiable by
idin the row data — if the bookkeeper wants to investigate a payment they can paste the id into the Accounting page's tx-search. Adding a click-through link adds wire-up that wasn't in the owner's prompt; defer until requested. - Pagination / virtualization for very large recipient lists. Today no recipient in prod has more than a handful of payments per YA; the table just scrolls inside the modal. Revisit if a recipient ever crosses ~200 rows.
Decision log¶
2026-06-25 — T-121 opened + shipped¶
- ✅ Attestation (Accounting (Diagnostics)): read
AGENTS.md(tip26330aac); board scan by scope — T-047 owns the IR56M modal as a feature, T-058 owns fiscal-period UX, T-043 owns the WPP-side audit story. No covering task for "show contributing txs in the Recipient modal." New task. - Source: Accounting (Diagnostics) · https://claude.ai/code/session_01G58Y71noihrYCDEDMexmea
- Owner direction (verbatim, 2026-06-25): the prompt quoted in the "Why" section above.
- Tests + tsc:
npx tsc --noEmitclean. No new unit test — the endpoint's filter is a literal line-by-line mirror ofbuildIR56MSchedule's payment filter (lib/accounting/ir56m.ts:148-152), which is already exercised by the schedule path; the modal change is presentational (table render + state extension). Live render verification couldn't run in-sandbox becauseir56m.server.ts's dependency chain pulls a client-side firebase config; left as a post-deploy verification step for the owner. - Verification asked of owner: open the Filings tab, click a recipient → modal opens on the new page 0 → the table lists the contributing payments; the footer Total matches the
totalPaidshown in the Filings tab; clicking Next advances to the original form-fields page. Cancel from page 0 should close immediately without the save-prompt. - Blast radius: new endpoint reads only the bank-transactions collection the schedule already reads — no write surface, no permission changes. Modal change adds one new page state but the existing pages' behaviour (form validation, save-prompt, PDF preview fetch) is untouched. The parent component change is two extra props on an existing JSX element.
Side note — FY open/close workflow¶
Owner mentioned this came up in a prior conversation. Confirmed: the primitive (maybeFreezeFiscalYear in lib/periodClose/snapshot.server.ts) shipped in commit 78ef8294 as part of T-043 #7, but the explicit user-action workflow (open / soft-close / hard-close ladder with RE roll-forward + adjusting-JE review window) was never built. Discussed scoping a new task if the owner wants the workflow layer formalised — pending their go-ahead, this T-121 doc is the only record here.
2026-06-28 — Long display names roll left in the audit list¶
✅ Read AGENTS.md · Codex local session · checked the board by scope; T-121 owns the IR56M recipient modal audit list, so no duplicate task was opened · tracking T-121.
Source: Codex local session · local Codex desktop thread
Owner report (verbatim, 2026-06-27):
"While the display name on the IR56M modal could be really long, can you make the display name roll to the left so that the rest of the name can be shown?"
What changed. components/accounting/tax/IR56MRecipientModal.tsx now renders the audit-list display
name through RollingDisplayName, which measures the cell with ResizeObserver and animates left only
when the rendered display name overflows its column. The raw bank-statement description remains available
as the tooltip added by the T-127 display-name follow-up.
Verification. NODE_OPTIONS=--max-old-space-size=4096 npx tsc --noEmit passes.
Blast radius. Presentation-only change inside the IR56M recipient modal's contributing-transaction table. The endpoint, total tie-out, and IR56M form data are unchanged.