T 198
uid: T-198 title: Reimbursement WOPC preview correctness + paged previewer + duplicate-ref guard status: doing area: accounting created: 2026-07-31 updated: 2026-07-31 owner: girafeev1 assignee: Records (Infrastructure) related: T-178, T-194, T-196, T-197
β Read AGENTS.md Β· Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD Β· board next-free was T-198 (no dup). Follows T-197 (Records WOPC tab) β same reimbursement-WOPC surface, distinct concern: preview fidelity (drawer vs modal parity), a paged previewer, and a concurrency-safe reference-number allocation guard.
Owner items (2026-07-31), grouped.
Group A β reference-number correctness (owner request #1)¶
- Reimburse-modal previewer shows the real next-free ref. The previewer must show the actual
"next free" ERL-WOPC number at preview time. DONE / verified β the modal already peeks
/api/accounting/generate-payment-confirmation-number(non-reserving) on open, storespeekedRef, and threads it intobuildPreview({ referenceNumber: peekedRef })(in theuseMemodeps). The-β’β’β’placeholder only shows on a genuine peek failure. - Guard against minting two WOPCs with the same ref (Firestore/server).
generateNextWOPCNumberwas a non-atomic read-then-set of the year counter β two concurrentcreateWOPCcalls could both read N and both write N+1, minting a duplicateERL-WOPC/YYYY-NNN. DONE β the counter read-modify-write is now wrapped incounterDb.runTransaction(Firestore optimistic concurrency retries the loser), andcreateWOPCwrites with.create()(fail-loud on a colliding docId) instead of.set()(silent overwrite). The all-payees max-sequence scan stays OUTSIDE the transaction as a drift-correcting floor.
Group B β reimbursement WOPC renders wrong in the Records drawer (owner "major problem")¶
- Drawer thumbnail rendered a reimbursement as a sub-contractor fee, with missing info. Empty Ref,
empty "Dear ,", and the contractor closing ("services rendered under project titled:
"). Root cause: the T-197 thumbnail endpoint (and its tx-keyed twin) hand-built the PaymentConfirmationby reading FLAT fields (wopc.referenceNumber,wopc.contractorName,wopc.notes, β¦) off the raw doc β allundefinedon today's bucketed docs β and never derivedpurpose, so every WOPC defaulted tocontractor_fee. DONE β new shared mapperlib/paymentConfirmation/fromStoredWopc.tsflattens viatoViewand derivespurpose(same rule as the Records list serializer); both thumbnail endpoints now use it and passpurpose. - Drawer thumbnail showed only one page of a paginated WOPC. The endpoint scaled each page with
transform: scale(.33)(leaves the full-size layout box behind each page) inside anoverflow:hiddenbody β only page 1 visible. DONE (endpoint) β switched tozoom(shrinks the layout box; Chromium-only app) so pages stack with no dead space. Drawer container must also stop clipping β see item 6.
Group C β the previewer as a paged viewer (owner requests #2/#3 of the 2026-07-31 batch)¶
- Reimburse-modal previewer scrolls through pages; owner wants
< >page navigation and the box too small to show one full page (page-size / screen-ratio). Build a reusable paged WOPC preview (one full page at a time,< >+ "Page n / N", sized to fit a full A4 page) and use it in the modal. (in progress) - Records drawer shows all pages β reuse the paged viewer (or make the thumbnail box navigable) so a multi-page reimbursement WOPC is fully viewable in the drawer, not clipped to page 1. (in progress)
Key findings (map)¶
- Only
ReimbursementsTab.tsx:1485passespurpose="reimbursement"; every other renderer defaulted tocontractor_fee. ThePaymentConfirmationcomponent takespurpose,closingMode,issuedByName/Titleas props. getWOPCByReferenceNumberreturns the RAW bucketed doc (WOPC.refNumber,contractor.name,bank.*,payee.*,WOPC.notes,WOPC.totalAmount).toView(lib/wopcDocShape) normalizes it;pdf.tsalready usestoView. The thumbnails did not.- Reimbursement derivation (shared with
pages/api/records/wopcs.ts):WOPC.notes === 'Reimbursement of out-of-pocket expenses'OR a line-item id startingreimbursement-. - The modal shrinks its preview with
zoom(nottransform: scale) precisely so every page scrolls in with no clipping β the same reason the thumbnail endpoint now useszoom.
Progress / SHAs (append-only, newest last)¶
fix(T-198): concurrency-safe WOPC reference allocationβ Group A item 2.generateNextWOPCNumber's counter read-modify-write wrapped inrunTransaction(the max-sequence scan stays outside as a drift floor);createWOPCwrites via.create()(fail-loud) not.set(). Item 1 (real peeked ref in the modal) was already live β verified. tsc + WOPC/paymentConfirmation tests green.fix(T-198): reimbursement WOPC renders correctly in the Records drawerβ Group B items 3β4. New sharedlib/paymentConfirmation/fromStoredWopc.tsflattens a stored (bucketed) WOPC viatoViewand derivespurpose; both thumbnail endpoints (Records ref-keyed + tx-keyed) now use it and passpurpose, so a reimbursement stops rendering as a sub-contractor fee (empty ref/recipient, wrong closing). tsc + tests green.feat(T-198): paged WOPC previewer (β βΆ nav, full-page sizing)β Group C items 5β6. Newcomponents/accounting/WopcPagedPreview.tsx(fit-to-contain zoom, one full A4 page at a time, page indicator) in the Reimburse modal; the Records drawer thumbnail endpoint gained an in-frame β βΆ pager (fits one page to the iframe viewport) + a larger box, so a multi-page reimbursement WOPC is no longer clipped to page 1. tsc + tests green.