uid: T-158 title: Company payee blocks resolve from the bank-account registry (kill the 7+ hardcoded copies) status: done area: accounting created: 2026-07-02 updated: 2026-07-09 related: I-030
T-158 β One getCompanyPayeeBlock(), zero hardcoded payee blocks¶
Why (from I-030, owner-requested sweep 2026-07-02)¶
The full ERL payee block (bank name + clearing code + account number + FPS ID)
is hardcoded in 7+ files and has drifted into live wrongness: customer-facing
payment requests carry account number 802-756699-831 (wrong β the registry
+ bank statements say 802-757699-831), and receipts carry FPS ID 114418007
(ERL-DSB-S β a closed account). The registry (bankAccount/{id}) is correct;
the copies are not. Owner's framing (verbatim): "the utilization of bank account
identifier (like 'ERL-AWX-HKD') is being undermined."
What to build¶
getCompanyPayeeBlock(bankAccountId)β one resolver (client + server flavors mirroringresolveBankAccountIdentifier(Server)) returning{ company, bankDisplay ("Oversea-Chinese Banking Corporation (035)"), accountNumber, fpsId, fpsEmail }from the registry doc. Company name from the subsidiary profile (_metadata/config), not a string literal.- Replace every hardcoded block (from I-030): coaching payment-request
DEFAULT_PAYEE;ToolsApppayment-request + receipt defaults;payment-request-preview+payment-receipt-previewdefaults/fallbacks;fetchSharedDoc.server.tsshared-doc payee block; coaching receiptfpsReceiverId. Default the receiving account per surface to the active registry account (today:ERL-OCBC-S) β configurable, not baked. - Freeze semantics unchanged: already-issued/shared documents keep the values they were issued with (documents snapshot at creation); the resolver feeds new documents and previews only.
- Fix the
scripts/migrate-coa.tsERL-DBS-*βERL-DSB-*seed drift (or retire the script if superseded). - Sweep-verify (T-156-style propagation matrix in this file) that no literal
756699,115278533,114418007, or "Overseas Chinese Banking" strings remain outside tests/docs/issued-document fixtures.
Not in scope¶
The structural ID plumbing consolidation (prefixβDB map Γ4, split('-')[0]
Γ7, provider maps like OCBC acct#βid) β that's T-157 Phase 0, where the
routing layer gets centralized as the consolidation's foundation.
Decision log¶
2026-07-02 β opened from I-030¶
- β
Attestation (Accounting (Diagnostics)): read
AGENTS.md; board checked by scope β no existing task covers payee-block resolution (T-156 covers display labels; this is payment-instruction data). Escalated from I-030. - Source: Accounting (Diagnostics) Β· https://claude.ai/code/session_01G58Y71noihrYCDEDMexmea
- Proposed by: Accounting (Diagnostics) (from the owner-requested hardcode sweep). Approval to open: owner directed the investigation + simultaneous Issue-1 follow-ups (2026-07-02); the fix itself is unstarted β any agent may pick this up, but coordinate with T-157's owner (Accounting (Infrastructure)) since Phase 0 touches neighbouring plumbing.
- Urgency note: item 2 fixes a live wrong-account-number on customer-facing payment requests β worth shipping ahead of the big consolidation.
- 2026-07-02 hotfix (Diagnostics): the owner confirmed
757699-831is correct (verbatim in I-030); the five wrong literals were hot-fixed in place ahead of this task. Remaining here: the resolver itself, the receipts' FPS ID decision (a re-rendered receipt of an old DSB-era payment may legitimately show the closed account's FPS114418007β per-payment provenance, not a blind swap; new receipts should carry the active receiving account), the migrate-coa seed drift, and the final no-literals sweep.
2026-07-03 β built (in PR #838; done on merge)¶
- β
Attestation (Accounting (Diagnostics)): read
AGENTS.md. Owner: "Proceed to T-158 first", then "When you're done, please also check if any backfilling of records needed." - Source: Accounting (Diagnostics) Β· https://claude.ai/code/session_01G58Y71noihrYCDEDMexmea
- Resolver:
lib/companyPayee.ts(client; fetches via/api/bank-accounts/[id]β Admin-SDK-backed, avoiding the client-SDK rules asymmetry β + subsidiary profile for the company name) andlib/companyPayee.server.ts(Admin SDK). Contract: never throws; falls back field-by-field toFALLBACK_COMPANY_PAYEE_BLOCKβ the single hardcoded copy left in the codebase, registry-verified. Customer-facing semantics: uses officialbankName, never the T-156 nickname.DEFAULT_RECEIVING_BANK_ACCOUNT_ID = 'ERL-OCBC-S'is the one named interim constant (T-157 P1 owns making it a subsidiary-profile field). - Consumers rewired (6): coaching payment-request page (ref-based defaults,
registry-refreshed); ToolsApp payment-request + receipt sandboxes;
payment-request-preview+payment-receipt-previewtools; the coaching receipt page β which now shows per-payment provenance: the FPS ID of the account that actually received the payment (payment.identifierβ registry via the extendedlookupAccount, which now surfacesfpsId/fpsEmail), falling back to the current default only when the payment records no account. - Shared payment-request docs (
fetchSharedDoc.server.ts): precedence β stored invoice payee fields first (issued documents are frozen history), else live registry resolution. This fixed a pre-existing wrong behavior: the old hardcoded block silently overrode the 15 DSB-era invoices' stored Dah Sing details with OCBC ones (and the wrong digit). - Seed drift:
scripts/migrate-coa.tsERL-DBS-*βERL-DSB-*fixed. - Backfill check (owner-requested) β verdict: NO backfill needed. Read-only
scan of all 113 coaching session invoice maps (nested
Students/*/Sessions; legacy top-level +collectionGroup('invoice')both empty): 0 stored docs carry the wrong756699number (the typo lived only in code defaults, never persisted). The only stored payee blocks are 15 Γ the DSB-era block (Dah Sing Bank (040)/747-018-22400-8/ FPS114418007) β correct frozen history from when ERL received via Dah Sing, now honoured (not overridden) by the shared-doc renderer. - Literal sweep: no
756699/115278533/114418007/ "Overseas Chinese Banking" remain in live code outside the module fallback. Inert residuals, deliberately left: archivedSheets HTML/*exports (frozen documents) andlib/paymentRequest/instructionsScheme.tstemplate cells (verified inert βPaymentInstructions.getDynamicValueoverrides every payee cell from live data unconditionally). - Verification:
tsc --noEmitclean Β· vitest 583/583. Owner-on-deploy: a new coaching payment request and the Tools previews show the OCBC block from the registry; an old DSB-era shared request shows its stored Dah Sing block; a receipt for a DSB-era payment (withidentifier) shows FPS114418007, a new OCBC-era one shows115278533. - Blast radius (payment documents): payee-block resolution on coaching
payment requests/receipts, Tools previews, and public shared payment-request
docs. No data writes; issued documents unchanged. New dependency:
those surfaces now read
/api/bank-accounts/[id]+ the subsidiary profile at render (session-gated internal pages; the public shared-doc path uses the Admin-SDK server resolver).
2026-07-03 β renumbered T-154 β T-158¶
- β
Attestation (Accounting (Diagnostics)): read
AGENTS.md; renumber-on-merge, scope unchanged. - Source: Accounting (Diagnostics) Β· https://claude.ai/code/session_01G58Y71noihrYCDEDMexmea
- Why: T-154 was taken on
mainby a parallel agent (iOS 26 Safari optimization). This task (payee-block resolver) moved whole-identity β T-158, incl. in-code[T-158]markers across the resolver + consumers.
2026-07-09 β CLOSED (shipped, deployed, live) β status β done¶
- β
Attestation (Accounting (Diagnostics)): read
AGENTS.md. Board checkup; stale atdoing. - Source: Accounting (Diagnostics) Β· https://claude.ai/code/session_01G58Y71noihrYCDEDMexmea
- Merged in PR #838 β
main@c5e1cf75(2026-07-03) and deployed the same day. The registry-resolved payee blocks are live on every consumer (coaching payment requests, ToolsApp + both previews, receipts with per-payment FPS provenance, shared docs with stored-payee-wins); the wrong-digit hotfix and the closed-account FPS fix are in production. Backfill check closed pre-merge (0 stored docs affected; 15 DSB-era blocks correctly frozen). I-030 β fixed.