Skip to content

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 main by 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 (single collectionGroup('WOPC.refNumber') lookup, index live on tebs-epl) and getWOPCsByReferenceNumbers (batch). Use them.
  • StaffReimbursementForm.tsx writes relatedProjectId directly β€” 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

βœ… 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.ts finds 2 candidates, would strip 4 fields, 0 skipped β€” CSU5Mef6hsJbvosrKMDE gl=2110 (the rogue 2026-011 settle): payee, payeeAbbreviation Β· xTrsN4SGiPPuWLwA7oZg gl=5050 (ERL-WOPC/2026-009): payee, payeeAbbreviation. wopcRef and memo stay (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's esnext routes ts-node into the ESM loader, where the extensionless lib/wopc.server import dies) β€” and needs FIREBASE_ADMIN_{PROJECT_ID,CLIENT_EMAIL,PRIVATE_KEY} mapped from the GOOGLE_* values, because lib/firebaseAdmin reads only the former: without them the WOPC-resolve safety gate runs credential-less and (correctly) refuses every row as "WOPC not found".
  • --live NOT 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 before done is only the runtime payout-link verify (settle β†’ tx shows gl['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:

candidates: 0 | would strip: 0 | skipped (WOPC missing): 0 | fields removed: 0

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.