Skip to content

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)

  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, stores peekedRef, and threads it into buildPreview({ referenceNumber: peekedRef }) (in the useMemo deps). The -β€’β€’β€’ placeholder only shows on a genuine peek failure.
  2. Guard against minting two WOPCs with the same ref (Firestore/server). generateNextWOPCNumber was a non-atomic read-then-set of the year counter β€” two concurrent createWOPC calls could both read N and both write N+1, minting a duplicate ERL-WOPC/YYYY-NNN. DONE β€” the counter read-modify-write is now wrapped in counterDb.runTransaction (Firestore optimistic concurrency retries the loser), and createWOPC writes 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")

  1. 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 PaymentConfirmation by reading FLAT fields (wopc.referenceNumber, wopc.contractorName, wopc.notes, …) off the raw doc β€” all undefined on today's bucketed docs β€” and never derived purpose, so every WOPC defaulted to contractor_fee. DONE β€” new shared mapper lib/paymentConfirmation/fromStoredWopc.ts flattens via toView and derives purpose (same rule as the Records list serializer); both thumbnail endpoints now use it and pass purpose.
  2. 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 an overflow:hidden body β†’ only page 1 visible. DONE (endpoint) β€” switched to zoom (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)

  1. 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)
  2. 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:1485 passes purpose="reimbursement"; every other renderer defaulted to contractor_fee. The PaymentConfirmation component takes purpose, closingMode, issuedByName/Title as props.
  • getWOPCByReferenceNumber returns the RAW bucketed doc (WOPC.refNumber, contractor.name, bank.*, payee.*, WOPC.notes, WOPC.totalAmount). toView (lib/wopcDocShape) normalizes it; pdf.ts already uses toView. 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 starting reimbursement-.
  • The modal shrinks its preview with zoom (not transform: scale) precisely so every page scrolls in with no clipping β€” the same reason the thumbnail endpoint now uses zoom.

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 in runTransaction (the max-sequence scan stays outside as a drift floor); createWOPC writes 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 shared lib/paymentConfirmation/fromStoredWopc.ts flattens a stored (bucketed) WOPC via toView and derives purpose; both thumbnail endpoints (Records ref-keyed + tx-keyed) now use it and pass purpose, 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. New components/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.