T 198
uid: T-198 title: Reimbursement WOPC preview correctness + paged previewer + duplicate-ref guard status: done area: accounting created: 2026-07-31 updated: 2026-10-06 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.
2026-10-06 β The drawer shows the real WOPC, and opens it in its own window¶
β Read AGENTS.md. Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
Owner, verbatim: "The signed WOPC shown in the side drawer is 1. Not signed 2. And the rendering of the content is quite off from how the WOPC actually looks like β¦ I can't view them on an external window when clicked on the thumbnail, which I wish I could β¦ If a WOPC is fully processed (signed, sealed and delivered depending on whether the type of WOPC actually needed to be processed to all stages), I should be able to see the final stage of the WOPC on thumbnail and on an external window when clicked upon."
Why it was wrong. The drawer did not show the WOPC β it re-drew it from the database record, on the page (the paged previewer built under Group C). A re-draw can never show the director's signature or the company chop: those are pressed onto the archived PDF by the signing pipeline and exist nowhere else. Its fonts and layout were also the browser's approximation of the document, not the document. The drawer's own header could say "Signed" above an unsigned-looking copy. The right-click "Open in new window" had the same flaw β it opened the template preview page, another re-draw. Only Download already served the real file.
What a person sees now.
- The drawer shows the WOPC itself, at whatever stage it has reached β the signed-and-sealed copy with the signature and chop on it, the VOID-stamped copy if it was voided, or a print of the plain template if it was never signed. It is the same file Download gives you, page by page with β βΆ.
- Clicking it opens that document in its own window. A small expand mark in the corner says it is clickable; the keyboard works too.
- Right-click β "Open in new window" (one WOPC or several) now opens the same final-stage document, not the template page.
One address serves all three β /api/docs/view?kind=wopc&ref=β¦, which already picked the latest stage
for Download β so they can no longer disagree about what the document is. New:
components/records/PdfPagedThumbnail.tsx, a browser-only PDF viewer that opens on click. The
Reimburse dialog keeps its on-page re-draw (WopcPagedPreview), which is right there: it previews a WOPC
that does not exist yet.
Firestore: reads only. No document gained, lost or changed a field.
What is still not true.
- "Delivered" is not a separate stage in this app. Nothing produces a delivered copy, so a fully processed WOPC's final stage is the signed-and-sealed file (or the voided one). A "stamp-awaiting" stage exists as a filename convention, but nothing ever writes one.
- An unsigned WOPC takes a few seconds to appear in the drawer: there is no stored file, so the template is printed on the spot, exactly as for Download. Signed and voided ones come straight from Drive.
- Not driven in a browser β no app credentials in this environment. Type-checked, linted and tested.
Closing T-198. Groups AβC shipped earlier (see the list above); this is the follow-up the owner raised against Group C's drawer previewer, which it replaces.
- This entry's commit β see PR on
claude/friendly-newton-bLnSw.
Blast radius for other agents: components/records/RecordsApp.tsx (the WOPC drawer, "Open in new
window" for WOPCs) and the new PdfPagedThumbnail. No server or data change; /api/docs/view and the
signing pipeline are untouched. If you change what /api/docs/view?kind=wopc returns, you change the
drawer too β that is the point.