import { type PlanCatalog, type PlanModel } from "./plan-model.js"; /** * The seat types a member can BE on, cheapest first. * * `shared` types are excluded: they are the pool an API caller or a guest draws from, not a * rung a person climbs, and including them made "the next seat up" point at something no * human can be assigned. * * This ordering — non-shared, by monthly price — is the definition of "better seat" used by * every function below. It is exported because a UI that renders the ladder has to sort it * the same way, and the only alternative to publishing the rule is each caller guessing it. */ export declare function seatLadder(model: PlanModel): PlanModel["seatTypes"]; /** * The seat a member holds when nobody has assigned them one: the cheapest non-shared type. * * An UNASSIGNED member is not on "no seat" — they draw the plan's entry-level pack, which is * what the meter measures them against and what their badge says. Treating absent as zero * made the ladder offer a Standard member the Standard seat they were already effectively on, * which is how this was caught: the button read "Assegna Posto Standard". */ export declare function defaultSeatOf(model: PlanModel): string | null; /** What a seat costs per month — the ordering "a better seat" means. An absent assignment * resolves to the default seat, not to nothing. */ export declare function seatRank(model: PlanModel, seatType: string | null): number; /** The next seat type up, or null when they are already on the best one. */ export declare function nextSeatUp(model: PlanModel, seatType: string | null): string | null; /** * Is this the best seat the plan sells? * * `nextSeatUp(...) === null` already answered it, but only to a reader who knows that is what * null means there. A screen deciding whether to offer an upgrade, and an agent asking the * same question over the API, should not both have to know. */ export declare function isTopSeat(model: PlanModel, seatType: string | null): boolean; /** * Does THIS plan sell that seat type? * * Against the org's own model, which is the check that was missing: the `assign_seat_type` * tool validated against the union of seat keys across every plan in the catalogue, so a * Premium key was "valid" for a workspace on a plan that does not sell it, and the write * went through to a seat the meter then could not price. * * `shared` types count — an API caller genuinely holds one — so this is a wider question * than `seatLadder`. */ export declare function seatTypeExists(model: PlanModel, seatType: string): boolean; /** What a plan costs a month at its default basket — the ordering "a better plan" means. * A quote-only plan ranks above everything priced, because nothing self-serve exceeds it. */ export declare function planRank(model: PlanModel): number; export interface PlanActions { /** The next plan up, or null when already at the top. */ upgradeTo: string | null; /** The next plan down, or null when already at the bottom. */ downgradeTo: string | null; /** Whether there is a paid subscription to end. False on a free plan — there * is nothing to cancel, so the action shouldn't be offered. */ canCancel: boolean; /** The plan a cancellation lands on. */ cancelTo: string | null; } /** * Which of upgrade / downgrade / cancel apply to an org on `currentPlan`. * * Pure, so a UI can hide what doesn't apply instead of offering an action that * will be refused: no "upgrade" on the top plan, no "cancel" on a free one. */ export declare function planActions(plans: PlanCatalog, currentPlan: string | null): PlanActions; export interface PlanRequest { id: string; /** WorkOS user id of whoever asked. */ memberId: string; /** * WHAT they are asking to move: the workspace's plan, or their own seat. * * They are different asks with different prices and different approvers' reasoning — a * seat upgrade costs one seat's difference and affects one person, a plan change moves * everybody — but they queue in the same place, because to an owner they are one list of * people waiting. * * Absent means "plan": the field was added after the queue existed, and a stored record * without it is a plan request. */ kind?: "plan" | "seat"; /** The target — a plan key, or a seat-type key when `kind` is "seat". */ plan: string; /** * `quoted` is the rung a QUOTE-only plan adds, and it sits between the two that were * here: somebody asked, an operator priced it, and nobody has paid yet. `done` still * means "an admin acted on it", which for a quoted request means they accepted and it * has been charged or invoiced. */ status: "pending" | "quoted" | "done" | "denied"; createdAt: string; /** Why they are asking, in their words. Optional, capped at 140 chars by the caller. */ note?: string; /** * Whatever the deployment's own form collected, verbatim. * * A bag rather than fields, because the library ACTS on none of it: `totalEstimatedSeats` * is what one consumer asks for and the next will want a region, a use case, a contract * start. Typing each one here would make this package the place a consumer edits to add a * question to its own form, which is the opposite of what a seam is for. * * Kept small on purpose — the whole queue shares one 600-character metadata value, so an * oversized bag is dropped rather than allowed to evict somebody's pending ask. */ metadata?: Record; /** * Who to answer, when it is not simply the account they signed up with. * * A person's registered email is personal as often as not, and the first thing a * salesperson needs is an address on the company's own domain. */ contact?: { firstName: string; lastName: string; email: string; }; /** * What an operator answered: a quantity, and a price PER CREDIT. * * Per credit rather than a total because that is the number a negotiation is actually * about — "we can do 0.7¢" survives the customer changing how much they want, and a * total does not. The total is arithmetic, and the tool does it. */ quote?: { credits: number; /** Minor units per credit, so 0.7¢ is 0.7 — a fraction is allowed on purpose. */ unitPriceMinor: number; /** Minor units, `credits × unitPriceMinor` rounded. What they will actually be charged. */ totalMinor: number; /** ISO date the price expires. A price with no expiry is a price for ever. */ validUntil?: string; note?: string; at: string; }; /** * What happened when the admin accepted: charged on file, invoiced, or waiting on the * cardholder's BANK. * * The third is not a failure and not a bill — the invoice exists and is authenticated * from its hosted page — and recording it as either of the others would send somebody to * the wrong place to fix it. */ accepted?: { at: string; method: "saved_card" | "invoice" | "needs_authentication"; invoiceId?: string; invoiceUrl?: string; }; } /** Has the workspace already reached (or passed) what this request asked for? */ export declare function isSatisfied(request: PlanRequest, plans: PlanCatalog, currentPlan: string | null, /** The asker's seat type now — needed only for a seat request. */ currentSeatType?: string | null): boolean; /** * WHICH ask to offer someone who is out of usage. One decision, in one place. * * The ladder climbs the cheapest, most targeted rung first, and each rung exists because the * one below it cannot help: * * 1. a better SEAT — their pack is what their seat includes, so the way to have more of it * is a bigger seat. A Standard member should be offered this, never a top-up: topping * up buys them a few days and leaves them in the same place next week. * 2. CREDITS, where money can actually lift the wall: a `covers: "included"` window paces * only what the plan gives away, and a pack whose plan overflows to the wallet is the * same statement. Paying works, permanently and without anybody's permission, so * asking an owner for a free exception would be the worse of two available answers. * 3. extra USAGE on the blocked window — the answer where money CANNOT help: a * `covers: "all"` window is the product's own pace and no purchase touches it, so an * exception somebody grants is the only door. * 4. a PLAN change — for a plan with no per-member allowance at all. A pooled plan's * windows belong to the workspace, so there is nothing personal to raise and * `grant_top_up` refuses it outright. * * Rungs 2 and 3 were ONE rung, and it was wrong in whichever direction the deployment went. * A plan whose card says pay-as-you-go sent a blocked member to ask an owner for something * they could have bought in a click; a plan pacing the product offered credits that lift * nothing, taking money for a wall that would still be there. Which of the two applies is * not a preference — it is what `covers` says, so it is read rather than configured again. * * Returns null when nothing is blocked, which is when nothing should be offered — a control * permanently on screen asks a question nobody at 40% can answer. */ export declare function nextUsageAsk(model: PlanModel | null, input: { /** From `topUpTargetOf` — what, if anything, is refusing them, and whether paying lifts it. */ blocked: { kind: "rate" | "pack"; covers?: "all" | "included"; } | null; seatType?: string | null; plans: PlanCatalog; currentPlan?: string | null; }): { ask: "seat"; to: string; } | { ask: "credits"; } | { ask: "usage"; } | { ask: "plan"; to: string; } | null; /** Which tool carries out (or asks for) a rung. Tool names, deliberately: the answer to * "I am blocked, now what" should name the next call, not a UI concept a headless caller * has to translate. */ export type UsageActionTool = "assign_seat_type" | "buy_credits" | "grant_top_up" | "change_plan" | "request_seat_change" | "request_top_up" | "request_plan_change"; export interface UsageAction { /** The rung, from `nextUsageAsk`. */ rung: "seat" | "credits" | "usage" | "plan"; /** What the rung points at: a seat type, or a plan key. Absent for `credits`/`usage`. */ to?: string; /** Whether the person looking at this can carry it out, or has to ask an admin. */ actor: "self" | "admin"; /** The call that does it — the action itself when `actor` is "self", the request when not. */ action: UsageActionTool; } /** * The rung, and WHO may act on it. `nextUsageAsk` answers the first half; this answers both. * * The second half used to be the consumer's, and the note here said so. It came back as a * gap: every act on a rung is an owner action — `change_plan`, `assign_seat_type` (a seat is * a price), `grant_top_up` — so a member's only route is a request, and each app worked that * out again in a React component. Scartoffie's lived in the component that renders the * button, which meant an agent hitting the same wall through the API got the rung and no * idea that buying was not its call. * * `purchase` is the one part that is genuinely a deployment's choice (`config.roles.purchase`), * because a product whose members hold their own cards is a real arrangement. Everything else * follows from gates this library already enforces, so it is read rather than configured * twice — and `buy_credits` enforces the same value, which is what makes this answer true * rather than advisory. */ export declare function usageAction(model: PlanModel | null, input: { blocked: { kind: "rate" | "pack"; covers?: "all" | "included"; } | null; seatType?: string | null; plans: PlanCatalog; currentPlan?: string | null; /** The person asking. An org API key with no principal behind it IS the org, so it is * owner-level — the same reading `enforceAdmin` applies. */ actor?: { isAdmin?: boolean; }; /** `config.roles.purchase`. Defaults to the config default, not to permissive. */ purchase?: "admin" | "member"; }): UsageAction | null; //# sourceMappingURL=ladder.d.ts.map