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:
parent
fa4858ed66
commit
1a24bfdda8
1 changed files with 31 additions and 6 deletions
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue