import Stripe from "stripe"; import type { BillingAdapter, BillingConfig, ResolvedConfig } from "./types.js"; export declare function getStripe(): Stripe; export declare function stripeConfigured(): boolean; /** * Replace the memoised client. TESTS ONLY. * * The money paths are worth asserting without a network, and the client is a * module-local by design (one per process, built lazily). This is the seam that * makes those assertions possible; nothing in the library calls it. */ export declare function __setStripeForTests(client: unknown): void; export interface StripePrice { id: string; productId: string | null; productName: string | null; lookupKey: string | null; nickname: string | null; unitAmount: number | null; currency: string; interval: string | null; intervalCount: number | null; } /** All active recurring (subscription) prices on the Stripe account. Uses the * SDK's async auto-pagination so accounts with >1 page of prices aren't * silently truncated. */ export declare function listSubscriptionPrices(): Promise; /** Resolve a single subscription price without any env config. Resolution * order: explicit `priceId` → matching `lookupKey` → the sole recurring price. * Returns null if nothing matches or the choice is ambiguous (multiple prices, * no lookupKey) — the caller can then list options via listSubscriptionPrices. * The priceId/lookupKey paths query Stripe directly (no full-list scan). */ export declare function resolveSubscriptionPrice(opts?: { priceId?: string; lookupKey?: string; }): Promise; export declare function getBillingCustomerId(adapter: BillingAdapter, orgId: string): Promise; /** Subscription state as the billing-sync recorded it, through the seam. * * All-null when the adapter has no org-metadata store to keep it in, which * reads the same as "no subscription" — the caller's fallback either way. The * point of having it here is that an app holding a `BillingAdapter` shouldn't * have to reach past it to WorkOS (and re-derive the metadata key names) to * read what the engine wrote. */ export declare function getOrgSubscription(adapter: BillingAdapter, orgId: string): Promise>>>; export declare function ensureStripeCustomer(adapter: BillingAdapter, orgId: string, email: string | undefined, config: ResolvedConfig): Promise; /** * The customer's credit balance, in `currency`. * * `customer.balance` is a SINGLE scalar, denominated in `customer.currency` — * whichever currency first touched the customer, pinned there for good. Stripe * nonetheless tracks a separate running balance per currency: credit a customer * pinned to EUR in USD and the call is accepted, the USD balance gets its own * `ending_balance`, and the scalar does not move. * * So reading the scalar for a customer whose currency isn't the one being * metered reports the WRONG balance, silently: debits land in the metered * currency while the number on screen keeps reflecting the other one. Measured, * not theorised. Passing `currency` (do — it's `config.currency`) reads the * balance for that currency instead, from the latest balance transaction in it. * * Without `currency`, or when it matches the customer's own, the scalar is * correct and is used directly — one API call, as before. */ /** * The customer object, or null if it is deleted. * * Exists so the two reads that both want it — the wallet balance and the spend * controls — can share ONE retrieve. They are issued together on the hot path of * every metered call, and measured under load `/v1/customers/:id` was the single * largest consumer of the account's request budget, because each did its own. */ export declare function retrieveBillingCustomer(stripeCustomerId: string): Promise; export declare function getCreditBalance(stripeCustomerId: string, currency?: string, /** An already-retrieved customer, to avoid a second fetch of the same object. */ prefetched?: Stripe.Customer | null): Promise; export declare function deductCredits(stripeCustomerId: string, toolName: string, cost: number, currency: string, /** Who ran it — written as balance-transaction metadata so per-seat / per- * caller cycle usage can be summed with `usageSince` (no separate ledger). */ caller?: { kind: string; id?: string; }): Promise; export declare function usageSince(stripeCustomerId: string, since: number, filter?: { callerKind?: string; callerId?: string; }): Promise; /** * `usageSince` for SEVERAL windows in one pass. * * The walk is the same whatever the window: transactions come newest-first and it * stops at the oldest `since` asked for, adding each transaction to every window * that contains it. Two windows over one caller — a monthly pack and a weekly * limit — therefore cost ONE walk rather than two, and the pages are the expensive * part: an org whose API usage the wallet funds writes a transaction per call. * * Windows share the filter, because that is how the caller-scoped reads arrive. */ export declare function usageSinceWindows(stripeCustomerId: string, windows: readonly { since: number; until?: number; }[], filter?: { callerKind?: string; callerId?: string; }): Promise; export declare function grantCredits(stripeCustomerId: string, amount: number, description: string, currency: string, /** Pass a stable key (e.g. the source invoice/session id) so replayed events * — a re-delivered webhook, an overlapping poll — credit exactly once. */ idempotencyKey?: string): Promise; /** * How a top-up is taxed and where it returns. * * Tax is an OPTION rather than a default because only the app knows where it is * established and what the customer's place of supply is — the same reason the * seat checkout takes `taxRates`. What is NOT acceptable is the previous * behaviour: a seller charging 22% IVA on its seats and 0% on a top-up bought * through the same account, on an invoice that states neither. `checkBillingSetup` * warns when a taxed account sells untaxed top-ups. */ export interface TopUpCheckoutOptions { /** Manual Stripe TaxRate ids, as `taxRatesFor` returns. */ taxRates?: string[]; /** * Collect a business tax ID (VAT number) on the form. On by default. * * Off, a business customer has no way to hand over the number that reverse-charges * the sale, so a B2B purchase is invoiced as B2C. This session issues an invoice * (`invoice_creation`), which makes it the one place that number can be captured. */ taxIdCollection?: boolean; /** Use Stripe Tax instead. Ignored when `taxRates` is given — Stripe rejects both. */ automaticTax?: boolean; /** Defaults to `${baseUrl}/billing/success?credits=…`. */ successUrl?: string; /** * Render the payment form INSIDE the app instead of sending the customer to * Stripe's hosted page. * * `hosted` (the default) returns a URL to redirect to. `embedded` returns a * client secret for a `ui_mode: "elements"` session, which * `BillingCheckoutSessionProvider` mounts — the same components the seat * checkout uses, so a top-up can show card fields in a dialog rather than * leaving the page. `returnUrl` then covers both success and cancel, because an * embedded session has one place to come back to after an off-site step. * * Embedded is NOT the default: a redirect works with no Stripe.js on the page * and no publishable key wired, and switching every existing consumer's top-up * flow silently is not a thing a minor release should do. */ uiMode?: "hosted" | "embedded"; /** Defaults to `${baseUrl}/billing/cancel`. */ cancelUrl?: string; /** * Whether the embedded form ASKS before keeping the card. * * `ask` (the default) renders Stripe's "save my payment details" checkbox and * lets the answer decide. `always` drops the checkbox and keeps the card * regardless — for an account where a saved card is the point of the purchase * rather than a favour to the buyer: auto-reload has nothing to charge without * one, and a customer who declines silently disables it. * * Consent is not skipped by `always`. The session already carries * `setup_future_usage: "off_session"`, which is what makes Stripe render the * mandate line ("by providing your card you allow … to charge it for future * payments") under the fields. The checkbox is a second, narrower question on * top of that text — removing it leaves the disclosure in place. * * `ask` stays the default deliberately: which cards a customer ends up with is * exactly the kind of behaviour a minor release must not change under an * existing consumer. */ savePaymentMethod?: "ask" | "always"; } /** What a credit purchase costs, before anyone is charged. */ export interface CreditQuote { /** Credits bought. 1 unit of currency = 100 credits, as `buy_credits` says. */ credits: number; /** Minor units, exclusive of tax. */ subtotal: number; /** Minor units of tax added on top. 0 under reverse charge, or with no rates. */ tax: number; total: number; /** Summed percentage of the EXCLUSIVE rates applied, for a "IVA (22%)" label. */ taxPercent: number; /** Every rate id this quote accounted for — the ones the charge will carry. */ taxRateIds: readonly string[]; } /** * Quote a credit purchase from the SAME Stripe TaxRate objects the charge will * carry — not from a percentage kept somewhere else. * * A dialog that says "Estimated tax €4.40" and a Checkout Session that charges * something else is the drift this library keeps designing out: pass the rate ids * you will pass to `createCreditCheckoutSession` (`topUp.taxRates(orgId)`) and the * two cannot disagree, because they are the same objects. * * INCLUSIVE rates are counted as already inside the amount, so `total` stays the * amount asked for — that is what "inclusive" means, and adding them on top would * overstate the charge. */ export declare function quoteCreditPurchase(amountMajor: number, taxRateIds?: readonly string[]): Promise; /** Forget cached TaxRate percentages — for a test, or after editing a rate. */ export declare function invalidateCreditQuotes(): void; export declare function createCreditCheckoutSession(stripeCustomerId: string, orgId: string, amountMajor: number, config: ResolvedConfig, opts?: TopUpCheckoutOptions): Promise<{ url: string | null; clientSecret: string | null; sessionId: string; }>; /** * HOW the money is collected. One purchase, four ways to pay for it. * * Every one of these existed as a different shape or not at all: `checkout` and `embedded` * were a `uiMode` on one function, the off-session charge lived only inside `tryAutoReload` * (threshold-triggered, uncallable), and the emailed invoice did not exist. So a consumer * with a browser wrote its own purchase and a caller without one had a single answer — a * link. Two implementations of one act, and only one of them had the app's settlement. * * checkout a hosted Stripe Checkout URL. Anyone can open it. The default. * embedded a client secret for Stripe.js — what an app's own Elements form mounts. * saved_card charge the default card off-session. The ONLY fully headless path: no * browser, no link, no human. Refuses `no_card`. * invoice Stripe emails a payable invoice. The path for a customer with NO card, * which is the case `saved_card` cannot bootstrap. Refuses `no_email`. */ export type PurchaseMethod = "checkout" | "embedded" | "saved_card" | "invoice"; export type PurchaseResult = { status: "checkout"; method: PurchaseMethod; credits: number; url: string | null; clientSecret: string | null; sessionId: string; } | { status: "charged"; method: "saved_card"; credits: number; invoiceId: string; } | { status: "invoiced"; method: "invoice"; credits: number; invoiceId: string; hostedInvoiceUrl: string | null; dueAt: number | null; /** Whether Stripe actually SENT the email. False means the invoice exists and is * payable at `hostedInvoiceUrl` — the account just cannot email it yet. */ emailed: boolean; } | { status: "refused"; reason: "no_card" | "no_email" | "charge_failed"; message: string; }; /** * Sell credits at a price that is not the list price. * * The one thing no other path here can do. Everywhere else `credits` IS the money — * `CREDITS_PER_UNIT = 100` makes them the same number, deliberately, so a customer typing an * amount and an agent calling `buy_credits` cannot be quoted differently. A negotiated deal * is precisely the case where they must differ: 600 000 credits for €4 000 is the whole * point of an Enterprise conversation, and until now the library could describe that plan and * not sell it. * * So the two numbers are separated HERE and nowhere else, behind an operator gate, and only * as an INVOICE: * * • The invoice item carries `amountMinor` — what they agreed to pay. * • `metadata.credits` carries the quantity — what they agreed to get. * • Paying it credits the wallet through the `invoice.paid` branch that already exists, * with the `credit:invoice:` key it already uses. * * That last point is why this is not a grant. There is no second crediting path to keep in * step with the first, an unpaid quote hands over nothing, and a refund reverses through the * same machinery as any other invoice. */ /** * What a credit invoice must ACTUALLY grant, once Stripe has taken its bite. * * Stripe applies a customer's credit balance to any invoice it finalizes, and this * library's wallet IS that balance — so an invoice for 600 000 credits at €4 200 was * settled €5 cheaper out of the 500 credits the customer was already holding. Measured on * a real account: `subtotal 420000, starting_balance -500, amount_due 419500, * ending_balance 0`. They pay less money and LOSE credits they had already bought, which * is the one outcome nobody would agree to. * * There is no per-invoice flag to refuse that — it happens at finalization. So the fix is * on the other side: grant what was sold PLUS whatever the invoice ate, which puts the * customer exactly where the deal said they would be. `starting_balance` is negative when * a credit was applied, and is in minor units, which is the same unit as a credit. * * The same arithmetic serves every invoiced purchase — a quote, a `buy_credits --method * invoice`, an auto-reload — which is why it lives here rather than at three call sites. */ export declare function creditsOwedFor(invoice: { metadata?: { credits?: string | null; } | null; starting_balance?: number | null; }): number; export declare function sellCredits(stripeCustomerId: string, orgId: string, config: ResolvedConfig, input: { /** What they get. */ credits: number; /** What they pay, in minor units of `config.currency`. Deliberately unrelated to `credits`. */ amountMinor: number; /** Shown on the invoice — the deal, in the customer's own words. */ description?: string; /** Net terms. Procurement rarely pays on receipt; 30 is the usual answer here. */ daysUntilDue?: number; /** Their PO, on the invoice rather than in an email, because that is what unblocks * payment. */ purchaseOrder?: string; /** * Resolved tax for this charge. Omitted, it is resolved from `config.tax` — an approved * quote is a real invoice and must carry the same rate, and the same mandatory mention, * as every other charge on the account. */ tax?: ChargeTax; /** Reuse an existing invoice for a retried approval rather than raising a second one. */ idempotencyKey?: string; /** * How to collect it. * * `auto` (the default) charges the CARD ON FILE and falls back to emailing an invoice * when there is none — which is the behaviour a person expects from "accept": somebody * who has already given us a card does not want a bill in their inbox, and somebody who * has not cannot be charged. `invoice` forces the bill; `saved_card` forces the charge * and refuses rather than falling back, for a caller that needs to know. */ method?: "auto" | "saved_card" | "invoice"; }): Promise<{ status: "invoiced"; invoiceId: string; hostedInvoiceUrl: string | null; dueAt: number | null; emailed: boolean; } | { status: "charged"; invoiceId: string; hostedInvoiceUrl: string | null; paid: true; } /** The card is fine and the BANK wants the cardholder. Nothing is charged yet; the hosted * invoice page is where they confirm it. */ | { status: "needs_authentication"; invoiceId: string; hostedInvoiceUrl: string | null; message: string; } | { status: "refused"; reason: "no_email" | "no_card" | "invalid_amount" | "charge_failed"; message: string; }>; /** * Buy credits, by whichever method the caller can actually complete. * * The single implementation behind `buy_credits` AND behind a consuming app's own purchase * dialog — which is the point. scartoffie had its own copy that forced the embedded mode * and bypassed the tool, so `savePaymentMethod: "always"`, the card prune and the settlement * lived on one path and not the other. * * `credits` are minor units: 1 credit = 1 cent, the same equivalence * `createCreditCheckoutSession` and `tryAutoReload` already use. */ export declare function purchaseCredits(stripeCustomerId: string, orgId: string, amountMajor: number, config: ResolvedConfig, opts?: TopUpCheckoutOptions & { method?: PurchaseMethod; /** How long an emailed invoice is payable for. Stripe requires it on `send_invoice`. */ daysUntilDue?: number; /** Resolved tax for the charge — the same rates the quote used. */ tax?: ChargeTax; }): Promise; /** * Where a portal link LANDS. Stripe calls it `flow_data`, and it is the difference between * handing someone a menu and handing them the form they need. * * It matters because this link is the library's answer to the one thing that genuinely * cannot be done headlessly: entering a card. A caller with no browser cannot confirm a * SetupIntent — but it can produce a URL, and a URL that opens on "add a payment method" * is a different quality of answer from one that opens on a dashboard the customer then * has to navigate. */ export type PortalFlow = "payment_method_update" | "subscription_cancel" | "subscription_update"; /** A Stripe Billing Portal session URL — the no-code self-serve surface where a * customer manages their subscription (upgrade/downgrade/cancel), updates the * payment method (fixes a failing card), and views invoices. * * Pass `flow` to open a specific one directly. `subscription_cancel` and * `subscription_update` need a subscription id, which is why they are only reachable when * the caller supplies one — Stripe rejects the session otherwise, and a 400 at link-creation * time is a worse failure than the menu. */ export declare function createBillingPortalSession(stripeCustomerId: string, returnUrl: string, opts?: { flow?: PortalFlow; subscriptionId?: string; }): Promise; export interface SpendControls { /** Credits allowed per calendar month, or null for no ceiling. */ limitCredits: number | null; /** Ascending credit thresholds to warn at. Empty when none. */ alertCredits: number[]; } /** Parsed from an already-fetched customer, so a caller that has one does not * pay for a second read. `spendControlsOf` is the whole parser. */ export declare function spendControlsOf(metadata: Stripe.Metadata | null | undefined): SpendControls; export declare function getSpendControls(stripeCustomerId: string, /** An already-retrieved customer, to avoid a second fetch of the same object. */ prefetched?: Stripe.Customer | null): Promise; /** * Write either field; omit one to leave it alone. * * "" is what CLEARS a Stripe metadata key, which is why null/empty map to it * rather than to "0" — a stored "0" would read back as a ceiling of zero. */ /** * Why an alert threshold cannot be accepted here, or null when it can. * * `alertCredits` is a promise: "warn me at 10 000". A deployment with no notifier wired * cannot keep it — nothing reads the number — and storing it anyway is how a billing page * comes to offer "email alerts" that never arrive. That exact defect shipped in one * consumer for months and was only found by grepping for who read the field, which is not * a way to find things. * * The CEILING is unaffected and is never refused: the meter enforces it whether or not * anybody can be told, so it is a real setting either way. This refuses the whole call * rather than accepting half of it — a caller that asked for two things and got one, with * no error, is the shape this is here to prevent. */ export declare function spendAlertRefusal(canNotify: boolean, alertCredits?: number[]): string | null; export declare function setSpendControls(stripeCustomerId: string, input: { limitCredits?: number | null; alertCredits?: number[]; }): Promise; export declare function getAutoReloadSettings(stripeCustomerId: string): Promise<{ enabled: boolean; threshold: number; reload_to: number; } | null>; export declare function setAutoReloadSettings(stripeCustomerId: string, threshold: number, reloadTo: number, enabled: boolean): Promise; /** * Recharge a customer who has dropped to their auto-reload threshold. * * Two properties this must have, both learned the hard way: * * **It bills as an INVOICE, not a bare charge.** A PaymentIntent produces a * receipt with no invoice number and no tax line. That is not a valid sales * document for a business customer (an Italian buyer needs a fattura), and it * meant the one purchase a customer never explicitly confirms was also the one * with no paperwork. An invoice also carries the same tax treatment as every * other line the account bills. * * **It is idempotent.** This is fired and forgotten from the meter on every * metered call and from the auth path, so N concurrent calls all observe the * same low balance and all used to charge. The key below collapses them: Stripe * returns the first invoice for every duplicate within its 24h window, so the * customer is charged once no matter how many callers raced. */ /** What a charge carries for tax, as `taxFor` returns it. */ export type ChargeTax = { taxRates?: string[]; automaticTax?: boolean; }; /** * Fire-and-forget auto-reload with the deployment's tax settings applied. * * Both trigger points (the meter and the auth gate) go through this, so a reload * can't be taxed on one path and untaxed on the other. * * **The tax settings are passed as a THUNK, not a value, and that is the whole * point of this function's shape.** It used to `await taxFor(...)` here and hand * the result down — so every wallet-funded metered call resolved tax for a reload * that, almost always, was not going to happen. Under `mode: "local"` that meant a * live **VIES** request plus a Stripe customer retrieve per metered call, for a * customer whose balance was nowhere near their threshold. * * VIES is a shared European Commission service, and the failure it invites is a * cascade rather than an error: get rate-limited there and every B2B customer * silently stops reverse-charging, because an unverifiable number means CHARGE. The * bill for hammering it would have arrived as "why is everyone suddenly paying VAT". */ export declare function autoReloadFor(stripeCustomerId: string, config: { currency: string; tax?: BillingConfig["tax"]; }): Promise; export declare function tryAutoReload(stripeCustomerId: string, currency: string, /** Resolved tax, or a thunk resolving it — the thunk runs only if we actually * charge, which is what keeps `taxFor` off the metered hot path. */ opts?: ChargeTax | (() => Promise)): Promise; export interface InvoiceEntry { id: string; /** `purchase` = a Stripe invoice; `auto_reload` = a bare off-session charge. */ type: "purchase" | "auto_reload"; number: string | null; /** * What to SHOW, in minor units: the amount paid once settled, the amount * still owed while open. Reading `amount_paid` alone renders every open * invoice as zero, which is the one number a customer must not be shown. */ amount: number; amount_paid: number; amount_due: number; currency: string; /** Stripe's own status: draft | open | paid | uncollectible | void. */ status: string | null; paid: boolean; created: string; /** Due date of an open invoice, ISO; null when Stripe set none. */ due_date: string | null; /** A human line for the row: the invoice description, else its first line. */ description: string | null; invoice_url: string | null; invoice_pdf: string | null; } export declare function listInvoices(stripeCustomerId: string, limit?: number): Promise; /** * One invoice (or auto-reload charge) belonging to `stripeCustomerId`. * * **The ownership check is the point.** An invoice id is guessable-adjacent and * these are exposed as a tool and a server action, so a retrieve that didn't * compare the customer would hand any caller anyone else's invoice. Not theirs * (or gone) → null, which every caller renders as "not found"; the two are * deliberately indistinguishable to the caller. */ export declare function getInvoice(stripeCustomerId: string, invoiceId: string): Promise; /** * The PDF link for one of the customer's invoices, ownership-checked. * * Stripe's `invoice_pdf` is a long-lived unauthenticated URL, so this is what a * download button opens. Null when the row has no PDF at all: a draft invoice, * or an auto-reload charge (a receipt page: use `invoice_url` for those). */ export declare function invoicePdfUrl(stripeCustomerId: string, invoiceId: string): Promise; /** The org's recent invoices, newest first. Empty when billing never started. */ export declare function listOrgInvoices(adapter: BillingAdapter, orgId: string, limit?: number): Promise; /** One of the org's invoices, or null when it isn't theirs / doesn't exist. */ export declare function getOrgInvoice(adapter: BillingAdapter, orgId: string, invoiceId: string): Promise; /** PDF link for one of the org's invoices; null when it has none. */ export declare function orgInvoicePdfUrl(adapter: BillingAdapter, orgId: string, invoiceId: string): Promise; //# sourceMappingURL=billing.d.ts.map