/** * checkout-flow.ts, the purchase, start to finish, with nothing merchant- * specific in it. * * ══ The order, and why it is exactly this order ═══════════════════════════ * * 0 GATES enabled, card, address, owner-direct, leader * 0b TAINT the purchase must be owner-initiated; the item and any * stated limit must be the owner's. The MERCHANT may be one * the owner found while browsing, see below. * 0c LINK the checkout url must resolve to a registrable domain * 0d RECOURSE who takes the card, and what silence will mean * 1 EXTRACT page strings → integers WE parsed * 2 CART what is in it is what the owner asked for, nothing added * 3 RECURRING a subscription is refused outright * 4 DECIDE budget, ceiling, overage pool, shipping ladder * 5 RESERVE money is held before any window opens * 6 NOTICE + WINDOW ONE message, sent once, with the final total * 7 APPLY SHIPPING the tier the ladder chose * 8 FILL the daemon types the card * 9 SUBMIT journalled BEFORE the click * 10 RECORD the audit ledger, and the budget is committed * * Extraction comes before the cart check because the check compares parsed * lines; the cart check comes before the decision because a cart containing * something the owner did not ask for should never reach a budget question; and the * reservation comes before the window because a window is minutes wide and two * purchases decided in the same minute would otherwise each fit what remains * and together exceed it. * * ══ The merchant the owner named, and the merchant class ═════════════════ * * The taint gate applies exactly as written: the merchant, the checkout url, the item * and any stated limit come from the owner or the purchase is refused. The owner * names the merchant, or there is no purchase. Buying "the cheapest X you can find * online" is therefore refused, and taint-gate.ts documents that as a designed * consequence rather than a gap. * * What the merchant CLASS does here is separate and strictly narrower: it can * make an in-budget purchase stricter. A merchant with no established recourse * turns a veto into an approval, so silence stops it instead of allowing it. * It never moves in the other direction. * * ══ ONE notification, and the merchant decides what silence means ═════════ * * Design rule: "show it to me" and "alert me if it is not a major retailer" collapse * into a single step. There is one message, sent once, when the item is chosen and * the final total is known, before payment. Both modes carry the same content; * the merchant only changes the RULE: * * recourse established, within budget ⇒ VETO. Silence PROCEEDS. * anything else ⇒ APPROVAL. Silence DENIES. * * They compose in the strict direction only, `windowForPurchase` escalates and * never downgrades, so a recognised retailer buys no leniency on an over-budget * purchase. The two window state machines in windows.ts stay separate, because * their silence rules are opposite and must never be unified; this module calls * whichever applies and never a shared helper. * * ══ Nothing here knows a merchant ═════════════════════════════════════════ * * No selector, no host, no page shape, no label vocabulary. The reading arrives * as structured values from whoever read the page, and every page operation * goes through six port methods whose arguments are opaque targets the caller * supplied. A merchant this has never seen works or nothing does. */ import { type RequestedLine } from './cart.js'; import { type GateInput } from './gates.js'; import { type RawCheckoutReading } from './checkout-extraction.js'; import { type MerchantJudgePort, type MerchantPolicy } from './merchant-recourse.js'; import type { MarketplaceListing } from './marketplace-listing.js'; import { BudgetLedger, type BudgetLimits } from './budget.js'; import { type CardFieldTarget } from './fill-card.js'; import { type AddressFieldTarget, type AddressStore } from './address.js'; import type { CardMaterialStore } from './card-material.js'; import type { CardMaterialRedactor } from './card-redaction.js'; import { type CheckoutChallenge, type CheckoutPageDriver } from './checkout-page.js'; import type { PurchaseRecord } from './purchase-record.js'; import type { CheckoutRegistry } from './checkout-registry.js'; import type { UntrustedContentLedger } from '../security/untrusted-content.js'; import type { CurrencyCode, OwnerSuppliedText, RefusalCode, ShippingTier } from './types.js'; /** What the owner asked for, in their own words, from an owner-direct turn. */ export interface PurchaseRequest { readonly purchaseId: string; readonly merchantDomain: string; readonly checkoutUrl: string; readonly item: OwnerSuppliedText; readonly requestedLines: readonly RequestedLine[]; readonly cardId: string; /** * Whether the owner NAMED this storefront or we found it while browsing. * * `false` puts the merchant and the checkout url through the taint check, the owner * named them, so they have to be the owner's. `true` skips that check by design and * hands the domain to the judge instead, which is the safeguard for a request phrased * like: "alert me prior to purchasing if it is not a major retailer". */ readonly merchantDiscovered?: boolean | undefined; readonly preferredTier: ShippingTier; /** A limit the owner stated in the request, if any. Taint-checked, never page text. */ readonly requestedMax?: string | undefined; /** * Where the item was browsed, when the checkout is on a different host. * * A checkout that leaves the recourse-bearing domain breaks the * qualification, and the notification says so rather than staying silent. */ readonly storefrontHost?: string | undefined; /** Page-derived, carried for the audit record. Never used to infer majorness. */ readonly sellerIdentity?: string | undefined; readonly saleType?: 'first-party' | 'third-party' | 'unknown' | undefined; /** * The specific listing, on marketplaces where recourse is per-seller. * * eBay's ruled behavior: Buy It Now only, and a seller-side selling record. * An auction is refused structurally, there is no final total before it ends, * so the "show the total, then wait" flow cannot run at all. */ readonly listing?: MarketplaceListing | undefined; } export type { PurchaseRecord } from './purchase-record.js'; export type { PurchaseLedger, PaymentNotifier } from './payment-ports.js'; import type { PurchaseLedger, PaymentNotifier } from './payment-ports.js'; export interface CheckoutFlowDeps { readonly registry: CheckoutRegistry; readonly cards: CardMaterialStore; /** * The stored shipping and billing addresses. * * The address on the order comes from here, never from the model's memory of * what the owner said and never from anything on the page. */ readonly addresses: AddressStore; readonly redactor: CardMaterialRedactor; readonly driver: CheckoutPageDriver; readonly ledger: BudgetLedger; readonly purchases: PurchaseLedger; readonly notifier: PaymentNotifier; readonly untrusted: UntrustedContentLedger; readonly limits: BudgetLimits; readonly budgetCurrency: CurrencyCode; readonly timezone: string; readonly gates: GateInput; readonly approvalMinutes: number; readonly vetoMinutes: number; readonly now: () => number; /** * The owner's additions and removals to the recognised-retailer list, and the * per-listing bar for marketplaces that carry one. * * Curated data, read at decision time. The judgement about what counts as * recourse is exercised when the list is EDITED, never by a model looking at * a page, a storefront built to look trustworthy is the easiest thing in the * world to produce. */ /** Judges the merchant's recourse from its validated domain alone. */ readonly merchantJudge: MerchantJudgePort; /** Owner-authored overrides from daemon config. */ readonly merchantPolicy?: MerchantPolicy | undefined; } /** * Where the card fields are, and where the delivery and submit controls are. * * Supplied by the caller for the page it is actually looking at. This module * has no opinion about any of them, which is what lets it work on a merchant * nobody has written anything about. */ export interface CheckoutControls { readonly cardFields: readonly CardFieldTarget[]; /** * Where each address field goes on this checkout. * * Empty when the checkout asks for no address, a digital order, or a page * that already has one on file. Every KIND named here must be stored in full * or the purchase refuses. */ readonly addressFields?: readonly AddressFieldTarget[] | undefined; /** Target for each delivery option, in the same order as the reading's list. */ readonly shippingTargets: readonly string[]; readonly placeOrderTarget: string; readonly expirySeparator?: string | undefined; readonly twoDigitYear?: boolean | undefined; } /** * Refusals this layer can produce that the decision layer has no code for. * * Kept here rather than added to `RefusalCode` because they are execution-layer * facts: a listing format we will not buy, and an approval window that closed * for a reason that had nothing to do with the budget. Folding them into the * budget codes would make `payments.purchases.list` report "over budget" for a * purchase that was well within it and simply happened at a merchant the owner had not * vouched for. */ export type ExecutionRefusalCode = 'extraction-failed' /** An auction, a Best Offer, or a format we could not confirm as fixed-price. */ | 'listing-not-purchasable' | 'merchant-not-recognised-denied' | 'merchant-not-recognised-expired' | 'merchant-not-recognised-undeliverable'; export type CheckoutOutcome = { readonly kind: 'refused'; readonly code: RefusalCode | ExecutionRefusalCode; readonly reason: string; } | { readonly kind: 'cancelled'; readonly reason: string; readonly report: string; } | { readonly kind: 'challenge'; readonly challenge: CheckoutChallenge; readonly reason: string; } | { readonly kind: 'purchased'; readonly record: PurchaseRecord; readonly orderId: string | null; }; /** * Run a purchase. * * Every exit either charges nothing and says why, or charges once and records * it. There is no path that submits without a journal entry written first, and * no path that retries a submit. */ export declare function runCheckout(request: PurchaseRequest, reading: RawCheckoutReading, controls: CheckoutControls, deps: CheckoutFlowDeps): Promise; /** The merchant identity for a url, computed by us. Null when it has none. */ export declare function merchantIdentity(url: string): string | null; //# sourceMappingURL=checkout-flow.d.ts.map