import { type Localized, type LocaleOptions, type LocalizedList, type PartialMessages } from "./i18n.js"; export type BillingInterval = "monthly" | "yearly"; /** Minor units (cents). 1 credit = 1 minor unit, throughout this library. */ export type Money = number; export interface IntervalPrice { monthly: Money; yearly: Money; } export declare const INTERVALS: readonly BillingInterval[]; export interface SeatTypeDisplay { /** "Standard seat". A plain string, or one per locale. */ label: Localized; /** One muted line on what the seat buys. */ usage?: Localized; /** * Short form for a badge or a chip: "Standard", where `label` is "Standard * seat". A pricing card has room for the noun; the pill on a usage screen that * says which seat you hold does not, and truncating `label` there is the app * guessing at where the word ends. */ badge?: Localized; } export interface PlanDisplay { /** * Commercial proper noun: "Pro". * * Every text field here is {@link Localized} — a plain string for one language, * or `{ en: …, it: … }` to serve several from one config. The library resolves * it against the surface's locale, falling back to the language subtag and then * to `defaultLocale` (English). */ name: Localized; /** Short fragment under the name. No full stop. */ tagline?: Localized; /** Ascending. Plans without one sort after, in config order. */ order?: number; /** Badge text: "Most popular". */ badge?: Localized; featured?: boolean; /** Lead-in above the bullets: "Everything in Hobby, plus:". */ featuresIntro?: Localized; /** A list, or one list per locale — translations legitimately differ in length. */ features?: LocalizedList; /** The library picks the CTA *kind* from `sale`; the words are the app's. */ cta?: { label: Localized; href?: string; }; /** Copy for a plan with no per-seat figure to show (a committed package). */ pooled?: { title: Localized; note?: Localized; }; /** Keep out of generated pricing surfaces (internal or grandfathered). */ hidden?: boolean; } /** The seat type a member is assumed to hold when nothing says otherwise. */ export declare const DEFAULT_SEAT_TYPE = "standard"; export interface SeatTypeSpec { /** Per-seat recurring price. 0 = free (no Stripe price is minted). */ price: IntervalPrice; /** Credits this seat contributes to the cycle's entitlement. Default 0. */ includedCredits?: number; /** Max seats of this type. null/absent = unlimited. */ max?: number | null; /** Minimum purchasable, and where a stepper starts. Default 0. */ min?: number; /** * A seat drawn by API keys and agents rather than by a named person. * * There is normally one per workspace (`max: 1`), it is not a card row, and * its allowance defaults to overflowing into the wallet rather than blocking — * an agent hitting a hard stop mid-run is worse than a small charge. This * makes declarative what used to be a hardcoded `caller.kind === "user"` test. */ shared?: boolean; display?: SeatTypeDisplay; } export type Sells = /** No Stripe product and no subscription: a free plan, or a pure wallet. */ { kind: "nothing"; } /** One line item per seat type; quantity = seats bought of that type. */ | { kind: "seats"; seatTypes: Record; /** Minimum TOTAL seats across every type ("a team of one is Hobby"). */ minSeats?: number; maxSeats?: number | null; /** * The seat count is chosen when the plan is BOUGHT and cannot be changed * afterwards — growing the team is a plan change, not a bigger basket. * * A product decision, so it is declared rather than assumed: plenty of * products do sell a seat onto a live subscription, and for them this stays * off and `changePlan` keeps taking a new basket. Declared, `changePlan` * refuses a same-plan change that only moves quantities, which is what makes * the rule true for an API, CLI and MCP caller and not merely for whichever * screen happens to omit the control. A UI that hides a button the API still * honours is a rule enforced by absence, which is the gap this library exists * to close. * * The FIRST purchase is unaffected (there is no subscription to change), and * so is a move to a different plan, which may carry any basket that plan * allows. */ seatsFixed?: boolean; /** Intervals actually sold. Default both. */ intervals?: readonly BillingInterval[]; } /** * ONE line item, quantity 1 — a flat subscription or an annual commitment. * * Note there is no plan-level price on the `seats` variant: a seat-typed * plan's plan-level price was never minted by `ensurePlans`, yet it was read * elsewhere to decide whether the plan was purchasable. Making it * unrepresentable is the point. */ | { kind: "flat"; price: IntervalPrice; intervals?: readonly BillingInterval[]; }; export type Grant = /** * Nothing is credited. The right answer for a free plan, for a committed * plan, and for anything whose included usage is an entitlement — i.e. for * everything except a plan that literally sells prepaid credit. */ { kind: "none"; } /** Σ seatTypes[t].includedCredits × PURCHASED quantity. */ | { kind: "purchased_seats"; } /** credits × active member count. */ | { kind: "per_member"; credits: Money; } /** A fixed number of credits per cycle. For a plan whose product IS credit. */ | { kind: "fixed"; credits: Money; }; /** What happens when an entitlement window is used up. */ export type Exhausted = /** * Refuse, even if the wallet could pay. Right for a committed package (its * overage is a conversation, not a silent charge) and for a free plan (whose * users have no wallet to protect). */ "block" /** Fall through to the prepaid wallet, so a top-up funds the overage. */ | "wallet"; /** * The window a `cap` is measured over. * * `cycle` (the default, and the historical behaviour) is the SUBSCRIPTION period: * a plan billed annually gets one window a year. That is right for a committed * package — a year's worth, bought up front — and wrong for anything a customer * is told they get "per month", because an annual subscriber would receive twelve * months' allowance on day one and nothing after it ran out. * * `month` measures the same allowance over the calendar month whatever the * billing interval, which is what "1 000 per seat per month, billed annually" * actually means. It is the only way to say that: the pack size cannot express it, * since the size is a number and the period is the window. */ /** * Which seat the caller holds, and the plan's own words for it. * * On the PURE module: resolving a seat's name is a lookup in the catalogue plus a locale * choice, and it was reachable only through `usage.ts` — which imports the allowance engine, * which imports Stripe. So a seat picker or a pricing card, the two screens that most want a * seat's name, had to pull the whole engine through the root barrel to get a string. That is * the mistake `CREDITS_PER_UNIT` already made once, at the cost of every page in a consumer. */ export interface UsageSeat { /** Seat type key: a sold seat type, the plan's implicit seat, or `api`. */ type: string; /** * `display.badge`, else `display.label`, resolved for the requested locale — * null when the config gave that seat no display at all. The badge form wins * because this is the pill on a usage screen, not a pricing card. */ label: string | null; /** * The FULL form on its own — `display.label`, null when the config gave none. * * `label` above is the PILL value and collapses the two, so a caller writing a sentence * ("Passa a Posto Premium") could not get the long name back once a badge existed — one * reached into `model.seatTypes.find(...)?.display?.label` itself to recover it, which is a * consumer reading the catalogue shape to answer a question the resolver is for. */ name: string | null; /** * The SHORT form on its own — `display.badge`, null when the config gave none. * * `label` collapses the two, which is right for a pill and wrong for everything else: a * consumer needing the short word where the long one already carries a noun ("Posto * Premium" in a row that says "posto") was reduced to stripping the noun with a regex, in * two files. Both forms are the config's, so both are reported and the caller picks. */ badge: string | null; } /** * How a plan presents one seat type: a sold seat type first, then the implicit * seat of a plan that sells none. Exported because a members list wants the same * pill as a usage screen, and reimplementing this lookup is how the two drift. */ export declare function resolveSeat(model: PlanModel | null, type: string, locale?: LocaleOptions): UsageSeat; /** * Credits per unit of currency — 100, i.e. one credit per cent. * * On the PURE module deliberately. It is the ratio a checkout summary and a credit control * render ("€12.50 = 1250 crediti"), so consumers reach for it from CLIENT components — and a * copy defined next to the Stripe engine, or re-exported through the root barrel, drags the * MCP SDK, Stripe and WorkOS into a browser bundle to answer a multiplication. Measured: doing * exactly that turned every page of a consumer into a 500. * * It is a ratio, not a currency thing: a credit is a unit of usage, and `config.currency` * denominates the price of usage. Changing it is a pricing change for every deployment, which * is why it is a constant rather than a config field nobody would keep in step. */ export declare const CREDITS_PER_UNIT = 100; /** Currency → credits, rounded the way every charge in this library rounds. */ export declare function creditsForAmount(amountMajor: number): number; /** Credits → currency, for a control sized in credits (a share of somebody's allowance). */ export declare function amountForCredits(credits: number): number; export type CapWindow = "cycle" | "month"; /** * WHO an included window covers. * * `all` (the default, and the historical behaviour) means every caller draws it, * agents included. `users` restricts it to people: a machine caller (an API key, * an agent — `caller.kind: "api"`, or a `shared` seat) gets NO included * allowance and is funded by the prepaid wallet from its first call. * * That is a different statement from `onExhausted`, which only says what happens * once a window is spent — a machine caller already overflows to the wallet there. * This says the window was never theirs, which is what "API usage is * pay-as-you-go, 0 credits included" means: the two are ordinarily sold as * different things, and a plan whose seat pack is a person's monthly allowance * should not hand the same allowance to a script that can spend it in a minute. */ export type CapCovers = "all" | "users"; export type Cap = /** * No entitlement window: the prepaid wallet is the only gate. This is exactly * what the old `allowanceMode: "global"` did, which is why that value * normalises to here and NOT to a pool. */ { kind: "wallet"; } /** Each caller is capped to its seat type's pack for the window. */ | { kind: "per_seat"; window?: CapWindow; covers?: CapCovers; onExhausted?: Exhausted; } /** * ONE org-wide window: all usage in the cycle counts against `credits`, * whoever spends it. "We don't care about seats." */ | { kind: "pool"; /** The package size, set in config. */ credits?: Money; /** * The package size PER SEAT instead of a flat number: the pool is * `perSeat × seats`, shared across the workspace. * * This is the rung between a flat pool and `per_seat`, and it exists because * a promise and an enforcement are different things. "1 000 credits per seat * per month" is what a pricing page says; `per_seat` additionally *enforces* * it member by member, which is a stricter product than most teams sell and * the only shape that needs a per-member counter to gate. Pooled, the same * promise is one org-wide window — countable by a single Stripe meter * summary at any volume, with no per-member store anywhere. * * The trade is fairness: one member can draw the team's share. Say * `per_seat` when that matters. * * **`"included"` sums each seat TYPE's own `includedCredits` × its purchased * quantity**, which is the only form that can express a plan with more than * one tier. A number cannot: 3 Standard (1 000 each) + 1 Premium (5 000) * should pool 8 000, and `perSeat: 1_000` gives 4 000 while `perSeat: 5_000` * gives 20 000 — one under-delivers against the pricing page and the other * hands Standard seats five times what they paid for. Use a number only when * every tier includes the same amount, or there is one tier. * * Seats are the PURCHASED quantity when the adapter reports one * (`getSubscription().seatCounts` / `.seats`), falling back to the active * member count, then to 1. Purchased rather than active on purpose: a * workspace that bought ten seats and filled six paid for ten. * * Mutually exclusive with `credits`. */ perSeat?: Money | "included"; /** * Unused allowance survives into the next cycle. * * Not a sweep — it widens the window to the subscription's start instead * of the cycle's. Which is the whole reason a window beats a credit: with * no rollover there is nothing to expire. */ rollover?: boolean; /** Optional per-caller ceiling inside the pool, so one member cannot burn * an annual package. Same filtered read, so it costs nothing extra. */ perCallerMax?: Money; /** Mutually exclusive with `rollover`, which widens the window instead. */ window?: CapWindow; covers?: CapCovers; onExhausted?: Exhausted; }; export interface Replenish { /** * Self-serve credit purchase. * * `min`/`max` are what ONE purchase may be, in currency units — the bounds `buy_credits` * and `preview_credit_purchase` enforce. They were literals in the tool schema (5 and * 200 000) and again in every consumer's buy form, which is two places for one rule and * exactly the kind that drifts silently: a UI that lets someone type 3 gets a 400 from a * tool whose own description promised 5. */ purchase?: { packs?: readonly Money[]; min?: Money; max?: Money; }; /** * Threshold-triggered card charge. These are the DEFAULTS the plan offers; * the live per-customer settings stay on the Stripe customer, because a * customer's threshold and card are theirs, not the plan's. */ autoReload?: { threshold: Money; reloadTo: Money; enabledByDefault?: boolean; }; /** * Member asks, owner approves (see topup.ts). Raises that member's pack for the cycle; * never credits the wallet. * * `percent` is what ONE ask is worth, as a share of that member's own seat pack — the * same unit `grantExtraAllowance` uses, and for the same reason: "a quarter more" means * the same thing on a 1 000-credit seat and a 5 000-credit one. It exists because the * person asking should not have to name a number: they know they are out, not what a * reasonable top-up is. Default 25. * * `maxPerCycle` caps what one member may accumulate in a cycle, counting what is already * granted AND what is already queued. */ request?: { percent?: number; maxPerCycle?: Money; /** * The most one grant may be worth, as a share of the member's pack. Default 500. * * A ceiling exists because this is a number an owner types: `percent: 2500` is a typo * that hands out 25× a seat, silently and for free. The consuming app clamped it in its * own server action and again in its number input, and the tool accepted 1000 — three * answers, and the one an agent hit was the loosest. */ maxPercent?: number; /** The percentages a UI offers as one-tap choices. Default `[25, 50, 100]`. */ presets?: readonly number[]; /** Step for a custom percentage input. Default 25. */ step?: number; }; } /** What one credit purchase may be, for this plan: the bounds the tools enforce and the * ones a buy form must not contradict. Currency units, not credits. */ export declare function purchaseBounds(model: PlanModel | null): { min: number; max: number; }; /** What one grant/ask may be worth, for this plan, plus what a UI should offer. */ export declare function requestBounds(model: PlanModel | null): { percent: number; maxPercent: number; presets: readonly number[]; step: number; }; /** * Credits for a share of an allowance — the `replenish.request` axis's own unit * ("a quarter more" means the same thing on a 1 000-credit seat and a 5 000-credit one). * * This IS the arithmetic `grantExtraAllowance` and `requestExtraAllowance` apply, exported * so the dialog that offers "+25%" quotes the number the grant will be. It existed once in * the engine and three more times in a consumer's grant dialog, which is three chances for * the quoted figure and the granted one to disagree. */ export declare function creditsForPercent(basis: number, percent: number): number; /** The inverse, for a control that shows what a typed amount is as a share of the pack. * 0 when the basis is unknown or empty — never NaN or Infinity, which a form renders. */ export declare function percentForCredits(basis: number, credits: number): number; /** The smallest whole percent worth at least `minCredits` — what a percent stepper's own * floor is when the purchase path has a minimum. 0 when the basis is unknown. */ export declare function minPercentFor(basis: number, minCredits: number): number; /** * The auto-reload offer a plan makes, or null when it makes none. * * Null on purpose, with no invented numbers: unlike `purchaseBounds`, whose tool schema * needs a figure, there is no engine default for a reload threshold — the two consumers * that needed one invented it per screen, and one of those screens drifted from its own * config (a dialog prefilling 2 000 against a plan declaring 5 000). A card that gets * null should not prefill or offer the control. These are the plan's DEFAULTS; the live * per-customer settings stay on the Stripe customer. */ export declare function autoReloadDefaults(model: PlanModel | null): { threshold: Money; reloadTo: Money; enabledByDefault: boolean; } | null; /** One purchase, in currency units. Stripe's own floor is well below this; 5 is the point * where the processing fee stops eating the purchase. */ export declare const DEFAULT_PURCHASE_MIN = 5; export declare const DEFAULT_PURCHASE_MAX = 200000; /** What one ask is worth when the plan does not say: a quarter of the member's pack. */ export declare const DEFAULT_REQUEST_PERCENT = 25; /** And the most any single grant may be, so a mistyped 2500 cannot hand out 25× a seat. */ export declare const DEFAULT_MAX_PERCENT = 500; export declare const DEFAULT_PERCENT_PRESETS: readonly number[]; export declare const DEFAULT_PERCENT_STEP = 25; export type Sale = /** No money. The plan a workspace lands on. */ "free" /** Self-serve checkout. */ | "self_serve" /** Sales-assisted: the CTA contacts a human, and no checkout path accepts it. */ | "quote" /** Kept for existing subscribers, offered to nobody new. */ | "legacy"; /** How often a limit's window restarts. `cycle` is the subscription period. */ export type Every = "hour" | "day" | "week" | "month" | "cycle"; export interface RateLimit { every: Every; /** * The ceiling, in the unit the ledger counts: credit cost. With a rate card of * 1 credit per action that IS a request count, which is how these read on a * usage screen ("300 requests per week"). */ credits: Money; /** * Whose usage counts against it. `org` sums the whole workspace; `caller` * gives each member (and the shared API seat) a window of their own. Default * `org`, because a limit with no scope is a limit on the product. */ scope?: "org" | "caller"; /** Restrict to callers of one seat type. Only meaningful with `scope: caller`. */ seatType?: string; /** * Restrict to people (`user`) or to machines (`api`). * * The pace a person can sustain and the pace a script can are different * problems, so they are usually different limits — "500 a week each" for members * and "600 an hour" for agents. Before this, the only way to separate them was a * dedicated `shared` seat TYPE to hang `seatType` off; a plan that funds API * usage from the wallet has no such seat, and both limits then landed on every * caller, where the tighter one made the other unreachable. */ callerKind?: "user" | "api"; /** * WHAT the window governs. Default `all` — today's behaviour, unchanged. * * `all` is a pace cap on the PRODUCT: nothing lifts it, not a wallet and not a top-up, * and the caller waits. Right for "600 an hour" against an agent, where the thing being * protected is the infrastructure. * * `included` is a pace cap on the ALLOWANCE: it governs what the plan gives away, and * paid usage carries on past it. Right for "500 a week each" on a plan whose card says * pay-as-you-go — because under `all` a workspace with credits already bought sits * refused for three days, which is not what pay-as-you-go means. It is the shape Claude's * own weekly limit has: included usage stops, usage credits continue. * * The distinction is not cosmetic to the reads either: an `included` window counts only * included usage, so wallet-funded calls neither fill it nor are refused by it. */ covers?: "all" | "included"; /** Label for a usage screen. Defaults to the window name. */ label?: string; } export interface PlanLimits { /** Max members in the workspace. null = unlimited. */ members?: number | null; /** * Usage ceilings per window, all enforced at once. Independent of `cap`: these * never fund anything and never fall through to the wallet, because a rate * limit that a top-up could lift would not be a rate limit. */ rate?: readonly RateLimit[]; } export interface PlanSpec { sells: Sells; /** * Default `none`, for every `sells` — including seats. * * Crediting an invoiced plan's own included credits discounts its own renewal * (a Stripe credit balance auto-applies to the next invoice; measured at ~48% * off a seat). An included allowance belongs in `cap`, which is counted rather * than credited, so a plan that says nothing gets the safe answer instead of * the one `checkPlansConfig` immediately flags as an error. */ grant?: Grant; /** Default: `seats` → per_seat, otherwise wallet. A POOL IS NEVER INFERRED — * its size is a commercial decision, so it has to be written down. */ cap?: Cap; replenish?: Replenish; /** * REQUIRED, and deliberately not inferable. * * Inferring "sellable" from `price > 0` is what let an agent buy a quote-only * Enterprise plan at its placeholder price. You cannot express "quote-only" * by accident. */ sale: Sale; limits?: PlanLimits; display?: PlanDisplay; /** * The seat a member occupies on a plan that does NOT sell seats. * * A free or flat plan still seats people — they just aren't line items — and a * screen that badges "which seat do I hold" had nothing to show for them, while * the meter quietly filed them under `standard` (a seat type such a plan does * not even declare). Naming it here makes that seat sayable: `{ key: "solo", * display: { badge: "Solo" } }`. * * Ignored when `sells.kind === "seats"` — there the SOLD seat types are the * seat types, and a second answer would be a second source of truth. */ seat?: { key?: string; display?: SeatTypeDisplay; }; } /** What library functions accept. Was a supertype of the pre-0.54 `PlanDef` too, * until 4.0.0 removed it — see the note on `definePlans`. */ export type PlanCatalog = Record; /** * Identity helper for a plans config. * * Annotating with `PlanCatalog` widens the keys to `string` and loses `display` * autocomplete; this keeps the literal types while still checking the shape. */ export declare function definePlans>(plans: T): T; export interface NormalSeatType { key: string; price: IntervalPrice; includedCredits: number; min: number; max: number | null; shared: boolean; display: SeatTypeDisplay | null; } export interface PlanModel { key: string; sells: Sells; grant: Grant; cap: Cap; replenish: Replenish; sale: Sale; limits: { members: number | null; rate: readonly RateLimit[]; }; display: PlanDisplay | null; /** Intervals actually sold. Empty for `sells: nothing`; an annual-only * commitment is `["yearly"]` and no monthly price is ever minted. */ intervals: readonly BillingInterval[]; /** Empty for flat and free plans. Config order. */ seatTypes: readonly NormalSeatType[]; /** * The implicit seat of a plan that sells none, normalised. Null when the plan * sells seats (they are in `seatTypes`) or when the config didn't name one. */ seat: { key: string; display: SeatTypeDisplay | null; } | null; /** Flat-plan price, or null. */ price: IntervalPrice | null; } export declare function normalizePlan(key: string, spec: PlanSpec): PlanModel; export declare function normalizePlans(plans: PlanCatalog): readonly PlanModel[]; export declare function planModel(plans: PlanCatalog, key: string | null | undefined): PlanModel | null; /** Plan keys matching a predicate, e.g. `plansWhere(PLANS, p => p.sale === "self_serve")`. * Replaces the several hand-maintained lists of "which plans can be bought". */ export declare function plansWhere(plans: PlanCatalog, predicate: (model: PlanModel) => boolean): string[]; /** Plans a customer can buy without talking to anyone. */ export declare const selfServePlans: (plans: PlanCatalog) => string[]; /** * Which groups of billing tools a catalogue can actually satisfy. * * Every precondition here is already declared on the plan, so registration can * read it instead of taking a boolean per group. The failure this closes: an app * whose plans sell no seats and accept no top-up REQUESTS still registered * `list_seats`, `assign_seat_type` and the five top-up tools — seven tools that * answered `seat_types: []`, refused with "(none configured)", or queued a grant * against an allowance the plan does not have. An agent cannot tell a tool that * will always fail from one it is holding wrong, so a dead tool is not merely * wasted context; it is a false advertisement of what the product does. * * The union across plans is deliberate: a tool must exist for a caller on ANY * plan to discover it, and the per-call decision still belongs to the engine * (`enforceAccess`, then that org's own plan). This says what the catalogue can * ever need, not what today's caller may do. */ export interface ToolCapabilities { /** `buy_credits`, `preview_credit_purchase` — any plan sells top-ups. */ purchase: boolean; /** `set_auto_reload` — any plan offers threshold-triggered reloading. */ autoReload: boolean; /** The five top-up tools — any plan lets a member ask and an owner approve. */ request: boolean; /** `list_seats`, `assign_seat_type` — any plan sells seats to assign. */ seats: boolean; /** `change_plan` and friends — any plan can be bought without a salesperson. */ lifecycle: boolean; /** `get_usage`, `get_usage_limits` — any plan includes or paces usage. */ usage: boolean; /** * Custom pricing: `accept_plan_quote` for the customer, `quote_plan_change` and * `sell_credits` for an operator. * * Declared by `sale: "quote"` on any plan — a catalogue that sells everything * self-serve has no conversation to price, and three tools for one it cannot have is * the same false advertisement `seats` already refuses to make. */ quote: boolean; } /** Every group on. What a consumer that passes no catalogue keeps getting. */ export declare const ALL_TOOL_CAPABILITIES: ToolCapabilities; export declare function toolCapabilities(plans: PlanCatalog): ToolCapabilities; export type Quantities = Record; /** Where a seat stepper starts: each type's `min`, with the plan's `minSeats` * absorbed by the first non-shared type so the default basket is already valid. */ export declare function defaultBasket(model: PlanModel): Quantities; export interface BasketLine { /** Null for a flat plan's single line. */ seatType: string | null; quantity: number; /** Per-seat (or flat) price for the interval, minor units. */ unit: Money; /** quantity × unit. */ amount: Money; } /** * Price a basket from the catalogue — the DISPLAY half of `validateBasket`. * * Does not validate: `validateBasket` is the gate, and a stepper needs the number for * baskets the gate would refuse (that is how it explains the refusal). A consumer's * checkout hand-summed `seats[k] × price[interval]` beside the very component that * called `validateBasket`, which is two sources for one figure. * * `taxPercent` yields a pre-address estimate: the tax rounds ONCE on the summed * subtotal — the same rule `quoteCreditPurchase` applies — because rounding each line * and adding drifts a cent. The real total is still Stripe's once an address exists; * this is what a form shows before one does. */ export declare function priceBasket(model: PlanModel | null, seats: Quantities | undefined, interval: BillingInterval, opts?: { taxPercent?: number; }): { subtotal: Money; lines: BasketLine[]; tax?: Money; total?: Money; }; /** * The stepper's numbers, collapsed the way a UI needs them. * * `maxSeats` is the TIGHTEST of `sells.maxSeats` and `limits.members`, because * `validateBasket` enforces both and a stepper that read only one offered seats the * other refuses — a consumer re-derived exactly this with its own `tighter()`. * Deliberately no canAdd/canRemove predicates: with the numbers published the * comparison is a one-liner that cannot drift, and `validateBasket` stays the refusal * (the `seatCapacity` precedent — the guard answers one candidate, a UI needs the * numbers). */ export declare function basketBounds(model: PlanModel | null): { minSeats: number; /** Null = unbounded. */ maxSeats: number | null; seatTypes: Record; }; export type BasketProblem = { code: "unknown_plan"; } | { code: "not_purchasable"; sale: Sale; } | { code: "unknown_seat_type"; seatType: string; } | { code: "below_minimum"; min: number; got: number; } | { code: "seat_limit"; max: number; got: number; } | { code: "seat_type_limit"; seatType: string; max: number; got: number; } | { code: "member_limit"; max: number; got: number; } | { code: "interval_unavailable"; interval: BillingInterval; }; /** * Everything wrong with a basket, as data. * * Pure and network-free, so it can run in a stepper and at the checkout * boundary from one implementation. `seats` and the per-type caps were declared * for a long time and enforced nowhere: a crafted request could buy any * quantity of any seat type, including fifty of a seat meant to be unique. */ export declare function validateBasket(plans: PlanCatalog, opts: { plan: string; interval?: BillingInterval; seats?: Quantities; /** "purchase" also enforces `sale`. Use "display" to price a basket for a * plan nobody may buy. */ for?: "purchase" | "display"; }): BasketProblem[]; /** * A basket that may not be bought, thrown from the paths that would buy it. * * `changePlan` wraps the same `problems` in a `PlanChangeError("invalid_basket")` because * it can refuse for half a dozen other reasons and the caller branches on the code; a * Checkout Session has exactly this one refusal, so it gets its own type rather than a * plain Error a consumer has to string-match. */ export declare class InvalidBasketError extends Error { readonly problems: BasketProblem[]; constructor(problems: BasketProblem[], message?: string); } /** * One-line summary of a basket problem, for an error a customer may read. * * English unless a `messages` bundle says otherwise — the same bundle the pricing * derivations take, so an app translates these once. */ export declare function describeBasketProblem(problem: BasketProblem, messages?: PartialMessages): string; /** Credits to GRANT for a paid cycle. Zero for `grant: none`, which is now the * default for anything whose allowance is an entitlement. */ export declare function grantFor(model: PlanModel | null, ctx: { seatCounts?: Quantities; memberCount?: number; }): number; /** * The org-wide entitlement for a cycle, or null when the plan has no pool. * Falls back to the grant size when a plan both pools and credits. * * `seats` is only read by a `perSeat` pool, and takes either form: * * a NUMBER — total purchased seats. All `perSeat: ` needs. * a RECORD — seat type → purchased quantity. What `perSeat: "included"` * needs, since it multiplies each tier by its OWN allowance. * * It defaults to one seat rather than throwing, so a pricing surface with no org * in hand still gets a per-seat unit to display, and a caller that forgets it * under-reports the pool rather than over-granting it. Given a total where the mix * is needed, the SMALLEST tier is assumed for the same reason — a wrong number * that refuses too early is recoverable; one that hands out allowance nobody paid * for is not. `resolveAllowance` resolves the real counts; see `seatsFor`. */ export declare function poolSizeOf(model: PlanModel | null, seats?: number | Record): number | null; /** Whether this plan's pool is sized by seat count — i.e. whether a caller has to * resolve one before `poolSizeOf` means anything. */ export declare function poolIsPerSeat(model: PlanModel | null): boolean; /** The pack a caller is entitled to for the cycle, or null when uncapped. */ export declare function packSizeOf(model: PlanModel | null, seatType: string | undefined): number | null; /** * What to do when a window is used up. * * An agent's usage overflows into the wallet; a person's blocks. That was * previously a hardcoded `caller.kind === "user"` test in the meter, so the * caller kind is still honoured — a legacy config has no way to mark a seat type * as shared, and changing behaviour for it silently is not on. A config that * DOES declare `shared: true` gets the same answer without depending on how the * call arrived. */ export declare function exhaustedPolicy(model: PlanModel | null, caller?: { seatType?: string; kind?: "user" | "api"; }): Exhausted; /** * Whether this caller draws the plan's included window at all. * * False only for a machine caller on a plan whose cap declares `covers: "users"`. * A `shared` seat counts as a machine caller for the same reason it does in * `exhaustedPolicy`: the seat exists to be drawn by agents. */ export declare function capCovers(model: PlanModel | null, caller?: { seatType?: string; kind?: "user" | "api"; }): boolean; export interface CycleWindow { /** Epoch ms, inclusive. */ start: number; /** Epoch ms, exclusive. Null when open-ended (no subscription period known). */ end: number | null; /** Stable identity, for per-cycle records like top-up grants. */ key: string; } /** * The window usage is measured over. * * The SUBSCRIPTION period, when one is known — an annual pool measured over * calendar months would reset twelve times a year, handing out twelve times the * package. The calendar month remains the fallback for an org with no * subscription (a free plan, or a pure wallet), which is what this library did * unconditionally before. * * `rollover` widens the window to the subscription's start instead, which is all * "unused allowance carries over" has to mean. */ /** * The aligned window a rate limit is measured over, and when it resets. * * Aligned to UTC so the answer is the same everywhere and a reset time can be * stated: top of the hour, midnight, MONDAY midnight, the 1st. `cycle` defers to * the subscription period (`cycleWindowFor`), which is the only window whose * boundaries belong to the customer rather than the calendar. * * `end` is always known here — that is the point of a fixed window, and what * lets a usage screen count down to the reset. */ export declare function rateWindowFor(every: Every, now?: number, cycle?: CycleWindow): CycleWindow; /** What a `UsageLedger` implementation can count. Declared by the ledger itself * (`covers`), so a config check needs no knowledge of which one is in use. */ export type LedgerCoverage = { /** Can count an ORG-wide window including usage the wallet didn't fund * (`cap: pool`, `scope: "org"` limits, the spend limit). */ orgIncluded: boolean; /** Can count a PER-CALLER window including usage the wallet didn't fund * (`cap: per_seat`, `scope: "caller"` limits). */ callerIncluded: boolean; }; /** * Every window a ledger must be able to count for this plan to be enforceable. * A `cap: wallet` plan needs neither: nothing is included, so every call moves * money and the debits are their own record. * * The `callerIncluded` half asks TWO questions, because it used to ask one and * was wrong in both directions against the reads `resolveAllowance` actually * issues (`allowance.ts`): * * 1. is the read CALLER-FILTERED? `scope: "caller"` is not the only way — an * org-scoped limit carrying `callerKind` is summed across the workspace but * still filtered to that kind, so it is issued as `{callerKind}` and routed * to the per-caller leg. Asking only about `scope` filed it under * `orgIncluded`, so it passed every check and then read 0 forever: a limit * that never applies, which looks like generosity rather than a fault. * * 2. can the usage behind it be INCLUDED? A caller-filtered read over usage the * wallet always funds is answered exactly, and with no lag, by the debits — * no store required. That is the case for every `cap: wallet` plan, and for * an `api` caller under `cap.covers: "users"`, which excludes machines from * the included window and funds them from the wallet on their first call. * Asking only about `scope` rejected those configs, demanding a store for a * question Stripe already answers. */ export declare function coverageNeededBy(model: PlanModel): LedgerCoverage; /** * The plans whose included windows `covers` cannot count, split by cause. * * Both causes are the same silent failure — the window reads 0, so it never * applies and nothing is ever refused, which looks exactly like generosity — but * they have different fixes, which is why they are reported apart: an org-wide gap * is closed by a Stripe meter (no store), a per-caller one needs a store. */ export declare function ledgerGaps(models: readonly PlanModel[], covers: LedgerCoverage): { org: PlanModel[]; caller: PlanModel[]; }; /** * The limits that apply to this caller, in declaration order. * * A limit with a `seatType` applies only to callers holding it, so a plan can cap * an agent's burst rate without capping a person's. A limit with no caller at all * (an org-level read, or a usage screen with no member selected) keeps only the * org-scoped ones — a caller window means nothing without a caller. */ export declare function rateLimitsOf(model: PlanModel | null, caller?: { seatType?: string; kind?: "user" | "api"; } | null): readonly RateLimit[]; export declare function cycleWindowFor(model: PlanModel | null, period: { start?: string | number | null; end?: string | number | null; } | null, now?: number): CycleWindow; //# sourceMappingURL=plan-model.d.ts.map