Skip to content

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.

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.