/** * decide.ts, the decision order, as one pure function over a snapshot. * * Pure and injectable on purpose: this is the part that must be exercised * exhaustively, and a version that needed a browser, a card and a wall clock * would be tested thinly and then trusted anyway. * * ── The order (docs/payments.md §6) ─────────────────────────────────────── * * 0. GATES, terminal, no approval path, no downgrade path * enabled / card / address / owner request * TAINT: intent derived from untrusted content → REFUSE * LINK: checkout url from untrusted content → validate or REFUSE * currency mismatch, recurring charge → REFUSE * * 1. ITEM PRICE vs DAILY ITEM BUDGET (+ per-purchase ceiling) * over → ABOVE BUDGET: explicit approval required * undeliverable → REFUSE ; silence → DENIED * within → continue * * 2. UNAVOIDABLE + PREFERRED SHIPPING vs OVERAGE POOL * exceeds → LADDER: step down one tier at a time * fits at a lower rung → record the step-down, continue * nothing fits → tolerance pool, else REFUSE * * 3. RESERVE * 4. WITHIN BUDGET → VETO WINDOW (silence PROCEEDS) * 5. PAY, with challenge pauses * 6. COMMIT, write the audit record * * The ladder is ALWAYS attempted before an overage refusal. Owner's words: * * "if the notification can't be delivered, under/at budget items get through * while over budget items do not. however, if it is over budget due to * busting the overage budget, attempt to downgrade things like shipping. if * no downgrade is possible, the overbudget item does not go though." */ import type { BudgetLimits, PoolSnapshot } from './budget.js'; import { type ShippingLadderResult } from './shipping.js'; import type { CurrencyCode, MinorUnits, RefusalCode, ShippingOption, ShippingTier } from './types.js'; /** What the checkout quoted, once parsed into integers we trust. */ export interface QuotedTotals { readonly itemMinorUnits: MinorUnits; readonly taxMinorUnits: MinorUnits; /** Mandatory handling or booking fees only. Never a discretionary add-on. */ readonly mandatoryFeesMinorUnits: MinorUnits; readonly currency: CurrencyCode; readonly shippingOptions: readonly ShippingOption[]; } export interface DecisionInput { readonly quoted: QuotedTotals; readonly limits: BudgetLimits; readonly pools: PoolSnapshot; readonly budgetCurrency: CurrencyCode; readonly preferredTier: ShippingTier; } export type DecisionOutcome = { readonly kind: 'refuse'; readonly code: RefusalCode; readonly reason: string; } | { readonly kind: 'needs-approval'; readonly draw: BudgetDraw; readonly shipping: ShippingLadderResult; readonly reason: string; } | { readonly kind: 'within-budget'; readonly draw: BudgetDraw; readonly shipping: ShippingLadderResult; }; export interface BudgetDraw { readonly itemMinorUnits: MinorUnits; readonly overageMinorUnits: MinorUnits; readonly toleranceMinorUnits: MinorUnits; readonly totalMinorUnits: MinorUnits; } /** * Steps 1 and 2 of the order, over an already-gated purchase. * * Gate 0 lives in its own modules (taint-gate.ts, link validation, cart.ts) * because each is a hard refusal with its own evidence and its own message; by * the time control reaches here those have all passed. */ export declare function decidePurchase(input: DecisionInput): DecisionOutcome; //# sourceMappingURL=decide.d.ts.map