uid: T-164 title: Fix invoice writer to write canonical detail.payment.payTo (close the I-032 write-side drift) status: done area: accounting-invoices created: 2026-07-10 updated: 2026-07-10 related: I-032, T-078
Close-out (2026-07-10): β merged (
064a5b5c) Β· β deployed to Vercel production (dpl_2JqBCDQtXQagi96mAXa2MHUjJEmc, commite7d1985bwhich contains the fix, READY) Β· β prod data 0-drift (31/31 invoices canonical). The fix is deterministic and proven by the transform repro + unit tests; the one thing not drivable from a cloud agent is a UI edit (no app session), so the next organic invoice edit is the belt-and-suspenders confirmation. See the 2026-07-10 (iv) log entry.
T-164 β Stop the invoice editor deleting the canonical payTo¶
Deliberate write-side fix for the structural bug diagnosed in I-032 (the reader-only patch
dcd1b631 masked the symptom; the writer kept re-drifting). Origin traced in I-032: T-078 moved
the canonical payTo home to detail.payment.payTo but, by design, left the Phase-2 writer emitting the
old paymentDetail.payTo shape and delegated conversion to transformPayloadToNestedShape β whose payTo
handling was incomplete on the UPDATE path and mapped the writer's legacy-cleanup payTo delete onto the
new canonical field, deleting it on every edit.
What shipped¶
- Writer fix β
lib/projectInvoices.server.ts:buildInvoiceWritePayload(UPDATE branch + the clientCompany payTo-preservation branch): emit the value as a top-levelpayTo(which the transform routes todetail.payment.payTo) and delete the legacypaymentDetail.payToslot β instead of writing the value to the legacy slot and deleting the top-level field (which the transform turned into a canonical-field delete). Create path was already correct (emitspaymentDetailas an object, which the transform promotes) and is unchanged. The client writer (lib/projectInvoices.ts) was already correct β it routes payTo throughclientCompany.payTo, which the transform extracts to canonical β verified, not touched. - Regression test β
__tests__/lib/invoiceDocShape.test.ts(2 cases): the fixed writer shape (top-level payTo value + legacy-slot delete) yieldsdetail.payment.payTo = value; and a guard encoding the OLD shape (value in legacy slot + top-level payTo delete) that DELETED the canonical field β so a revert fails loudly. - Data normalization β the 2 invoices that had drifted (
ERL-2026-004-0508,ERL-2026-005-0601): movedpaymentDetail.payToβdetail.payment.payTo, deleted the legacy slot. Post-fix scan: 0 of 31 tebs-erl invoices legacy-only.
Verification¶
tsc --noEmit clean (0 errors); full suite green (19 files / 195 tests incl. the 2 new I-032 cases);
transform repro confirmed the fixed writer shape β canonical (detail.payment.payTo = "ERL-AWX-HKD"),
legacy slot cleaned; live read-back on both normalized invoices shows canonical set + legacy gone.
Deploy note¶
main is π‘π‘ (Vercel, manual deploy) β this fix is committed but not live until a manual deploy.
The data normalization is already in prod (a direct Firestore write). Until the code deploys, an edit on
the still-old build could re-drift an invoice, but the deployed reader fallback (dcd1b631) keeps it
user-visible either way; after deploy, edits keep payTo canonical.
Scope / follow-ups¶
- Coaching invoices (tebs-mel, different doc layout) not assessed β separate surface.
- A writer-level unit test would need
buildInvoiceWritePayloadexported (importing the.server.tspulls in the admin SDK); the invariant is instead locked at the transform level, which is the contract the writer depends on.
Decision log¶
2026-07-10 (iv) β CLOSED (status β done): deployed + prod-verified¶
- β
Attestation (Projects (Infrastructure)): read
AGENTS.md. - Source: Projects (Infrastructure) Β· https://claude.ai/code/session_01Q2xAAEtBSMfLUGjP1HAA5D
- Deploy β owner-ordered, one-off. Owner: "Deploy now" (2026-07-10). The Vercel connector's
deploy_to_vercelis a file-tree upload (wrong for this git-integrated project); env had no creds post-container-refresh, so per AGENTS.md's discovery order I readVERCEL_DEPLOY_HOOKfrom GCP Secret Manager (the runtime SA can access it) and fired it β the intended git-integrated production deploy. Secrets never printed; the fetched API token was shredded after use. - Outcome/verdict: production advanced
799f7e25βe7d1985b(deploymentdpl_2JqBCDQtXQagi96mAXa2MHUjJEmc, READY). Verified064a5b5cis an ancestor ofe7d1985bβ the fix is in the live build. Post-deploy prod scan: 31/31 invoices canonical, 0 legacy-only drift. Fix correctness was already proven deterministically (transform repro on the real payload + 2 regression tests + full suite 195 green). Residual: a live UI edit isn't drivable from a cloud agent (no app session) β the next organic invoice edit will confirm; I'll spot-check it. - Note β this deploy shipped the whole
maintip, i.e. the fix PLUS ~29 other agents' pending commits (T-165 waves D/E, T-041, T-162, β¦) that had accumulated since the prior production build. Owner was advised of this before ordering the deploy. - Blast radius: unchanged from (i) β
buildInvoiceWritePayloadpayTo emission; adjacent to thedcd1b631/14bd0401invoice-shape line of work. I-032 remainsfixed(task: T-164).
2026-07-10 (iii) β deployment check: fix NOT live yet (still doing)¶
- β
Attestation (Projects (Infrastructure)): read
AGENTS.md(incl. the strict one-off deploy policy β deploy only on the owner's explicit, per-command order; a status check is not one). - Source: Projects (Infrastructure) Β· https://claude.ai/code/session_01Q2xAAEtBSMfLUGjP1HAA5D
- Checked via the Vercel connector (read): live production deployment
dpl_B1imQ5PTddwyC47vCRoH6LxScp8D= commit799f7e25(PR #883, 2026-07-10 00:12 HKT). My fix064a5b5c(2026-07-10 11:00 HKT) is not in it β799f7e25is an ancestor of the fix;mainis 30 commits ahead of the live build, and the fix is among the undeployed commits. Gate 1 (deploy) unmet. - Data re-verified (already prod-live): 31/31 tebs-erl invoices canonical, 0 legacy-only drift β the normalization holds. Newest invoice edit is 2026-07-03, so there is no post-deploy edit yet to live-verify the writer against (Gate 2 pending Gate 1).
- Stays
doing. Flip todoneafter: (1) owner deploysmainto Vercel (note: that ships the fix + the other 29 pending commits), then (2) an invoice edit on the new build keepsdetail.payment.payTocanonical (I read it back / re-scan). No deploy triggered β awaiting an explicit one-off order.
2026-07-10 (ii) β status corrected done β doing (premature close)¶
- β
Attestation (Projects (Infrastructure)): read
AGENTS.md(close-the-loop-on-donerule requires a real verification; a write-path fix isn't verified until it runs deployed). - Source: Projects (Infrastructure) Β· https://claude.ai/code/session_01Q2xAAEtBSMfLUGjP1HAA5D
- Why: the owner questioned whether
donewas earned. It wasn't: the fix is merged + code-verified, but the defect is a production write behavior; nothing here proves the deployed writer keeps payTo canonical on a real edit. Board precedent supports the stricter bar for behavior changes (cf. T-145 "merged, deployed, PROD-VERIFIED"; T-092 "code done; pending deploy") over the merge-only close some tasks use (T-039/T-050 "done, no deploy"). - Flip-to-
donecriteria: (1)maindeployed to Vercel (manual β π‘π‘; owner or a creds-bearing session), then (2) live-verify β edit an invoice on the deployed build and confirmdetail.payment.payTostays set (I can read it back in Firestore, or re-scan for 0 legacy-only). The data normalization is already prod-live and needs no deploy.
2026-07-10 (i) β fix shipped (owner-directed)¶
- β
Attestation (Projects (Infrastructure)): read
AGENTS.md; scope-scanned β this is the deliberate T-NNN escalation I-032 called for; no dup. - Source: Projects (Infrastructure) Β· https://claude.ai/code/session_01Q2xAAEtBSMfLUGjP1HAA5D
- Proposed + approved: the owner ("Proceed please", 2026-07-10) authorised implementing the I-032 fix (write-path + data normalization + regression test).
- Outcome/verdict: landed as planned β writer emits canonical payTo, 2 drifted docs normalized, tests green. Verified per above. Commit: see the commit carrying this entry.
- Blast radius (for other agents): touches
lib/projectInvoices.server.ts:buildInvoiceWritePayloadpayTo emission β the invoice write core, adjacent to thedcd1b631/14bd0401invoice-shape line of work. No change totransformPayloadToNestedShapeitself or to the client writer. Anyone editing invoice payTo handling should read I-032's origin trace first.