/** * `checkoutFinalize` saga — runs on `payment.completed` for * bookings created through the storefront's checkout-start path. * * Steps: * 1. transition_to_confirmed — flip the booking from * `awaiting_payment` to `confirmed`, stamping `paidAt`. This * emits `booking.confirmed` which fans out to: * - legal's auto-generate-contract subscriber (if wired) * - finance's auto-generate-invoice subscriber (Phase 5) * 2. issue_invoice — explicit fallback when finance auto-generation * isn't wired. Idempotent — checks if an invoice already exists. * * Compensation: if `issue_invoice` fails after the booking is * already confirmed, we don't roll back to `awaiting_payment` — the * payment was real and the booking is real. Instead the saga leaves the * booking in `confirmed` and rethrows so operations can surface the failure. * * This is an in-process compensation saga. It is not a background job or * durable execution surface; the payment subscriber invokes it inline. */ import type { EventBus } from "@voyant-travel/core"; import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; export interface CheckoutFinalizeInput { bookingId: string; /** Optional payment metadata for audit logging. */ paymentSessionId?: string; paymentIntent?: "card" | "bank_transfer" | "hold" | "ticket_on_credit"; } /** * Optional step-lifecycle hooks the caller can wire to an observability sink. * Catalog stays neutral — it just emits the events. */ export interface CheckoutFinalizeStepRecorder { startStep(name: string): Promise | void; completeStep(name: string, output?: Record | null): Promise | void; failStep(name: string, error: unknown): Promise | void; } export interface CheckoutFinalizeDeps { db: PostgresJsDatabase; eventBus?: EventBus; /** Optional observability sink — see CheckoutFinalizeStepRecorder. */ recorder?: CheckoutFinalizeStepRecorder; /** * Confirms the booking — flips it from `awaiting_payment`/`on_hold` * to `confirmed`. Implementations should emit `booking.confirmed` * once the transaction commits so downstream subscribers fan out. */ confirmBooking: (bookingId: string) => Promise; /** * Issues the final invoice for the booking. When `convertedFromInvoiceId` * is supplied (proforma → invoice path), implementations should * preserve the linkage. Returning `null` is treated as "skipped" * (e.g. invoice already issued) and not an error. */ issueInvoice: (input: { bookingId: string; convertedFromInvoiceId?: string | null; }) => Promise<{ invoiceId: string; } | null>; /** * Look up an existing proforma for this booking so we can pass * its id into `issueInvoice` (for the conversion linkage). Return * `null` if there isn't one — the booking went through card or * inquiry rather than bank-transfer. */ findProformaForBooking?: (bookingId: string) => Promise<{ invoiceId: string; } | null>; /** * Reconcile paid `payment_sessions` for the booking against the * just-issued invoice: update each paid session's `invoice_id` * pointer and write a `payments` row so the invoice flips to paid. * * The session was created at storefront-checkout time with * `target_type: "booking"` and `invoice_id: NULL` because the * invoice didn't exist yet. Without this back-link, the invoice * permanently reads as unpaid even though the customer's money is * sitting in the paid session. * * Idempotency: implementations should skip sessions that already * have an `invoice_id` set or already have a `payment_id`. Returns * the count of newly-linked sessions for observability. */ linkPaymentToInvoice?: (input: { bookingId: string; invoiceId: string; /** Hint from the saga input — when set, prefer linking this session. */ paymentSessionId?: string; }) => Promise<{ paymentId: string | null; sessionsLinked: number; }>; } export declare const checkoutFinalizeSaga: import("@voyant-travel/core").SagaDefinition; export interface RunCheckoutFinalizeOptions { /** * For resume runs — name of the step to resume from. Steps before * this one are skipped and their outputs hydrated from * {@link RunCheckoutFinalizeOptions.seedResults}. */ skipUntil?: string; /** Step outputs from the parent run, keyed by step name. */ seedResults?: Record; } /** * Run the saga with deps seeded. Wraps `checkoutFinalizeSaga.run` * with the dependency-injection plumbing — the saga primitive * doesn't carry a "deps" concept on its own, so we pass them through * `ctx.results` keyed under `__deps`. * * Resume support: when `skipUntil` is set, the seeded `__deps` step * is added to `seedResults` automatically so the resumed step still * sees `ctx.results.__deps`. The caller doesn't need to know about * the deps-injection mechanism. */ export declare function runCheckoutFinalize(input: CheckoutFinalizeInput, deps: CheckoutFinalizeDeps, options?: RunCheckoutFinalizeOptions): Promise;