Records WOPC preview — Drive embed leak + transaction-linked payee/people broken
Renumbered T-126 → T-128 (2026-06-25, renumber-on-merge). While this task was in flight on
claude/friendly-newton-bLnSw, a parallel agent (Accounting (Diagnostics)) grabbed T-126 on main first with the IR56M audit endpoint helper-sharing fix. Per the AGENTS.md "whoever merges second renumbers" rule, this task bounced to the next-free T-128; the in-codeT-126references in the commit messages and README log were updated for consistency. Source commit message stays historicalT-126.
Why (owner, 2026-06-25)¶
"The WOPC tab on the Records page is showing a list of WOPCs, but clicking open the WOPC detail modal, the small preview window shows: EgxavxxoBveNmSdF78Cx
Also, when I click on a WOPC thru a transaction (20hjEVHBVk3bm53eSjqu) that's matched to 5050, in which the WOPC got created from, the WOPC is unable to show the payee's information, and regarding people's information, it's only the closing director that got rendered"
Two distinct bugs in one report.
Diagnosis¶
#1 — Records WOPC detail-drawer iframe shows a raw Drive file ID¶
WopcDetailDrawer in components/records/RecordsApp.tsx:495-503 built
the iframe src differently per WOPC state:
if (showingVoided && row.voidedDriveFileId) {
return `https://drive.google.com/file/d/${row.voidedDriveFileId}/preview`
}
if (showingSigned && latest?.signedDriveFileId) {
return `https://drive.google.com/file/d/${latest.signedDriveFileId}/preview`
}
return `/api/records/wopcs/pdf?ref=${encodeURIComponent(row.referenceNumber)}`
For signed/voided WOPCs that branch embeds the Drive preview URL
directly. But WOPC PDFs are uploaded by the accounting service account
— the viewer's Google session never has access. Drive's response to
the iframe is its access-required chrome that prominently displays the
file ID (the "EgxavxxoBveNmSdF78Cx" the user pasted is the literal
voidedDriveFileId of one of his voided WOPCs).
The local /api/records/wopcs/pdf?ref=… endpoint ALREADY does the
latest-state pickup server-side (pages/api/records/wopcs/pdf.ts:119
voided → pages/api/records/wopcs/pdf.ts:144 signed-from-request →
fall-through render). Using that endpoint for every state is the
correct path: it stays on-domain, authenticates via the SA, and
renders identically regardless of WOPC state.
#2 — Transaction-linked WOPC preview missing payee + contractor¶
The /accounting/payment-confirmation/[transactionId]/preview client
calls /api/accounting/wopc/[referenceNumber] and pipes the doc
through mapWOPCToPaymentConfirmation, which reads flat keys:
referenceNumber: wopc.referenceNumber,
payeeAbbreviation: wopc.payeeAbbreviation,
contractorName: wopc.contractorName || wopc.payeeName,
contractorAddress: wopc.contractorAddress || {},
…
The T-080 cleanup (2026-06-24, lib/wopcDocShape.ts docstring at L23)
retired the legacy-shape fallback in the shared accessors — every
prod WOPC now lives in the canonical bucketed shape (WOPC.refNumber,
payee.name, contractor.name). The /api/accounting/wopc/<ref>
endpoint returns the raw Firestore doc straight through. Post-T-080
the flat reads all hit undefined, and the preview rendered with
empty payee + contractor + bank fields.
Only the closing director still rendered because its resolver falls
through to the PaymentConfirmation component's hardcoded default when
both payeeAbbreviation (now undefined) and closingDirectorId
(never carried by the mapper) come up empty.
What shipped¶
components/records/RecordsApp.tsx¶
WopcDetailDrawer.previewUrl now always uses
/api/records/wopcs/pdf?ref=… regardless of WOPC state. The
showingVoided / showingSigned derivations stay — the title still
shows the VOID / Signed badge.
pages/api/accounting/wopc/[referenceNumber].ts¶
The endpoint now applies toView(raw) from lib/wopcDocShape.ts and
reshapes the canonical view back to the historic flat WOPCDocument
contract via a new viewToFlatDocument helper. The wire shape the
preview client + email templates receive is unchanged in structure —
every flat key (referenceNumber, payeeAbbreviation,
contractorName, bankName, closingDirectorId, …) is populated
from the bucketed view's matching field. Soft-delete tombstone fields
that aren't in the view (deletedAt/deletedBy, a tiny pre-#664
set) pass through from the raw doc.
app/accounting/payment-confirmation/[transactionId]/preview/client.tsx¶
mapWOPCToPaymentConfirmation now also forwards closingDirectorId
when present. The PaymentConfirmation type doesn't declare the
field (it's read via a duck-typed cast in the resolver call below),
so the assignment goes through a spread + final as
PaymentConfirmation. For non-director payees with no stored
closingDirectorId, the resolver still falls through and the component's
default director wins (unchanged behaviour for that branch).
Why this and not "fix mapWOPCToPaymentConfirmation to read bucketed fields"¶
A client-side dual-read would duplicate the legacy-shape adapter the
shared wopcDocShape.ts deliberately retired in T-080. Single source
of truth wins: the API endpoint applies toView exactly the way the
records-side endpoint already does, so every WOPC consumer reads from
one accessor instead of two.
Decision log¶
2026-06-25 — T-128 opened + shipped (renumbered from T-126)¶
- ✅ Attestation (Records (Infrastructure)): read
AGENTS.md; no open task covered either symptom. T-080 retired the dual-read inwopcDocShape.ts; the two consumers that still read flat keys (/api/accounting/wopc/<ref>+ the Records drawer's Drive embed branch) weren't audited at the time. - Source: Records (Infrastructure) · https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
- Owner direction (verbatim, 2026-06-25): quoted above.
- Tests + tsc:
npx tsc --noEmitclean. Live verification left to the owner — open a signed WOPC's detail drawer on Records → expect the PDF iframe (not the Drive access-required chrome with the file ID). Open the WOPC preview via a transaction → expect the payee / contractor / bank fields to populate and the stored closing director to render (not the component default). - Blast radius: API endpoint reshape is internal (consumers already expected the flat shape; the field set is the same). The Records drawer change drops two URL branches in favour of the third, which already runs through the same Drive-SA fetch server-side. The mapper adds one optional field; the existing duck- typed cast in the closing-director resolver below still works.
Commit index (backfilled 2026-07-01, best-effort · Coaching (Diagnostic))¶
Candidate related commits, auto-backfilled from git on main: commits whose message references this task's UID or a PR number it cites. Not verified — this squash-merged history can't yield a precise per-task list, so rows tagged (mentions only) name the task in passing (may be tangential) and untagged work commits may be missing. Treat as a starting point: verify, prune tangential rows, and append any real ones per the AGENTS.md "record every related SHA" policy.
6fd542b2026-06-26 — docs(tasks): add T-128 WOPC preview fix record