feat(lnbits): report_dispense RPC and the dispense-report wire types (ADR-005 §2)
One cash-out's dispense outcome, sent on success as well as failure — the success report is what captures the settlement server-side. Field names follow lamassu-server's cash_out_txs / cash_out_actions (dispense_confirmed, error, error_code) with raw_code and error_class alongside, per-denomination bills with `requested`, per-bay cassettes verbatim, the payment hash as the join key, and counts_uncertain. Idempotent on txid (the server upserts), so the call is wrapped in idempotent() and safe for the machine's outbox to retry. Until spirekeeper registers the RPC it rejects with LnbitsRpcError, which the outbox treats like any other transient failure.
This commit is contained in:
parent
7f055cd4c6
commit
7120f306b6
3 changed files with 81 additions and 0 deletions
|
|
@ -48,6 +48,8 @@ import type {
|
||||||
CreateWithdrawResult,
|
CreateWithdrawResult,
|
||||||
LnbitsWithdrawLink,
|
LnbitsWithdrawLink,
|
||||||
UniqueHashesResponse,
|
UniqueHashesResponse,
|
||||||
|
DispenseReportBody,
|
||||||
|
DispenseReportAck,
|
||||||
} from './types.js'
|
} from './types.js'
|
||||||
|
|
||||||
const LNBITS_KIND_RPC = 21000
|
const LNBITS_KIND_RPC = 21000
|
||||||
|
|
@ -234,6 +236,18 @@ export class LnbitsClient {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report a cash-out's dispense outcome (ADR-005 §2). Sent on success and on
|
||||||
|
* failure; the success report is what captures the settlement server-side.
|
||||||
|
* Idempotent on `txid` (the server upserts), so it is safe to retry — and the
|
||||||
|
* caller keeps it in a durable outbox and resends until this resolves.
|
||||||
|
* Rejects with LnbitsRpcError while spirekeeper has not registered the RPC;
|
||||||
|
* the outbox treats that like any other transient failure.
|
||||||
|
*/
|
||||||
|
async reportDispense(body: DispenseReportBody): Promise<DispenseReportAck> {
|
||||||
|
return this.idempotent(() => this.sendRpc<DispenseReportAck>('report_dispense', { body }))
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Invoices
|
// Invoices
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -80,4 +80,8 @@ export type {
|
||||||
UniqueHashEntry,
|
UniqueHashEntry,
|
||||||
UniqueHashesResponse,
|
UniqueHashesResponse,
|
||||||
LnbitsPayLink,
|
LnbitsPayLink,
|
||||||
|
DispenseReportBody,
|
||||||
|
DispenseReportAck,
|
||||||
|
DispenseReportBill,
|
||||||
|
DispenseReportCassette,
|
||||||
} from './types.js'
|
} from './types.js'
|
||||||
|
|
|
||||||
|
|
@ -310,3 +310,66 @@ export interface MachineConfigResponse {
|
||||||
/** Freshness watermark (unix s) for the consumer's fee-config replay guard. */
|
/** Freshness watermark (unix s) for the consumer's fee-config replay guard. */
|
||||||
created_at: number
|
created_at: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Dispense outcome report (ADR-005 §2) — machine → spirekeeper `report_dispense`
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/** Per-denomination outcome. `requested` is what the sale asked for. */
|
||||||
|
export interface DispenseReportBill {
|
||||||
|
denomination: number
|
||||||
|
requested: number
|
||||||
|
dispensed: number
|
||||||
|
rejected: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Per-bay outcome, verbatim from the machine's cassette_bills row. */
|
||||||
|
export interface DispenseReportCassette {
|
||||||
|
position: number
|
||||||
|
denomination: number
|
||||||
|
provisioned: number
|
||||||
|
dispensed: number
|
||||||
|
rejected: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One cash-out's dispense outcome, sent on SUCCESS as well as failure — the
|
||||||
|
* success report is what captures the settlement server-side. Field names
|
||||||
|
* follow lamassu-server's cash_out_txs / cash_out_actions (dispense_confirmed,
|
||||||
|
* error, error_code) so the server's model lines up with ten years of prior
|
||||||
|
* art. Idempotent on `txid`: the machine resends until acked, the server
|
||||||
|
* upserts.
|
||||||
|
*/
|
||||||
|
export interface DispenseReportBody {
|
||||||
|
txid: string
|
||||||
|
/** Hash of the invoice the customer paid — the join key to the LNbits payment. */
|
||||||
|
payment_hash: string | null
|
||||||
|
tx_type: 'cash_out'
|
||||||
|
/** Σ(denomination × dispensed) === requested fiat value (computed on value). */
|
||||||
|
dispense_confirmed: boolean
|
||||||
|
/** Human message; null on success. */
|
||||||
|
error: string | null
|
||||||
|
/** The error's NAME, e.g. 'F56DispenseError'; null on success. */
|
||||||
|
error_code: string | null
|
||||||
|
/** Driver-native code, e.g. '78 42'; null when none. */
|
||||||
|
raw_code: string | null
|
||||||
|
error_class: 'terminal' | 'recoverable' | 'inventory' | null
|
||||||
|
fiat_cents: number
|
||||||
|
currency: string
|
||||||
|
bills: DispenseReportBill[]
|
||||||
|
cassettes: DispenseReportCassette[]
|
||||||
|
/** The machine could not vouch for its bay counts after this dispense. */
|
||||||
|
counts_uncertain: boolean
|
||||||
|
/** Set when this report closes an earlier failed txid via manual dispense. */
|
||||||
|
remediates_txid?: string
|
||||||
|
/** unix seconds the outcome was recorded on the machine */
|
||||||
|
at: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Server acknowledgement. `settlement_status` is what the server moved the settlement to. */
|
||||||
|
export interface DispenseReportAck {
|
||||||
|
txid: string
|
||||||
|
received: boolean
|
||||||
|
settlement_status?: string
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue