Skip to content

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)

  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.