Skip to content

Cancel, manual verify, and the monitor must commit one terminal invoice outcome - #563

Open
woahwhattheheck wants to merge 7 commits into
Quittance-Labs:mainfrom
woahwhattheheck:latch/quittance-558-terminal-settlement
Open

woahwhattheheck wants to merge 7 commits into
Quittance-Labs:mainfrom
woahwhattheheck:latch/quittance-558-terminal-settlement

Conversation

@woahwhattheheck

@woahwhattheheck woahwhattheheck commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #558.

One storage transition decides the terminal invoice status. Cancel, manual verify, and the Horizon monitor all call it; the loser receives a stable conflict code instead of overwriting the winner. Wallet identity stays Freighter-only — no login session.

Changes

  • backend/src/domain/invoice-settlement.ts — legal terminal transitions (PENDING→CANCELLED|PAID, late EXPIRED|CANCELLED→PAID), InvoiceTerminalConflictError, and cancel/paid guards used by both storages.
  • Memory + Postgres cancel — PENDING→CANCELLED only; loser gets INVOICE_ALREADY_PAID / INVOICE_ALREADY_CANCELLED / INVOICE_EXPIRED without clearing payment_tx_hash.
  • Cancel handler — body-only Freighter proof (sellerPublicKey + signature over cancel:<invoiceId>). Query and header seller keys are rejected even when they match the body.
  • Shared claim lock — verify and monitor already share markAsPaid / PaymentClaimError; race tests now cover monitor attribution and muxed destination claims.
  • Pay page — CANCELLED is a terminal pay state so the pay surface matches the seller workspace after a lost race.
  • shared/verification.ts — adds INVOICE_ALREADY_CANCELLED.
  • Docs — EXPIRY-AND-LATE-PAYMENT.md notes the one-commit rule; late AFTER_CANCEL settlement remains per LATE_PAYMENT_POLICY.

Done when

  • PAID cannot become CANCELLED (cancel after settle → 409 INVOICE_ALREADY_PAID, hash retained)
  • Concurrent claims of one tx hash leave one invoice PAID and the other TX_HASH_ALREADY_USED
  • Cancel that loses the race returns a stable code and does not clear payment_tx_hash
  • Pay page and seller workspace show the committed status after the race (CANCELLED terminal on pay page)
  • Wallet identity stays Freighter-only; cancel proof is not replaced with a login session

Test plan

  • node --import tsx --test tests/invoice-terminal-settlement.test.ts tests/invoice-cancel-payment-race.test.ts tests/invoice-handlers.test.ts tests/shared-verification-contract.test.ts tests/payment-attribution.test.ts tests/invoice-payment-loop.test.ts (backend) — 159 pass
  • node --test tests/payment-page-state.test.js tests/pay-terminal-guard.test.js tests/pay-page-status-matrix.test.js tests/verification.test.js (frontend, Node 24) — pass
  • npm run typecheck (backend + frontend)

Original contributor reward review

/claim #558

This is @woahwhattheheck's original contribution for issue #558. I affirmatively request review for any eligible GrantFox OSS contribution reward and confirmation of acceptance, reward allocation, payout method, and expected payment timing. The issue's “Maybe Rewarded” label does not establish any fixed amount or an awarded or received payment.

Closes Quittance-Labs#558.

Legal terminal transitions live in the settlement domain and both
storages enforce them. Cancel is PENDING to CANCELLED only; a loser
gets a stable conflict code and does not clear payment_tx_hash. Verify
and the monitor share the payment-claim lock, including muxed
destinations that resolve to the seller. Late CANCELLED/EXPIRED to
PAID settlement stays the AFTER_CANCEL / AFTER_EXPIRY path.

Cancel proof stays Freighter-only: sellerPublicKey and signature over
cancel:<invoiceId> in the JSON body. Query and header seller keys are
rejected even when they match. The pay page treats CANCELLED as
terminal so both surfaces show the status storage committed.
@CemAyyildiz

Copy link
Copy Markdown
Collaborator

This branch now has merge conflicts with main (storage changes from #577 landed). Please rebase. #580 targets the same issue (#558); only one of the two will be merged.

@woahwhattheheck

Copy link
Copy Markdown
Contributor Author

@CemAyyildiz The October 1 integration request is addressed on the existing branch. Current head a8e120035ebae89baca64c85256a19079b60b32d includes main@282c75ad933651a93b89a8412ab649b48714c739, and GitHub reports it mergeable. The original contribution and storage changes from #577 are preserved.

The committed integration record links the accepted execution evidence for integration commit 6382fb59: backend typecheck and 41 selected contract cases passed in run 37191747168. That is the retained integration result, not a new run on the later head.

One acceptance-policy question remains: #558 requires CANCELLED never to become PAID, while #507 and the existing late-payment policy allow an exact ledger payment to settle after cancellation with AFTER_CANCEL history. This branch preserves the existing late-payment behavior. Please confirm which behavior should govern and identify any concrete remaining requirement on the current head.

I maintain my original #558 contribution claim and request payment. Please confirm the acceptance decision, compensation amount and settlement route, preserving my authorship and all existing contributor claims.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cancel, manual verify, and the monitor must commit one terminal invoice outcome

2 participants