import type { BillingAdapter } from "./types.js"; import type { Notify } from "./notifications/index.js"; import { type PlanCatalog } from "./plan-model.js"; /** * Chars available in one metadata value. * * WorkOS is the tightest store this library targets (600 per value, 10 keys per * org, ASCII), and it is what the shipped adapter writes to. Anything packed * into a value is measured against this rather than against a record count — * a count cannot be checked against the thing that actually rejects the write, * which is exactly how a cap of 50 shipped for a value that holds 2. */ export declare const METADATA_VALUE_LIMIT = 600; export interface TopUpRequest { id: string; memberId: string; amount: number; cycle: string; status: "pending" | "approved" | "denied"; createdAt: string; /** Set when an admin granted it outright rather than approving a request. */ grantedBy?: string; } /** * Keep the newest requests that FIT, giving up settled records before pending ones. * * The previous bound was a count (50) on a value that holds 2, which is why this * failed in production and never in a test — a count cannot be validated against * the thing that rejects the write. Bounding by the same unit the store limits * (characters) is the only version that cannot drift from it. * * A settled record is history; a pending one is a member waiting for an answer. * So settled records go first, oldest-first, and pending ones are only dropped * when nothing settled is left to give up — at which point the oldest goes, * since a request nobody answered for a whole cycle is stale anyway. */ export declare function trimRequestsToBudget(list: TopUpRequest[], limit?: number): TopUpRequest[]; /** A user requests extra credits for the current cycle (owner must approve). */ export declare function requestTopUp(adapter: BillingAdapter, orgId: string, req: { id: string; memberId: string; amount: number; cycle: string; createdAt: string; }, notify?: Notify): Promise; export declare function listTopUpRequests(adapter: BillingAdapter, orgId: string): Promise; /** Approve a request → adds its amount to the member's cycle grant. Org-scoped; * gate on adapter.isAdmin(orgId, userId) upstream if you need a per-user check. */ export declare function approveTopUp(adapter: BillingAdapter, orgId: string, requestId: string, notify?: Notify): Promise<{ ok: boolean; reason?: "not_found"; }>; /** * Grant extra allowance to a member DIRECTLY, with no request to approve. * * The request→approve flow assumed the member notices the wall and asks. An admin * looking at a usage screen has already noticed, and had no way to act: every path * into a grant needed a `TopUpRequest` that only the member could create. This is * that missing half. * * The grant is also recorded as an already-approved request, so it appears in the * same history as the ones that were asked for. A grant that existed only as a * number in `topUpGrants` would leave an allowance nobody could explain. * * `id` makes it idempotent: a double-clicked button grants once. */ export declare function grantTopUp(adapter: BillingAdapter, orgId: string, input: { memberId: string; amount: number; /** MUST be the key the meter measures with — see `grantExtraAllowance`. */ cycle: string; /** Who granted it, for the history. */ grantedBy?: string; id?: string; createdAt?: string; }, notify?: Notify): Promise<{ ok: boolean; total: number; reason?: "invalid_amount" | "duplicate"; }>; /** * The same grant, with the cycle resolved the way the METER resolves it. * * This is the part a caller must not be trusted with. A grant is stored under a * cycle key and `resolveAllowance` looks it up under the key that * `cycleWindowFor(model, subscriptionPeriod)` produces — the SUBSCRIPTION period * when there is one, the calendar month otherwise. A caller passing a plausible * "2026-08" for an org whose period starts on the 12th writes a grant that is * never read, and nothing anywhere reports an error: the admin sees "granted", * the member stays blocked. So the resolution lives here, once, and both the tool * and any UI go through it. * * Returns `not_capped` for a plan that caps nothing per seat (a pool, or a pure * wallet). There a grant is not merely useless, it is unreadable: the meter only * adds extra allowance on top of a seat PACK, so it would be silently ignored. */ export declare function grantExtraAllowance(adapter: BillingAdapter, input: { orgId: string; plans: PlanCatalog; plan?: string | null; memberId: string; /** * Extra as a PERCENTAGE of the member's own seat pack (25 = +25%). * * The natural unit for this decision: "a quarter more than their seat" means * the same thing on a 1 000-credit seat and a 5 000-credit one, where a fixed * +250 is a rounding error on one and a third of the other. The pack is * resolved here from the member's seat type, so no caller has to know it. */ percent?: number; /** An absolute number of credits instead. Exactly one of the two. */ amount?: number; grantedBy?: string; /** Say that it was granted. See `notifications/`. */ notify?: Notify; id?: string; now?: number; /** * Raise THIS window instead of the billing cycle — `topUpTargetOf` names it. * * The grant is filed under the window's own key, so it lasts exactly as long as the * window does: when the week rolls the key no longer matches and the member is back on * the plan's pace, with nothing to expire or clean up. */ windowKey?: string; /** What `percent` is a percentage OF, when raising a window rather than a seat pack. */ basis?: number; }): Promise<{ ok: boolean; /** The member's grand total for the cycle, after this grant. */ total?: number; /** What this call actually added, in credits. */ granted?: number; /** The pack the percentage was taken from. */ packSize?: number; cycle?: string; reason?: "invalid_amount" | "not_capped" | "duplicate"; }>; /** What one ask is worth when the plan does not say — the same default `grantExtraAllowance` * applies, so asking for a top-up and being granted one unasked are the same size. It lives * in `plan-model.ts` beside the other replenish bounds; re-exported here because this is * where it has always been imported from. */ export { DEFAULT_REQUEST_PERCENT } from "./plan-model.js"; /** The member's own request still waiting on an answer, for `cycle`. */ export declare function pendingTopUpFor(adapter: BillingAdapter, orgId: string, memberId: string, cycle: string): Promise; /** * File a request WITHOUT naming an amount — the mirror of `grantExtraAllowance`. * * The person asking knows they are out of allowance; they do not know what a reasonable * top-up is, and making them type a number invites both the 10-credit ask that solves * nothing and the 100 000 one an owner has to talk them down from. So the size comes from * the plan (`replenish.request.percent`, default 25%) applied to that member's own seat * pack, resolved here the way the meter resolves it. * * Two refusals matter and neither existed before: * * `already_pending` — one open ask per member per cycle. Without it a button that cannot * choose an amount is a button that queues an identical request every * time it is pressed, and the owner answers the same question N times. * The pending request comes back with the refusal, so a UI can say * "waiting" instead of failing. * `limit_reached` — `maxPerCycle` was declared and enforced NOWHERE, so the ceiling a * plan advertised admitted any number of asks. Counted against what is * already GRANTED plus what is already QUEUED, because approving the * queue is what makes it real. */ export declare function requestExtraAllowance(adapter: BillingAdapter, input: { orgId: string; plans: PlanCatalog; plan?: string | null; memberId: string; /** Say that it was asked for. See `notifications/`. */ notify?: Notify; /** Override the plan's share. Absolute credits win over it, as in `grantExtraAllowance`. */ percent?: number; amount?: number; id?: string; now?: number; /** Ask against THIS window rather than the billing cycle — see `grantExtraAllowance`. */ windowKey?: string; /** What `percent` is a percentage of, when the window is not the seat pack. */ basis?: number; /** * Is anything actually refusing this member right now? * * `false` REFUSES the ask, because a request from somebody at 40% is a question with no * answer: nothing is stopping them, so there is nothing for an owner to decide, and the * pending record blocks the real ask they will make when they do run out. The rule * existed only in the screens that draw the button — so any tool call, server action or * agent could file one, and one did. * * `null` or absent ALLOWS: a caller that cannot read usage must not be able to leave a * member unable to ask for anything, which is the same trade-off `enforceMember` and * `seatAssignable` make where the fact cannot be established. */ blocked?: boolean | null; }): Promise<{ ok: boolean; id?: string; /** What was asked for, in credits. */ amount?: number; /** The pack the percentage was taken from. */ packSize?: number; cycle?: string; /** The ask already open, when that is why this was refused. */ pending?: TopUpRequest; reason?: "invalid_amount" | "not_capped" | "already_pending" | "limit_reached" | "not_blocked"; }>; export declare function denyTopUp(adapter: BillingAdapter, orgId: string, requestId: string, notify?: Notify): Promise<{ ok: boolean; reason?: "not_found"; }>; /** A member's approved extra allowance for a cycle. `resolveAllowance` reads this * on the hot path and adds it to the seat pack. Reads the member's own store when * the adapter has one, falling back to the org blob so a grant written by an * earlier version is still honoured. */ export declare function extraAllowance(adapter: BillingAdapter, orgId: string, memberId: string, cycle: string): Promise; //# sourceMappingURL=topup.d.ts.map