uid: T-185 title: Can't-pay-twice marker on payable documents status: doing area: accounting created: 2026-07-25 updated: 2026-07-31 owner: girafeev1 related: T-178
T-185 β A document that has been paid cannot be paid again¶
Why¶
From T-178's phase 7. Nothing today stops a WOPC (or, once T-186 lands, a vendor invoice) being paid a second time β the unpaid-WOPC picker and the "Pay withβ¦" entry point (T-184) would both still offer it.
Scope¶
- Mark a payable document as paid at the point the payment is linked, and exclude it from every picker that offers payable documents.
- Cover all the routes that can link a payment, not just the newest one: manual match, the WOPC auto-matcher, and "Pay withβ¦".
- Decide the behaviour on unlink β undoing a match must release the document again, or an honest correction becomes impossible.
Note on the related reimbursement marker¶
Distinct from the reimbursement payout marker settled in T-178 on 2026-07-25, which records
which receipts a payout settled by reusing the transaction's existing root receiptIds. That one is
about a transaction not reading as unmatched; this one is about a document not being paid twice.
Source¶
Split out of T-178's phase 7 on 2026-07-25 at the owner's request ("Split Phase 7 in T-178"). Phase 7 was a catch-all of three unrelated items; each now stands alone.
Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
Wrap-up (2026-07-31) β building the general marker¶
Owner approved building the general "paid" marker during the T-198 wrap-up. Today only piecemeal guards
exist: the unpaid-picker exclusion (pages/api/accounting/wopc/unpaid.ts), the reimbursement-receipt lock
(components/accounting/ReimbursementsTab.tsx, disabled: !!r.wopcRef), and the settle sign-gate
(pages/api/receipts/[id]/settle.ts). Scope: one stored marker set at link/pay time, honoured across
manual match + the WOPC auto-matcher + Pay-with (T-184, when built), with a defined unlink/release. Status
todo β doing. Build notes + SHAs below.
Layer 1 (SHIPPED) β centralized "paid" predicate¶
isWopcPaid(view) added to lib/wopcDocShape.ts; the two hand-rolled copies of the check
(pages/api/accounting/wopc/unpaid.ts, lib/accounting/wopcAutoMatch.server.ts) now call it, so the
unpaid-picker and the auto-matcher agree on "paid". Covers the fee routes (manual + auto + pick-existing β
all reach status === 'active'); release is automatic via the existing delete/void/uncategorize
cascade. No Firestore structure change. tsc + tests green.
Layer 2 (PENDING owner schema sign-off) β the single stored marker¶
The general "payment-fired" marker the task envisions is a stored WOPC.paidAt?: Timestamp (optional
paidVia) on the WOPC doc β a Firestore structure change, so it hits the AGENTS.md structure gate and
needs explicit owner approval on the field shape before it is written. It is the only thing that closes the
reimbursement gap: a reimbursement WOPC is born pending_transaction and is settled via the receipt +
the 2110 tx WITHOUT ever flipping WOPC status, so the status/tx-based isWopcPaid can't see it β it still
reads as unpaid and would leak into the fee unpaid-picker. Set points when approved: createWOPC
(born-with-tx), updateWOPCMatched, reimbursements/issue.ts. Release: the delete/void cascade +
reimbursement unsettle (FieldValue.delete()). Awaiting sign-off.
Reframed into T-199 (2026-08-01)¶
Owner engaged with the structural roots and proposed a fuller redesign: move the reimbursement payout
transaction.id onto the WOPC, and replace the vague active/pending_transaction status with an honest
lifecycle (standBy β awaitingSignature β processing β settled, error, voided) that a reimbursement
can actually set. This can't-pay-twice guarantee becomes Phase 1 (honest status) + Phase 3 (the
idempotency interlock behind sign-to-pay) of that design. Layer 1 (isWopcPaid) already shipped stands
as the seam. See T-199 for the full design + phased plan (red-pen pending). The standalone
"paidAt/paidVia marker" idea is dropped β the owner rejected redundant fields in favour of the honest
status.