docs(adr): ADR-005 — a partial dispense distributes once, when the outcome is final

Decision 1's partial row said "operator confirms → distribute scaled"
and left the undispensed remainder's fate implicit. Made explicit:
partial_pending holds everything — including the share of the notes
that did dispense — until the operator records how the shortfall was
resolved (remediated → full amount; vouchered or written off → scaled),
then one distribution runs at that amount with the existing scaling
arithmetic. A vouchered remainder waits for redemption or expiry.

The alternative (scaled part now, remainder on resolution) is recorded
as deferred, not rejected: it needs a second additive distribution pass
the repo lacks, and a partial is almost always a terminal fault that
has latched cash-out off, so resolution is hours. Decided 2026-10-10.

Also carries the hold-invoice decision rule that fell out of the same
analysis: dispensed > 0 → settle, dispensed == 0 → cancel. An exit jam
that reports zero cancels cleanly — the customer is charged nothing and
the stuck note is the operator's to recover — so hold invoices remove
owed-cash for full faults and exit jams, not for true partials.
This commit is contained in:
Padreug 2026-10-10 21:06:08 +02:00
commit 1a24bfdda8

View file

@ -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.