/** * payments-gateway-service.ts, the daemon side, so the capability is reachable. * * ══ What was missing ══════════════════════════════════════════════════════ * * `runCheckout` was complete and had no caller. `routes/payments.ts` declared a * `PaymentsGatewayService` interface and nothing implemented it. So every piece * worked and the daemon had no way to begin a purchase, the chain broke at the * point where a request turns into a checkout. * * This is that link: one service, constructed from the daemon's own managers, * behind the `payments.checkout.*` verbs. * * ══ Everything it needs is injected ═══════════════════════════════════════ * * No manager is reached for from inside. The card store, the address store, the * budget ledger, the notifier, the browser driver factory and the clock all * arrive through the constructor, for the reason the rest of this capability * does the same: the containment assertions have to be able to drive a whole * purchase with a sentinel card and then search every output for it, and a * service that resolved its own dependencies could only be tested against a * real daemon or not at all. * * ══ The service holds the registry, not the caller ════════════════════════ * * `begin` opens a checkout and `fillCard` completes one, and they are separate * verbs arriving as separate control-plane calls. The in-flight registry has to * outlive both, so it lives here for the life of the service rather than being * constructed per call, which is also what makes "refuse a fill with no * decision in flight" enforceable across two independent invocations. */ import { BudgetLedger, type BudgetLimits } from './budget.js'; import { type CheckoutJournal, type InterruptedVerdict } from './checkout-registry.js'; import { type WindowRecovery } from './windows.js'; import { CardMaterialRedactor } from './card-redaction.js'; import { type PurchaseLedger } from './checkout-flow.js'; import { type RawCheckoutReading } from './checkout-extraction.js'; import type { AddressStore } from './address.js'; import type { CardMaterialStore } from './card-material.js'; import type { CheckoutPageDriver } from './checkout-page.js'; import type { GateInput } from './gates.js'; import type { MerchantJudgePort } from './merchant-recourse.js'; import type { PaymentNotifier } from './checkout-flow.js'; import type { UntrustedContentLedger } from '../security/untrusted-content.js'; import type { CurrencyCode, ShippingTier } from './types.js'; /** * Ceiling on each composition-supplied I/O call the recovery sweep makes * (notifier delivery, the purchases lookup, the arming-page cleanup hook). * Ten seconds is deliberate: long enough for a channel router doing a real * network send, short enough that even a journal full of records cannot make * verb attachment hang on one wedged dependency. A call that exceeds it is * reported through the audit path and the sweep continues. */ export declare const RECOVERY_IO_TIMEOUT_MS = 10000; /** What one boot's recovery sweep did, for the composition's audit log. */ export interface CheckoutRecoverySweep { /** False when the sweep did not run at all (see `skipped`). */ readonly swept: boolean; /** Why a non-swept boot skipped: this daemon is not the payments leader. */ readonly skipped?: 'not-leader' | undefined; readonly settlements: readonly InterruptedCheckoutRecovery[]; } /** One interrupted checkout settled (or deliberately held) by boot recovery. */ export interface InterruptedCheckoutRecovery { readonly purchaseId: string; readonly merchantDomain: string; readonly verdict: InterruptedVerdict; /** * `released`: nothing was submitted, the budget hold is released and the * record closed. `held`: the record is kept, either because the submit * outcome is unknowable or because a `submitted` entry's purchase record * could not be verified on the ledger. `closed`: the order was submitted, * its purchase record was verified, and only the journal entry needed * closing. `failed`: settling this one record threw; the message carries * the error and the rest of the sweep continued. */ readonly action: 'released' | 'held' | 'closed' | 'failed'; readonly reservationReleased: boolean; readonly notified: boolean; /** * What happened with the owner's notice for this record: `delivered` on a * confirmed landing, `failed` when delivery errored, timed out, or landed * nowhere, and `already-notified` when an earlier boot's stamped notice * made a repeat unnecessary. */ readonly notice: 'delivered' | 'failed' | 'already-notified'; readonly message: string; /** * Present when the record carried a persisted delivery report and the * delivery-keyed window rules were applied; the audit record for which * rule governed and which channels it named. */ readonly windowRecovery?: WindowRecovery | undefined; /** Present only for records interrupted while arming the payment. */ readonly armingPageCleanup?: 'done' | 'failed' | 'unavailable' | undefined; } /** What a `payments.checkout.fillCard` call reports. Field names and a boolean. */ export interface PaymentFillCardResult { readonly ok: boolean; readonly filled: readonly string[]; readonly failedField: string | null; readonly reason: string | null; } /** What a `payments.checkout.begin` call reports. */ export interface PaymentBeginResult { readonly outcome: string; readonly purchaseId: string | null; readonly reason: string | null; readonly merchantOrderId: string | null; readonly totalMinorUnits: number | null; readonly currency: string | null; readonly shippingTierUsed: string | null; readonly steppedDown: boolean; /** Set when the merchant interrupted with 3-D Secure, a CAPTCHA or an OTP. */ readonly challengeStep: string | null; } export interface PaymentsServiceConfig { readonly limits: BudgetLimits; readonly budgetCurrency: CurrencyCode; readonly timezone: string; readonly preferredTier: ShippingTier; readonly approvalMinutes: number; readonly vetoMinutes: number; } export interface PaymentsServiceDeps { readonly cards: CardMaterialStore; readonly addresses: AddressStore; readonly ledger: BudgetLedger; readonly purchases: PurchaseLedger; readonly notifier: PaymentNotifier; readonly untrusted: UntrustedContentLedger; readonly journal: CheckoutJournal; /** Judges merchant recourse from the validated domain alone. */ readonly merchantJudge: MerchantJudgePort; /** Resolves the driver for an open browser session and page. */ readonly driverFor: (sessionId: string, pageId: string) => CheckoutPageDriver; /** * Composition-supplied cleanup for a page that outlived the process while a * checkout was arming the payment. Receives record identity only, never a * driver: the composition holds the browser authority and decides whether * it can still reach the page (same discipline as the checkout seam, so * payments code cannot mint browser access recovery was not given). * Resolves true when the card fields were cleared. Absent, recovery keeps * the documented posture of refusing to claim a cleanup it cannot perform. */ readonly armingPageCleanup?: ((record: { readonly purchaseId: string; readonly sessionId: string; readonly pageId: string; }) => Promise) | undefined; /** The gate inputs the daemon alone can answer, leadership most of all. */ readonly gates: () => GateInput; readonly config: () => PaymentsServiceConfig; readonly now?: (() => number) | undefined; /** * Override for `RECOVERY_IO_TIMEOUT_MS`, the per-call ceiling on the * sweep's composition I/O. Tests use it; compositions normally do not. */ readonly recoveryIoTimeoutMs?: number | undefined; /** * The redactor this service arms, supplied rather than minted. * * Absent, one is constructed here and the service is self-contained, which is * what every existing caller and every containment test does. Present, it is * the guard the browser engine was built with, and passing the SAME object is * the whole point: the engine scrubs page output against what this service * armed, and two instances would leave the engine scrubbing an empty set * while a card sat on the page. The browser-backed driver refuses rather than * types if the two ever come apart (see browser-checkout-driver.ts). */ readonly cardFieldGuard?: CardMaterialRedactor | undefined; } /** The input a `begin` call carries, already shape-checked by the route. */ export interface BeginCheckoutInput { readonly sessionId: string; readonly pageId: string; readonly merchantDomain: string; readonly checkoutUrl: string; readonly item: string; readonly cardId: string; readonly requestedLines: readonly { readonly label: string; readonly quantity: number; }[]; readonly reading: RawCheckoutReading; /** * The page controls, in the WIRE shape (`ref`), translated below. * * The wire says `ref` because that is what a snapshot calls an element; the * flow says `target` because it has no opinion about what an addressing * string is. Translating here, explicitly, rather than by casting, is what * keeps a mismatch a compile error instead of a runtime refusal that reads * like a missing address. */ readonly controls: { readonly cardFields: readonly { readonly field: string; readonly ref: string; }[]; readonly addressFields?: readonly { readonly kind: string; readonly field: string; readonly ref: string; }[] | undefined; readonly shippingTargets?: readonly string[] | undefined; readonly placeOrderTarget: string; readonly expirySeparator?: string | undefined; readonly twoDigitYear?: boolean | undefined; }; readonly preferredTier?: ShippingTier | undefined; readonly requestedMax?: string | undefined; /** True when the storefront was found while browsing rather than named. */ readonly merchantDiscovered?: boolean | undefined; } export declare class PaymentsGatewayServiceImpl { private readonly deps; private readonly registry; private readonly redactor; constructor(deps: PaymentsServiceDeps); /** * The redactor this service types cards through. * * Exposed so the daemon can hand the SAME instance to the browser engine as * its `cardFieldGuard`. They must be one object: the engine scrubs against * what this service armed, and two instances would mean the engine scrubbing * against an empty set while a card sat on the page. */ cardFieldGuard(): CardMaterialRedactor; /** * Begin and run a checkout. * * Everything the flow needs that only the daemon knows, the limits, the * timezone, the leadership answer, is read HERE, at the moment of the call, * rather than captured at construction. A budget raised five minutes ago * should apply to this purchase. */ beginCheckout(input: BeginCheckoutInput): Promise; /** * Settle every checkout a restart interrupted, before new checkouts run. * * The journal is the restart's only witness, and records live in this * process are running, not interrupted; the registry filters them out. * Each record settles by its phase verdict. Nothing submitted releases its * budget hold and closes, and the owner's message follows the actual * release result. An unknowable submit keeps its hold and its record. A * `submitted` record is closed only after the purchase record is verified * on the ledger; when it is missing, or this composition cannot look, the * record is kept and the owner is told exactly what is and is not known. * A record interrupted inside a window settles conservatively by refusal, * because delivery of the window's notice cannot be verified after a * restart; the delivery-keyed rules in `recoverInterruptedWindow` apply * once deliveries are persisted. * * A kept record notifies the owner at most once, ever: the first delivered * notice stamps `recoveryNotifiedAtMs` back through the journal. One * record's failure never abandons the rest; it is reported in the results * as `action: 'failed'`. Recovery also cannot clear card material from a * browser page that outlived the process (attach-based compositions): the * composition that owns the page owns that cleanup, including for records * interrupted in the arming phase. */ recoverInterruptedCheckouts(): Promise; private settleInterrupted; /** * The window sentence for an interrupted-window record, keyed on the * PERSISTED delivery report when one exists. The purchase itself can never * resume after a restart (the page and the in-flight call are gone), so * every branch settles with the hold released and nothing charged; what the * delivery-keyed rules decide is what the owner is told about the notice * and which channels still owe a read. */ private verifyPurchaseRecord; private settleWindowSentence; private runArmingPageCleanup; /** * Type the stored card into an open checkout. * * The refusals live in `fillCard`; this only adapts the shapes. A * `FillCardRefusal` is rethrown so the route can forward its message, it * carries no card material and it is the owner's business why the fill was * refused. */ fillCardIntoCheckout(input: { readonly sessionId: string; readonly pageId: string; readonly targets: readonly { readonly field: string; readonly ref: string; }[]; readonly expirySeparator: string | undefined; readonly twoDigitYear: boolean | undefined; }): Promise; /** Validate a reading without buying anything, so a caller can check its parse. */ previewReading(reading: RawCheckoutReading): { ok: boolean; reason: string | null; }; private refused; private describe; } //# sourceMappingURL=payments-gateway-service.d.ts.map