WOPC withdraw → reassign lets closing director be the WOPC's own payee (cross-issuance bug)
Goal (owner, 2026-06-15)¶
BUG on the Records page WOPC tab: after a user WITHDRAWS a WOPC signing-request email, they can re-select ANY available director as closing director — the picker ignores who the WOPC is FOR. The cross-issuance safeguard must hold: the closing director CANNOT be the person the WOPC was created for (a director can't sign off on their own WOPC/payment).
Notes¶
- Same financial control as T-048 R3 (directors). The enforcement exists for creation via resolveRequiredClosingDirector(payeeAbbreviation) (lib/accounting/wopcPipeline.server.ts / matchPlugins/wopcInline.tsx), but the withdraw→reassign path on the Records WOPC tab (components/records/WopcSigningRequestModal.tsx director picker) does NOT apply it.
- Fix: when the WOPC's payee IS a director, exclude that director from the picker options + validate server-side on (re)assign/send. Reuse the existing cross-issuance resolver; don't duplicate the rule.
- Verify on both the withdraw→re-request flow and a normal request for a director-payee.
Log¶
- 2026-06-15 created (owner). Backlog; unrelated to T-048 but same cross-issuance safeguard.
- 2026-07-09 — doing (Records Infrastructure; routed via EOP Local Assistance fork, owner 2026-07-09). ✅ Read AGENTS.md · checked the board by scope (no dup; T-048 R3 / T-160 share the same resolver). Source: Records (Infrastructure) · https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
- Root cause (verified): two entry points assign a director to a signing request, and neither
applied the cross-issuance rule the render layer forces. The Records "Request signature" modal
re-requests via
createOrGetRequest; the reassign endpoint hitsassignDirector. Both let ANY director be chosen — including the WOPC's own payee. - Fix: shared
assertCrossIssuanceOk(wopc, directorId)inoperations.server.ts, reusingresolveRequiredClosingDirector(payeeAbbreviation)(never duplicating the rule). When the WOPC's payee IS a director, the assignee must be the forced-opposite director; enforced server-side on bothcreateOrGetRequestandassignDirector(server is the source of truth). Non-director payee → resolver returns null → unchanged. - Client:
WopcSigningRequestModalnow takespayeeAbbreviation(threaded from the WOPC row inRecordsApp) and disables the payee-director in the picker + shows a "forced" hint, mirroring the creation picker (wopcInline). Defence-in-depth; the server guard is the actual control. - Full unfiltered
tsc --noEmitclean. PR open for review → flips todoneon merge. - 2026-07-09 — done (merged to
mainvia PR #875, no deploy — owner: commit + merge every change, don't deploy;main🟡🟡 = manual). Verdict: shipped as planned — the sharedassertCrossIssuanceOkguards BOTH assignment entry points (createOrGetRequest+assignDirector) server-side (the control), and the modal picker disables the payee-director (defence-in-depth). ReusedresolveRequiredClosingDirector; no rule duplicated. SHAs:203d2b80(fix) · this close-out · PR #875 merge commit. Blast-radius: WOPC signing-request assignment (lib/wopc/signingRequests/operations.server.ts),components/records/WopcSigningRequestModal.tsx, and the signing-modal wiring inRecordsApp.tsx— an agent touching WOPC signing assignment / reassignment should read this. Owner runtime-verify (director-payee withdraw→re-request) still pending.