/** * Stripe webhook handling (GH #1059). * * This is where money becomes a tier. Stripe delivers an event, and * `tierStore.set()` is called for the subject that paid. * * ## Why this reads the subscription back instead of trusting the event * * Stripe retries a webhook until it gets a 2xx, and it does not promise * delivery order. The usual fix is a table of processed event ids plus a * version comparison, so a replayed or late event cannot undo a newer one. * * This takes a different route. On any handled event it reads the * **current** subscription from Stripe and writes the tier that matches it. * The consequences: * * - A duplicate delivery writes the same value twice. Idempotent by * construction, with no event-id bookkeeping. * - A late `subscription.deleted` that arrives after a newer * `subscription.updated` reads a live subscription and writes the paid tier. * It cannot strand a paying customer on `free`, which is the failure the * ordering guard exists to prevent. * - No table, so no migration. That matters here specifically: production runs * migrations only inside the manual `Deploy Production` dispatch while * Railway deploys code on every push to `main`, so a table and its writer in * one change would work on staging and fail on production. * * The cost is one extra Stripe API call per event. At the volume a $9 to $39 * product generates that is not a consideration. */ import type Stripe from "stripe"; import type { TierStore } from "./tierStore.js"; import { type StripeConfig } from "./stripe.js"; export type WebhookOutcome = /** Event type we do not act on. Acknowledged so Stripe stops retrying. */ { status: "ignored"; reason: string; } /** A tier was written. */ | { status: "applied"; subject: string; tier: string; subscriptionId: string; } /** Something is wrong with the event itself. Retrying will not fix it. */ | { status: "rejected"; reason: string; } /** A transient failure. The caller returns 5xx so Stripe retries. */ | { status: "failed"; reason: string; }; export interface HandleWebhookDeps { event: Stripe.Event; config: StripeConfig; tierStore: TierStore; /** Reads the current subscription. Injected so tests need no network. */ retrieveSubscription: (id: string) => Promise; } export declare function handleStripeWebhookEvent(deps: HandleWebhookDeps): Promise; //# sourceMappingURL=webhook.d.ts.map