diff --git a/docs/adr/005-cash-out-dispense-outcome.md b/docs/adr/005-cash-out-dispense-outcome.md index c68a89d..e4820c5 100644 --- a/docs/adr/005-cash-out-dispense-outcome.md +++ b/docs/adr/005-cash-out-dispense-outcome.md @@ -88,10 +88,30 @@ stays there until the machine reports. | Machine reports | Settlement becomes | Then | | ----------------------------------------- | ------------------ | --------------------------------------- | | `dispense_confirmed: true` | `pending` | claim + distribute → `processed` | -| partial (some notes out, value short) | `partial_pending` | operator confirms → distribute scaled | +| partial (some notes out, value short) | `partial_pending` | nothing moves until the shortfall is resolved (below) | | `dispense_confirmed: false`, nothing out | `cash_owed` | legs never run; funds stay in wallet | | no report within `DISPENSE_REPORT_TTL` | `dispense_unreported` | worklist; operator investigates | +**A partial dispense distributes once, when the outcome is final.** Some notes reached the +customer and some did not, so the sale's true amount is not yet known: it is the full amount +if the shortfall is remediated (an on-machine `manual_dispense` against the `txid`, or an +off-machine payout recorded with `settle_cash_owed`), and the scaled amount if the shortfall +is vouchered or written off. `partial_pending` therefore holds *everything* — including the +operator's and LPs' share of the notes that did dispense — until the operator records which +of those it was. Then one distribution runs, at that amount, using the existing +`apply_partial_dispense_and_redistribute` arithmetic for the scaled case (linear scale; the +fee split by the ratio locked at landing; operator absorbs rounding). A vouchered remainder +stays undistributed until redemption or expiry, per the voucher rules under *Future +directions*. + +The alternative — distribute the scaled part immediately and the remainder on resolution — +pays the LPs the same afternoon but needs a second, additive distribution pass keyed to the +same settlement, which the repo does not have and which the completed-legs guard in the +current tool would fight. It is deferred, not rejected: a partial is almost always a +terminal-class fault that has also latched cash-out off, so the operator is coming to the +machine anyway and resolution is hours, not weeks. Revisit if prompt LP payout ever matters +more than one-distribution-per-settlement. (Decided 2026-10-10.) + This is the card-processing shape — authorize, then capture — and it is the same ordering lamassu-server enforces between `dispense_confirmed` and `updateCassettes`. The cost is that operator and DCA legs land seconds later than they do today, which is the dispense time. @@ -302,11 +322,16 @@ cancels) is identical. Two properties bound what this buys: -- **Settlement is all-or-nothing per HTLC.** A hold invoice cannot be partially settled. A - **full** fault (nothing dispensed) is cleanly cancelled. A **partial** fault — notes out, - value short — cannot be: cancelling would refund a customer who is holding cash, and - settling takes the full amount. Partial therefore still needs the voucher or refund path - below. Hold invoices eliminate owed-cash for the common full-fault case, not for every case. +- **Settlement is all-or-nothing per HTLC.** A hold invoice cannot be partially settled, so + the machine's rule is **`dispensed > 0 → settle; dispensed == 0 → cancel`**. A fault with + nothing presented cancels cleanly — and that includes an exit jam like sintra's 2026-10-09, + where the note stopped in the transport and the counter read zero: the customer is charged + nothing and the note is the operator's to recover, with `countsUncertainSince` covering the + inventory side. A **partial** — notes actually in the customer's hand — cannot cancel without + refunding someone holding cash, so it settles the full amount and from that instant is + identical to a plain-BOLT11 partial: `partial_pending`, one distribution when the shortfall + is resolved (Decision 1). Hold invoices eliminate owed-cash for full faults and exit jams, + not for true partials. - **The hold window locks the payer's funds and route liquidity**, and some wallets surface a long-pending payment as a failure. The window should equal the dispense window — seconds, capped at a minute or two — with an automatic `cancel` on timeout, never an open-ended hold.