uid: T-193 title: 2110 reimbursement txs must store ONLY wopcRef (T-191 references-not-copies contract) β coordination for T-178 status: done area: accounting-wopc created: 2026-07-27 updated: 2026-08-10 assignee: Records (Infrastructure) owner: girafeev1 related: T-178, T-191
T-193 β 2110 WOPC-attach must carry only the reference¶
UID note: first created as T-192 on 2026-07-27 (the 2110 handover). That slot was then taken on
mainby User Management's "Org chart" task (their T-191, renumbered to T-192 on merge), which overwrote this record. Re-homed to T-193 by Accounting (Diagnostics) 2026-07-27 to restore it β renumber-on-merge, per the board README. Content unchanged; Records (Infrastructure) already accepted the contract before the collision.
Why (handover from Accounting (Diagnostics))¶
T-191 (DONE, deployed 2026-07-27) makes a WOPC-matched transaction store only wopcRef in its
gl[code] node and resolve everything else (payee, contractor identity, address, bank details,
project info, invoice number) from the attached WOPC at render time β retiring the denormalized copies
that went stale and made the ledger render non-deterministically. Owner directive: "the storing of
WOPC ref no. on a tx to be sufficient enough β¦ look just at the WOPC for all information needed."
GL 5050 (contractor fees) is done under T-191. GL 2110 is the other WOPC-attach account, introduced by T-178 (reimbursement WOPCs, Records (Infrastructure)) β no 2110 transactions exist yet.
The contract T-178 must honor¶
When T-178 wires 2110 reimbursement payments to attach a WOPC to a transaction, the 2110 gl['2110']
node must store only wopcRef (plus genuine own-facts / allocation data) β it must NOT
denormalize the payee / contractor / bank / project copies the way 5050 did. T-191's enrichment was
written GL-agnostic (any tx carrying a wopcRef resolves from the WOPC), so 2110 rows light up for
free if they honor this.
- Reuse, don't copy: T-191 shipped the shared WOPC resolver β
getWOPCByReferenceNumber(singlecollectionGroup('WOPC.refNumber')lookup, index live ontebs-epl) andgetWOPCsByReferenceNumbers(batch). Use them. StaffReimbursementForm.tsxwritesrelatedProjectIddirectly β when wired, project linkage for a reimbursement must live on the WOPC/reimbursement doc, NOT copied onto the 2110 tx.
Records (Infrastructure)'s own findings (acknowledged pre-collision)¶
Records confirmed (2026-07-27): their current T-178 reimbursement auto-match copies
payee/payeeAbbreviation/memo onto the tx (violates this contract β make it just the reference),
and flagged a likely latent bug β the reimbursement WOPC is pre-created at issue
(pending_transaction) but the shared matcher's save-payment-confirmation step calls createWOPC,
which always mints a NEW reference, so the payout path may create a DUPLICATE WOPC instead of linking
the existing one. Fixing that is part of doing 2110 correctly. Merge-order note (both agents agree):
T-191's resolver must be live before the first 2110 payout β it is now (deployed 2026-07-27).
Source¶
Handover from Accounting (Diagnostics) at the owner's invitation to flag cross-seam work (2026-07-27); re-homed off the clobbered T-192 slot.
Log¶
2026-08-04 β UNBLOCKED (doing): structure gate passed, link-don't-duplicate BUILT¶
β Read AGENTS.md Β· checked the board by scope (no dup) Β· tracking T-193. Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
Blocker cleared: T-191's GL-agnostic WOPC-first enrichment is done and deployed (2026-07-27).
The structure-gate proposal (whole-document current-vs-after, existing fields considered) was put to the
owner as its own question and approved 2026-08-04, verbatim: "Part 2 = Option 'carry the ref' β¦
WOPC <-> tx should point both ways. While the tx.id stores wopcRef, the WOPC should also store tx.id
(id number only of course), aligning with how it currently is for the 5050 pipeline."
The approved shape (tx accounting/transactions/entries/{id}, DB tebs-erl) β the gl node of a
reimbursement payout, shown under its immediate parent:
gl:
"2110":
+ wopcRef: string β the ONLY field (this contract): the document the payout pays.
Existing GLAccountData.wopcRef β the same field 5050 rows carry,
so T-191's GL-agnostic WOPC-first display covers 2110 for free.
receiptIds?: [...] unchanged (reserved, journal-inert β NOT used for reimbursements;
receipts keep owning their own link via settlement.bankTxId)
WOPC side: no structure change β transaction.{id, date} (existing fields) written by the existing
updateWOPCMatched transition (pending_transaction β active), exactly as 5050 does.
Built (all three write paths honor the contract):
- lib/accounting/wopcAutoMatch.server.ts β the interim T-192 guard is lifted, replaced by the real
branch: assign GL 2110 with expenseMetadata: { wopcRef } ONLY β updateWOPCMatched (no
save-payment-confirmation, so createWOPC can never mint the duplicate this task flagged) β fill
each already-settled backlinked receipt's missing settlement.bankTxId (receipts stay the owner of
their own link; unsettled receipts are untouched β the sign-gate keeps its teeth).
- lib/accounting/reimbursementSettle.server.ts β manual/sign settles now write the same pair: the ref
rides the 2110 assign (or merges onto an already-2110 tx via the dotted-path wopcReferenceNumber
update), and the WOPC flips via updateWOPCMatched (non-fatal; the auto-match rescan self-heals).
A tx already naming a DIFFERENT WOPC is a conflict: first ref wins, nothing overwritten, loud warn.
- Callers thread the receipt's backlink: pages/api/receipts/[id]/settle.ts + the sign-completion hook.
The 2026-011 cleanup needs NO new script: scripts/contract-5050-wopc-copies.ts is already
GL-agnostic (it strips retired copies from EVERY gl[code] node carrying a wopcRef, with the
WOPC-must-resolve safety gate). Running it post-deploy strips the rogue settle's denormalised
payee/payeeAbbreviation off gl['2110'] of tx CSU5Mef6hsJbvosrKMDE, leaving wopcRef (+ memo,
kept by that script's convention). Run order: merge β deploy β dry-run β --live, owner-commanded.
Verified: vitest β 3 new T-193 auto-match cases + a new 9-case settle-core suite, all green;
unfiltered tsc + full suite gate in the PR. Remaining before done: deploy + a runtime payout-link
verify + the contract-script run.
2026-08-06 β MERGED (#980) Β· DEPLOYED Β· contract dry-run clean; --live awaits the owner¶
β Read AGENTS.md Β· checked the board by scope (no dup) Β· tracking T-193. Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
- #980 merged to
main(e53d956a) on the owner's "Merge now"; deployed on the owner's standalone command ("Deploy after #980 is merged") β production READY (eop-l2lkqrzd7-tebs.vercel.appβpms.theestablishers.com/eop.theestablishers.com). The link-don't-duplicate payout path is live. - Contract-script dry-run (read-only) β run per the recorded order, results exact:
contract-5050-wopc-copies.tsfinds 2 candidates, would strip 4 fields, 0 skipped βCSU5Mef6hsJbvosrKMDEgl=2110 (the rogue 2026-011 settle):payee,payeeAbbreviationΒ·xTrsN4SGiPPuWLwA7oZggl=5050 (ERL-WOPC/2026-009):payee,payeeAbbreviation.wopcRefandmemostay (script convention). Both WOPCs resolve, so the stripped data is recoverable at render time via the deployed WOPC-first enrichment. - Runbook notes for whoever re-runs it here: the script must run in CJS mode β
npx ts-node --transpile-only -O '{"module":"commonjs","moduleResolution":"node"}'(the repo tsconfig'sesnextroutes ts-node into the ESM loader, where the extensionlesslib/wopc.serverimport dies) β and needsFIREBASE_ADMIN_{PROJECT_ID,CLIENT_EMAIL,PRIVATE_KEY}mapped from theGOOGLE_*values, becauselib/firebaseAdminreads only the former: without them the WOPC-resolve safety gate runs credential-less and (correctly) refuses every row as "WOPC not found". --liveNOT run β a production write to the books needs its own owner command (precedent: the 2026-07-29 backfills were each explicitly listed). On the go: re-run with--live, then the residual beforedoneis only the runtime payout-link verify (settle β tx showsgl['2110'].wopcRef, WOPC reads Reimbursed; a synced payout auto-links with no duplicate doc).
2026-08-06 β Owner: "proceed to both" Β· runtime verify GREEN Β· --live blocked by the cloud sandbox¶
β Read AGENTS.md Β· checked the board by scope (no dup) Β· tracking T-193. Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
Runtime verify (read-only, production aote-pms) β every T-193 property holds on the one live 2110 row:
- gl['2110'].wopcRef = ERL-WOPC/2026-011 on payout tx CSU5Mef6hsJbvosrKMDE, status categorized β
- The app's own resolver (getWOPCByReferenceNumber) finds the WOPC; transaction.id matches the
payout tx (bidirectional pair intact); signed; payee ("Jeffero Chan" / JC) derivable from the WOPC
for WOPC-first display β
- Exactly ONE document carries the ref β no duplicate mint β
- 15 receipts point at the payout (settlement.bankTxId) and 15 backlink the WOPC β receipts
own their own link, symmetric β
Documentation correction discovered on the way: the WOPC document lives in tebs-epl (1 doc
there, 0 in tebs-erl) β the T-178-era records repeatedly said "the subsidiary DB (tebs-erl)", which
is wrong for payees/{abbr}/wopc; it also explains why lib/firebaseAdmin's resolver client and the
WOPC.refNumber collectionGroup index sit on tebs-epl. Transactions and receipts DO live in
tebs-erl as documented.
The --live strip could not run from the cloud session: the sandbox's permission classifier blocks
the production-write invocation (the read-only dry-run is allowed; --live is not) β attempted on the
owner's "proceed to both", denied by the environment, not retried around. The one remaining command, to
run locally (or from a session with the permission granted):
FIREBASE_ADMIN_PROJECT_ID="$GOOGLE_PROJECT_ID" \
FIREBASE_ADMIN_CLIENT_EMAIL="$GOOGLE_CLIENT_EMAIL" \
FIREBASE_ADMIN_PRIVATE_KEY="$GOOGLE_PRIVATE_KEY" \
npx ts-node --transpile-only -O '{"module":"commonjs","moduleResolution":"node"}' \
scripts/contract-5050-wopc-copies.ts --live
Expected effect (from the verified dry-run): 2 rows, 4 fields β payee + payeeAbbreviation off
CSU5Mef6hsJbvosrKMDE (gl 2110) and xTrsN4SGiPPuWLwA7oZg (gl 5050); wopcRef/memo stay; render
unaffected (WOPC-first enrichment is live). T-193 stays doing until that strip lands β the write
paths honor the contract everywhere, but one legacy row still carries copies.
Addendum (2026-08-06, same session): the owner added the Bash allow rules to .claude/settings.json
on the branch (plus scripts/run-contract-wopc-copies.sh, the committed runbook wrapper; a missing
comma that had invalidated the whole settings file was repaired) β but the running cloud session's
classifier still blocked the bare allowed command. Conclusion for whoever finishes this: in-session,
this environment's auto-mode classifier does not let a project-file allow rule clear its block on this
production write. Finish with either: (a) a FRESH cloud session on this branch (settings load at
session start) instructed to run bash scripts/run-contract-wopc-copies.sh --live, or (b) the owner
locally: checkout the branch β bash scripts/run-contract-wopc-copies.sh --live (dotenv reads
.env.local; interactive approval, no rules needed). After the strip: verification dry-run reports
0 candidates β flip this task done.
- β
Read AGENTS.md Β· checked the board by scope (no dup β the T-178βT-191 seam).
- Source: Accounting (Diagnostics) Β· https://claude.ai/code/session_01G58Y71noihrYCDEDMexmea
2026-08-10 β DONE: production strip landed, verified candidates: 0¶
β Read AGENTS.md Β· checked the board by scope (no dup) Β· tracking T-193. Source: Records (Infrastructure) // T-193 Helper Β· https://claude.ai/code/session_01MEDuRaFi63jqaBdmLSWn2m
The one remaining command ran β the books are clean. The --live contract strip was run by the
owner locally; it removed the retired denormalized copies (payee, payeeAbbreviation) from the two
WOPC-linked rows, leaving only the wopcRef hook (+ memo). Independently verified from a fresh
cloud session by re-running the read-only script against production aote-pms: candidates: 0
(down from candidates: 2 pre-strip) β no row carries a retired copy anymore, and the WOPC-first
enrichment (deployed under T-191) resolves the display fields at read time, so the ledger render is
unaffected.
Both run outputs, bracketing the strip:
Pre-strip dry-run (read-only) β identical in the cloud session and the owner's local run:
[would strip] CSU5Mef6hsJbvosrKMDE gl=2110 (2): payee, payeeAbbreviation
[would strip] xTrsN4SGiPPuWLwA7oZg gl=5050 (2): payee, payeeAbbreviation
candidates: 2 | would strip: 2 | skipped (WOPC missing): 0 | fields removed: 4
--live strip (owner, local) β result confirmed by the verification below:
[strip] CSU5Mef6hsJbvosrKMDE gl=2110 (2): payee, payeeAbbreviation
[strip] xTrsN4SGiPPuWLwA7oZg gl=5050 (2): payee, payeeAbbreviation
candidates: 2 | stripped: 2 | skipped (WOPC missing): 0 | fields removed: 4
Done.
Post-strip verification dry-run (read-only, cloud) β verbatim:
Runbook correction for the local path (extends the 2026-08-06 "option (b)", which was untested): a
bare bash scripts/run-contract-wopc-copies.sh --live from a plain local shell does not work β the
wrapper maps FIREBASE_ADMIN_* from GOOGLE_* shell vars, but locally those live in .env.local, so
the mapping lands empty and lib/firebaseAdmin (which captures FIREBASE_ADMIN_* at module-load, before
the script's own dotenv.config()) gets no credentials β the WOPC safety gate correctly refuses every
row (skipped: 2, nothing written). Fix: load .env.local into the shell first, then run β
( set -a; source .env.local; set +a; bash scripts/run-contract-wopc-copies.sh --live ).
Verdict: every T-193 write path already honored the contract (built + merged under #980 /
e53d956a, deployed 2026-08-06); this strip cleared the last residue β one rogue legacy 2110 row and one
5050 row still carrying copies. T-193 is done.
- Related SHAs (append-only): e53d956a (#980 β link-don't-duplicate build + deploy) Β· 3adb590
(#984 β contract-script runbook wrapper + Bash allow-rule) Β· this docs-only close-out commit.
- Blast radius: data-only β 2 transaction rows in tebs-erl
(accounting/transactions/entries/{CSU5Mef6hsJbvosrKMDE, xTrsN4SGiPPuWLwA7oZg}), gl node, 4 fields
removed (none added β no schema-shape change); no code change in this commit. Anyone touching the
accounting-WOPC render or the 2110/5050 payout pipelines: the denormalized payee copies are now fully
retired in production β display resolves from the WOPC via the T-191 enrichment.
Wrap-up status (2026-07-31)¶
Blocked on T-191. Audited against current code during the T-198 wrap-up: the denormalization this
task guards against is not currently happening. The only path that would copy WOPC facts onto a 2110
tx (the shared matcher's save-payment-confirmation) is reached solely by 5050 contractor-fee WOPCs; the
2110 reimbursement branch in lib/accounting/wopcAutoMatch.server.ts returns early (raises a
match_suggestion, defers to manual settle). The manual settle path writes neither copies nor a wopcRef
β option 2 (2026-07-28): the payout tx stores only gl['2110']={} + status:'categorized'. So there is
nothing to correct until 2110 auto-link is re-enabled, which is gated on T-191's GL-agnostic WOPC-first
enrichment. When unblocked: link the pre-created pending_transaction WOPC via updateWOPCMatched
(not createWOPC), write only { wopcRef } on gl['2110']. Status todo β blocked.