import { type PartialMessages } from "./i18n.js"; import { type CycleWindow, type Every, type PlanCatalog, type PlanModel } from "./plan-model.js"; import type { BillingAdapter, ResolvedConfig } from "./types.js"; import { type FundingSource, type UsageLedger } from "./usage-ledger.js"; /** One limit, with the window it is being measured over. */ export interface LimitState { every: Every; scope: "org" | "caller"; /** Extra granted to THIS caller on THIS window, already included in `size`. Always 0 for * an org-scoped window, which is nobody's to raise. */ extra?: number; /** `all` refuses outright; `included` only stops the allowance, and paid usage continues. */ covers?: "all" | "included"; /** From the config; null when the plan didn't label it. */ label: string | null; size: number; used: number; remaining: number; /** The aligned window. `end` is always known, so a UI can count down to it. */ window: CycleWindow; /** * WHOSE limit this is. Absent means `rate` — every limit predates this field. * * `rate` is the product's, declared in the plan: the customer cannot lift it, * so the refusal tells them to wait. `spend` is the customer's OWN monthly * ceiling (`setSpendControls`): they can raise it, so the refusal says so * instead. Same mechanism, same arithmetic — only the advice differs, which is * why this is a field on one shape rather than a second kind of limit. */ kind?: "rate" | "spend"; /** * The credit figures the CUSTOMER asked to be warned at (`setSpendControls`). * * Only on the `spend` limit, because only that one is theirs. It comes off the customer * object this read already retrieves, so carrying it costs nothing — and it was being * dropped, which is why a billing page could offer "warn me at 10 000" and nothing * anywhere ever looked at the answer. */ alertsAt?: number[]; } export interface AllowanceState { plan: string | null; cycle: CycleWindow; /** * Every rate limit that applies right now, in config order. A call must fit * inside ALL of them; unlike pool/pack/wallet these fund nothing, they only * refuse. */ limits: LimitState[]; /** The org-wide included window, when the plan has one. */ pool: { size: number; used: number; remaining: number; } | null; /** The caller's seat pack, when the plan caps per seat and the caller has one. */ pack: { seatType: string; size: number; used: number; /** Owner-approved top-up for this member, this cycle. */ extra: number; remaining: number; } | null; /** Prepaid balance, in the configured currency. Clamped at 0 for display. */ wallet: number; } export interface AllowanceInput { orgId: string; plans: PlanCatalog; /** Resolved plan key, or null for an org with no plan (a pure wallet). */ plan?: string | null; caller?: { kind: "user" | "api"; id?: string; seatType?: string; }; /** Defaults to the customer on the adapter. */ customerId?: string; /** Overrides the window derived from the subscription period. */ cycle?: CycleWindow; ledger?: UsageLedger; /** Skip the wallet read — for a caller that only needs the entitlement. */ skipWallet?: boolean; /** Skip the customer's monthly spend ceiling. For a read that only wants plan * entitlement, or a surface that must never be refused by it. */ skipSpendLimit?: boolean; /** Pin the clock. Every rate window is derived from it, so a test can place * itself inside a window instead of waiting for one. Defaults to now. */ now?: number; } /** * Everything needed to decide whether a call is allowed, read in as few round * trips as the shape requires. * * The window comes from the SUBSCRIPTION period when one is known, not the * calendar month: an annual package measured monthly would reset twelve times and * hand out twelve packages. The calendar month remains the fallback for an org * with no subscription, which is what this library always did. */ /** * The cycle the meter is measuring right now — the ONE definition of "this * cycle" in the library. * * It is exported because anything that files something against a cycle has to * agree with the thing that reads it back. A top-up grant stored under a key the * meter never looks up is not a smaller grant, it is no grant at all, and it * fails silently: the approval succeeds, the balance never moves. That is * exactly what happened while `request_top_up` computed its own calendar month * and the meter used the subscription period. */ export declare function currentCycle(adapter: BillingAdapter, input: { orgId: string; plans?: PlanCatalog; plan?: string | null; now?: number; }): Promise; export declare function resolveAllowance(adapter: BillingAdapter, config: ResolvedConfig, input: AllowanceInput): Promise; export type DenialReason = "rate_limit_reached" /** The customer's OWN monthly ceiling. Distinct from `rate_limit_reached` * because they can raise it themselves, and the message must say so. */ | "spend_limit_reached" | "pool_exhausted" | "seat_allowance_reached" | "insufficient_balance"; export interface FundingDecision { ok: boolean; source: FundingSource | null; reason?: DenialReason; /** Which limit refused, when the reason is `rate_limit_reached`. */ limit?: LimitState; } /** * Which allowance pays for `cost` — or why nothing does. * * PURE: no Stripe, no adapter, no clock. The order is the point: * * 1. the org's included window, if the plan has one; * 2. the caller's seat pack, if the plan caps per seat; * 3. the prepaid wallet. * * An exhausted window either blocks or falls through to the wallet, per the * plan's `onExhausted`. Blocking even when the wallet could pay is the right * default for a committed package (its overage is a renegotiation, not a silent * charge) and for a free plan; falling through is right where top-ups are the * product. Checking the wallet LAST is also what stops a pooled org being told * "insufficient balance" when the truth is "your package is used up". */ /** * WHICH window a top-up should raise for this caller: the tightest one refusing them now. * * "More usage" is not one thing. A member can be inside their monthly seat pack and still * blocked by a weekly window, which is what pacing is for — and raising the pack there buys * them nothing, because `fundingFor` checks the rate windows FIRST and absolutely. Asking * this question before granting is what keeps the answer honest. * * A rate window wins over the pack when both are exhausted, and the SMALLEST window wins * among rate windows: it is the one that will refuse the next call. And because a rate * grant is filed under the window's own key, it lasts exactly as long as that window — come * the reset the key no longer matches and the member is back to the plan's pace. * * Only caller-scoped windows are offered. An org-wide limit protects the product from the * whole workspace; lifting it for one person is not an exception, it is a different plan. */ export declare function topUpTargetOf(state: AllowanceState): { kind: "rate"; windowKey: string; basis: number; extra: number; every: Every; resetsAt: number | null; covers: "all" | "included"; } | { kind: "pack"; basis: number; extra: number; } | null; export declare function fundingFor(state: AllowanceState, model: PlanModel | null, cost: number, caller?: { kind: "user" | "api"; seatType?: string; }): FundingDecision; /** * Human-readable, for a 402 body or a tool result. * * Through `Messages`, like every other string this package emits. It used to hardcode its * own English and skip the message table entirely — while three keys in that table * (`poolExhausted`, `seatAllowanceReached`, `insufficientBalance`) said nearly the same * thing and were read by nobody, and the two refusals a customer meets most had no key at * all. So the one part of the library a REFUSED caller actually reads was the one part a * deployment could not translate. * * The `reason` is still the contract for anything branching on it; this is the sentence. */ export declare function describeDenial(reason: DenialReason, state: AllowanceState, limit?: LimitState, messages?: PartialMessages): string; //# sourceMappingURL=allowance.d.ts.map