diff --git a/packages/lnbits/src/client.ts b/packages/lnbits/src/client.ts index ad212a8..ece7bf3 100644 --- a/packages/lnbits/src/client.ts +++ b/packages/lnbits/src/client.ts @@ -48,6 +48,8 @@ import type { CreateWithdrawResult, LnbitsWithdrawLink, UniqueHashesResponse, + DispenseReportBody, + DispenseReportAck, } from './types.js' 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 { + return this.idempotent(() => this.sendRpc('report_dispense', { body })) + } + // ============================================================================ // Invoices // ============================================================================ diff --git a/packages/lnbits/src/index.ts b/packages/lnbits/src/index.ts index e1e87c1..c37d73f 100644 --- a/packages/lnbits/src/index.ts +++ b/packages/lnbits/src/index.ts @@ -80,4 +80,8 @@ export type { UniqueHashEntry, UniqueHashesResponse, LnbitsPayLink, + DispenseReportBody, + DispenseReportAck, + DispenseReportBill, + DispenseReportCassette, } from './types.js' diff --git a/packages/lnbits/src/types.ts b/packages/lnbits/src/types.ts index fdebb95..135e0a8 100644 --- a/packages/lnbits/src/types.ts +++ b/packages/lnbits/src/types.ts @@ -310,3 +310,66 @@ export interface MachineConfigResponse { /** Freshness watermark (unix s) for the consumer's fee-config replay guard. */ 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 +}