@fluid-app/fluid-cli-billing 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/money.ts","../src/api.ts","../src/browser.ts","../src/index.ts"],"sourcesContent":["// Amounts as the billing API speaks them: integer cents, never floats.\n//\n// The CLI takes amounts in dollars, the unit a merchant reads on the billing\n// page, and turns them into cents with string arithmetic so \"19.99\" is\n// exactly 1999. What the API returns is printed as it came; these helpers\n// only phrase a figure inside a confirmation question.\n\nexport class AmountError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AmountError\";\n }\n}\n\nconst DOLLARS = /^\\$?(\\d+)(?:\\.(\\d{1,2}))?$/;\n\n/**\n * Parse a dollar amount (\"250\", \"$250\", \"19.99\") into integer cents.\n * Rejects anything else — signs, commas, exponents, a third decimal — rather\n * than guessing. `wholeDollars` refuses cents, as purchases and reload\n * figures require.\n */\nexport function parseDollars(\n input: string,\n opts: { wholeDollars?: boolean; label?: string } = {},\n): number {\n const label = opts.label ?? \"Amount\";\n const match = DOLLARS.exec(input.trim());\n if (!match) {\n throw new AmountError(\n `${label} \"${input}\" isn't a dollar amount. Expected something like 250 or 19.99.`,\n );\n }\n const whole = match[1] ?? \"0\";\n const fraction = (match[2] ?? \"\").padEnd(2, \"0\");\n const cents = BigInt(whole) * 100n + BigInt(fraction || \"0\");\n if (cents <= 0n) {\n throw new AmountError(`${label} must be more than $0.`);\n }\n if (opts.wholeDollars && cents % 100n !== 0n) {\n throw new AmountError(`${label} must be whole dollars, not \"${input}\".`);\n }\n if (cents > BigInt(Number.MAX_SAFE_INTEGER)) {\n throw new AmountError(`${label} \"${input}\" is too large.`);\n }\n return Number(cents);\n}\n\n/** A dollar flag that also takes \"none\" to clear the value (sent as null). */\nexport function parseDollarsOrNone(\n input: string,\n opts: { wholeDollars?: boolean; label?: string } = {},\n): number | null {\n return input.trim().toLowerCase() === \"none\"\n ? null\n : parseDollars(input, opts);\n}\n\n/** Phrase integer cents as US dollars (\"$1,029.00\") without float math. */\nexport function formatCents(cents: number): string {\n if (!Number.isSafeInteger(cents)) {\n throw new AmountError(`Expected whole cents from the API, got ${cents}.`);\n }\n const sign = cents < 0 ? \"-\" : \"\";\n const digits = String(Math.abs(cents)).padStart(3, \"0\");\n const dollars = digits.slice(0, -2).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n return `${sign}$${dollars}.${digits.slice(-2)}`;\n}\n","// Calls to the merchant billing API (`/api/settings/billing`, settings-v0)\n// and to usage intake (`/api/v202604/billing/usage_events`).\n//\n// Every amount stays in the API's integer cents. Responses are returned as\n// the API sent them; only confirmation questions phrase figures in dollars.\n\nimport { formatCents } from \"./money.js\";\n\n/** The slice of the CLI's API client these calls use. */\nexport interface BillingClient {\n get<T>(endpoint: string, params?: Record<string, unknown>): Promise<T>;\n post<T>(endpoint: string, body?: unknown): Promise<T>;\n patch<T>(endpoint: string, body?: unknown): Promise<T>;\n}\n\nexport class BillingError extends Error {\n /** A machine-readable reason, printed as `code` like the API's. */\n readonly code?: string;\n\n constructor(message: string, code?: string) {\n super(message);\n this.name = \"BillingError\";\n this.code = code;\n }\n}\n\n/** Why a plan can't be chosen while the saved card's funding is unknown. */\nexport const CARD_FUNDING_PENDING_MESSAGE =\n \"The saved card's type is still being checked, so whether it carries the credit card fee isn't known yet. Nothing was changed; try again in a few minutes.\";\n\nconst BASE = \"/api/settings/billing\";\nconst USAGE_EVENTS = \"/api/v202604/billing/usage_events\";\n\ntype Json = Record<string, unknown>;\n\nfunction id(value: string): string {\n const trimmed = value.trim();\n if (!/^\\d+$/.test(trimmed)) {\n throw new BillingError(`Expected a numeric id, got \"${value}\".`);\n }\n return encodeURIComponent(trimmed);\n}\n\nfunction field<T>(data: Json | null | undefined, key: string, what: string): T {\n const value = data?.[key];\n if (value === undefined) {\n throw new BillingError(`The ${what} response didn't include \"${key}\".`);\n }\n return value as T;\n}\n\n// ---------------------------------------------------------------------------\n// The billing page\n\nexport async function showBilling(client: BillingClient): Promise<Json> {\n return field<Json>(await client.get<Json>(BASE), \"billing\", \"billing\");\n}\n\n/** Set where invoices and billing emails go. null clears it, and Fluid then\n * sends no billing email until one is set. The server validates the\n * address. */\nexport async function setBillingEmail(\n client: BillingClient,\n email: string | null,\n): Promise<Json> {\n const address = email?.trim() || null;\n const data = await client.patch<Json>(BASE, { billing_email: address });\n return field<Json>(data, \"billing\", \"billing update\");\n}\n\nexport async function showStanding(client: BillingClient): Promise<Json> {\n const data = await client.get<Json>(`${BASE}/standing`);\n return field<Json>(data, \"standing\", \"standing\");\n}\n\n/** Move collections onto a saved bank account whose ACH authorization the\n * merchant already gave through instrument setup in the admin. */\nexport async function useBankAccount(\n client: BillingClient,\n bankAccountId: string,\n): Promise<Json> {\n const data = await client.patch<Json>(`${BASE}/instrument`, {\n instrument: { bank_account_id: Number(id(bankAccountId)) },\n });\n return field<Json>(data, \"billing\", \"instrument switch\");\n}\n\n// ---------------------------------------------------------------------------\n// Cursor-paged lists\n\ninterface CursorPage {\n meta?: { pagination?: { next_cursor?: string | null } };\n}\n\n/** The list endpoints cap a page at 100 and default to 25, newest first. */\nconst PAGE_SIZE = 100;\nconst MAX_PAGES = 25;\n\n/**\n * Read every page of a list, newest first. `limit` stops after that many\n * items; without it the whole list is read, and one longer than\n * MAX_PAGES * PAGE_SIZE is refused rather than returned partially.\n */\nexport async function listAll(\n client: BillingClient,\n path: string,\n key: string,\n limit?: number,\n): Promise<Json[]> {\n if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1)) {\n throw new BillingError(\"--limit must be a whole number of at least 1.\");\n }\n const all: Json[] = [];\n let cursor: string | undefined;\n for (let page = 1; page <= MAX_PAGES; page += 1) {\n const want = limit === undefined ? PAGE_SIZE : limit - all.length;\n const data = await client.get<CursorPage & Json>(path, {\n page: { limit: Math.min(PAGE_SIZE, want), cursor },\n });\n const items = (data[key] as Json[] | null | undefined) ?? [];\n all.push(...items);\n const next = data.meta?.pagination?.next_cursor;\n if (limit !== undefined && all.length >= limit) {\n return all.slice(0, limit);\n }\n if (!next || items.length === 0) return all;\n cursor = next;\n }\n throw new BillingError(\n `There are more than ${MAX_PAGES * PAGE_SIZE} ${key.replace(/_/g, \" \")}; pass --limit to read the newest ones.`,\n );\n}\n\nexport const listPayments = (client: BillingClient, limit?: number) =>\n listAll(client, `${BASE}/payments`, \"payments\", limit);\n\nexport const listDocuments = (client: BillingClient, limit?: number) =>\n listAll(client, `${BASE}/documents`, \"documents\", limit);\n\nexport const listCreditMovements = (client: BillingClient, limit?: number) =>\n listAll(client, `${BASE}/credits/movements`, \"credit_movements\", limit);\n\nexport const listCreditPurchases = (client: BillingClient, limit?: number) =>\n listAll(client, `${BASE}/credits/purchases`, \"credit_purchases\", limit);\n\n// ---------------------------------------------------------------------------\n// Stripe links. Stripe's links expire, so each is fetched when asked for.\n\nexport interface StripeLink {\n url: string;\n pdf_url: string | null;\n}\n\nasync function link(\n client: BillingClient,\n path: string,\n key: \"invoice\" | \"document\",\n): Promise<StripeLink> {\n return field<StripeLink>(await client.get<Json>(path), key, \"link\");\n}\n\nexport const paymentInvoice = async (\n client: BillingClient,\n paymentId: string,\n) => link(client, `${BASE}/payments/${id(paymentId)}/invoice`, \"invoice\");\n\nexport const documentLink = async (client: BillingClient, documentId: string) =>\n link(client, `${BASE}/documents/${id(documentId)}/link`, \"document\");\n\nexport const purchaseInvoice = async (\n client: BillingClient,\n purchaseId: string,\n) =>\n link(\n client,\n `${BASE}/credits/purchases/${id(purchaseId)}/invoice`,\n \"invoice\",\n );\n\n// ---------------------------------------------------------------------------\n// Credits and the plan\n\nexport async function showCredits(client: BillingClient): Promise<Json> {\n return field<Json>(\n await client.get<Json>(`${BASE}/credits`),\n \"credits\",\n \"credits\",\n );\n}\n\nexport type PlanChoice =\n | { keepCurrentPlan: true }\n | {\n planKey?: string;\n billingInterval?: \"monthly\" | \"annual\";\n monthlyPriceCents?: number;\n };\n\n/** The PATCH body for a plan choice. A first plan needs a key or a price;\n * the server checks the key against its catalog. */\nexport function planChoiceBody(choice: PlanChoice): Json {\n if (\"keepCurrentPlan\" in choice) return { keep_current_plan: true };\n const body: Json = {};\n if (choice.planKey !== undefined) {\n const key = choice.planKey.trim().toLowerCase();\n if (!key) throw new BillingError(\"The plan key is empty.\");\n body.plan_key = key;\n }\n if (choice.billingInterval !== undefined) {\n if (![\"monthly\", \"annual\"].includes(choice.billingInterval)) {\n throw new BillingError(\n `--interval is monthly or annual, not \"${choice.billingInterval}\".`,\n );\n }\n body.billing_interval = choice.billingInterval;\n }\n if (choice.monthlyPriceCents !== undefined) {\n body.monthly_price_cents = choice.monthlyPriceCents;\n }\n if (Object.keys(body).length === 0) {\n throw new BillingError(\n \"Name a plan, a --monthly-price or --interval, or pass --keep-current.\",\n );\n }\n return body;\n}\n\n/** What a plan choice charges each month, as the server will price it. */\nexport interface PlanCharge {\n planKey: string | null;\n billingInterval: string;\n monthlyPriceCents: number;\n monthlyChargeCents: number;\n surchargeCents: number;\n totalCents: number;\n}\n\n/** The card networks' cap on a card fee, in basis points (Billing::CardSurcharge). */\nconst CARD_NETWORK_CAP_BASIS_POINTS = 300;\n\nfunction num(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isFinite(value)\n ? value\n : undefined;\n}\n\n/**\n * Price a plan choice the way the server prices it (Billing::CreditTermsManager\n * and Billing::CreditTermsResolver): the choice is laid over the plan already\n * waiting for next month, or the plan in force; a key alone costs its catalog\n * price; an annual plan's monthly charge takes the annual discount; and a\n * credit card adds its fee (Billing::CardSurcharge). Throws when the price\n * can't be told, so nothing is changed on a figure nobody saw.\n */\nexport function planCharge(credits: Json, choice: PlanChoice): PlanCharge {\n if (\"keepCurrentPlan\" in choice) {\n throw new BillingError(\"Keeping the current plan prices nothing new.\");\n }\n const base = (credits.next_plan ?? credits.plan) as Json | null | undefined;\n // The catalog at the price each key alone gives it, as the server sends it.\n const offered = Array.isArray(credits.catalog_plans)\n ? (credits.catalog_plans as Json[])\n : [];\n const key = choice.planKey?.trim().toLowerCase();\n const catalog = key ? offered.find((plan) => plan.key === key) : undefined;\n if (key && !catalog) {\n const keys = offered.map((plan) => plan.key).filter(Boolean);\n throw new BillingError(\n `Plan \"${key}\" isn't offered to this company${keys.length ? `; choose one of ${keys.join(\", \")}` : \"\"}. Nothing was changed.`,\n );\n }\n const price =\n choice.monthlyPriceCents ??\n num(catalog?.monthly_price_cents) ??\n num(base?.monthly_price_cents);\n if (price === undefined) {\n throw new BillingError(\n \"Can't tell what this plan would cost each month: name a plan or a --monthly-price. Nothing was changed.\",\n );\n }\n const interval =\n choice.billingInterval ??\n (typeof base?.billing_interval === \"string\"\n ? base.billing_interval\n : \"monthly\");\n const discount = num(credits.annual_discount_percent) ?? 0;\n const charge =\n interval === \"annual\"\n ? Math.round((price * (100 - discount)) / 100)\n : price;\n\n const surcharge = credits.card_surcharge as Json | null | undefined;\n let fee = 0;\n if (\n catalog &&\n interval === \"monthly\" &&\n choice.monthlyPriceCents === undefined &&\n num(catalog.monthly_surcharge_cents) !== undefined\n ) {\n // The server priced this one already.\n fee = catalog.monthly_surcharge_cents as number;\n } else if (surcharge?.applies === true) {\n const rate = num(surcharge.rate_basis_points);\n if (rate === undefined) {\n throw new BillingError(\n \"The credits response didn't include the credit card fee rate. Nothing was changed.\",\n );\n }\n // Billing::CardSurcharge.cents_for: the rate rounded half up, held to\n // the fee's own cap and, on a card, to the networks' 3% rounded down.\n const caps = [Math.floor((charge * rate + 5000) / 10000)];\n const cap = num(surcharge.cap_cents);\n if (cap !== undefined) caps.push(cap);\n if (surcharge.kind !== \"ach\") {\n caps.push(Math.floor((charge * CARD_NETWORK_CAP_BASIS_POINTS) / 10000));\n }\n fee = Math.min(...caps);\n }\n return {\n planKey: key ?? (typeof base?.key === \"string\" ? base.key : null),\n billingInterval: interval,\n monthlyPriceCents: price,\n monthlyChargeCents: charge,\n surchargeCents: fee,\n totalCents: charge + fee,\n };\n}\n\nexport function planChoiceQuestion(\n choice: PlanChoice,\n charge?: PlanCharge,\n current?: Json | null,\n): string {\n if (\"keepCurrentPlan\" in choice) {\n const total = num(current?.monthly_total_cents);\n return `Keep the current plan${total !== undefined ? ` (${formatCents(total)} a month)` : \"\"} and drop the plan chosen for next month?`;\n }\n if (!charge) {\n throw new BillingError(\"A plan change needs its monthly charge priced.\");\n }\n const name = charge.planKey ? `plan ${charge.planKey}` : \"a plan\";\n const discount =\n charge.monthlyChargeCents !== charge.monthlyPriceCents\n ? `, ${formatCents(charge.monthlyPriceCents)} less the annual discount`\n : \"\";\n const fee =\n charge.surchargeCents > 0\n ? ` (${formatCents(charge.monthlyChargeCents)} plus a ${formatCents(charge.surchargeCents)} credit card fee)`\n : \"\";\n return `Change to ${name}, billed ${charge.billingInterval}${discount}, from the 1st of next month? It charges ${formatCents(charge.totalCents)}${fee} to the saved payment method each month.`;\n}\n\n/** Read the credits and price a plan choice before asking about it. */\nexport async function preparePlanChoice(\n client: BillingClient,\n choice: PlanChoice,\n): Promise<{ question: string; charge?: PlanCharge }> {\n planChoiceBody(choice); // refuse a malformed choice before reading\n const credits = await showCredits(client);\n if (\"keepCurrentPlan\" in choice) {\n return {\n question: planChoiceQuestion(\n choice,\n undefined,\n credits.plan as Json | null,\n ),\n };\n }\n // The server refuses a plan until the card's fee is known (409\n // card_funding_pending); say so before asking about a total.\n const surcharge = credits.card_surcharge as Json | null | undefined;\n if (surcharge?.funding_pending === true) {\n throw new BillingError(\n CARD_FUNDING_PENDING_MESSAGE,\n \"card_funding_pending\",\n );\n }\n const charge = planCharge(credits, choice);\n return { question: planChoiceQuestion(choice, charge), charge };\n}\n\n/** Make a plan choice. With the monthly charge and total that were shown,\n * the server refuses (409 plan_charge_changed) a choice that would now\n * charge or total anything else, as when another admin chose a plan or\n * switched the saved payment method to a card meanwhile. */\nexport async function choosePlan(\n client: BillingClient,\n choice: PlanChoice,\n shown?: Pick<PlanCharge, \"monthlyChargeCents\" | \"totalCents\">,\n): Promise<Json> {\n const body = planChoiceBody(choice);\n if (shown && !(\"keepCurrentPlan\" in choice)) {\n body.expected_monthly_charge_cents = shown.monthlyChargeCents;\n body.expected_monthly_total_cents = shown.totalCents;\n }\n const data = await client.patch<Json>(`${BASE}/credits`, body);\n return field<Json>(data, \"credits\", \"plan change\");\n}\n\nexport interface CreditQuote {\n amount_cents: number;\n bonus_percent: number;\n bonus_cents: number;\n credit_cents: number;\n surcharge_cents: number;\n total_cents: number;\n}\n\n/** Price a purchase as the purchase itself would be priced. */\nexport async function quoteCredits(\n client: BillingClient,\n amountCents: number,\n): Promise<CreditQuote> {\n const data = await client.get<Json>(`${BASE}/credits/quote`, {\n amount_cents: amountCents,\n });\n return field<CreditQuote>(data, \"quote\", \"quote\");\n}\n\n/** Everything a purchase sends back so the server can refuse it if the\n * price, the terms or the payment method changed since it was shown. */\nexport interface PreparedPurchase {\n quote: CreditQuote;\n termsVersion: string;\n paymentMethodKey: string | null;\n question: string;\n}\n\nexport function purchaseQuestion(\n quote: CreditQuote,\n paymentMethod: string,\n): string {\n const fee =\n quote.surcharge_cents > 0\n ? ` (${formatCents(quote.amount_cents)} plus a ${formatCents(quote.surcharge_cents)} credit card fee)`\n : \"\";\n return `Charge ${formatCents(quote.total_cents)}${fee} to ${paymentMethod} for ${formatCents(quote.credit_cents)} of credits (${quote.bonus_percent}% bonus)? This spends real money.`;\n}\n\nfunction describePaymentMethod(method: Json | null | undefined): string {\n const display = method?.display as Json | null | undefined;\n const last4 = display?.last4;\n const name = display?.brand ?? display?.bank_name;\n if (typeof last4 === \"string\") {\n return `${typeof name === \"string\" ? name : \"the saved method\"} ending ${last4}`;\n }\n return \"the saved payment method\";\n}\n\n/** Read the terms, the payment method and a fresh quote before a purchase. */\nexport async function prepareCreditPurchase(\n client: BillingClient,\n amountCents: number,\n): Promise<PreparedPurchase> {\n const credits = await showCredits(client);\n if (credits.purchasable === false) {\n throw new BillingError(\n \"This company can't buy credits now: there is no usable card or bank account on file.\",\n );\n }\n const terms = credits.purchase_terms as\n | { version?: unknown; text?: unknown }\n | undefined;\n if (typeof terms?.version !== \"string\") {\n throw new BillingError(\n \"The credits response didn't include the purchase terms.\",\n );\n }\n const [billing, quote] = await Promise.all([\n showBilling(client),\n quoteCredits(client, amountCents),\n ]);\n const method = billing.payment_method as Json | null | undefined;\n // A purchase names the terms version it agreed to, so the terms are part\n // of what is confirmed.\n const termsText =\n typeof terms.text === \"string\"\n ? `\\n\\nPurchase terms ${terms.version}:\\n${terms.text}`\n : \"\";\n return {\n quote,\n termsVersion: terms.version,\n paymentMethodKey: typeof method?.key === \"string\" ? method.key : null,\n question:\n purchaseQuestion(quote, describePaymentMethod(method)) + termsText,\n };\n}\n\n/**\n * The question for a retry under an earlier purchase key. Fluid answers a\n * known key with the purchase it already holds: it returns it, or resubmits\n * it as first made, with its own amount and its originally reserved payment\n * method. Only an unknown key makes the purchase described now.\n */\nexport function purchaseRetryQuestion(\n purchaseKey: string,\n newPurchaseQuestion: string,\n): string {\n return (\n `Retry purchase ${purchaseKey}? If Fluid already has a purchase under this key, it returns it, or resubmits it as first made: its original amount, to the payment method it reserved then, which may not be the one saved now. ` +\n `Only if Fluid has no purchase under this key does this make a new one: ${newPurchaseQuestion}`\n );\n}\n\n/** Buy what was prepared. The purchase key makes a retry charge once. */\nexport async function buyCredits(\n client: BillingClient,\n prepared: PreparedPurchase,\n purchaseKey: string,\n): Promise<Json> {\n const body: Json = {\n amount_cents: prepared.quote.amount_cents,\n expected_credit_cents: prepared.quote.credit_cents,\n expected_surcharge_cents: prepared.quote.surcharge_cents,\n purchase_key: purchaseKey,\n terms_version: prepared.termsVersion,\n };\n if (prepared.paymentMethodKey) {\n body.expected_payment_method_key = prepared.paymentMethodKey;\n }\n const data = await client.post<Json>(`${BASE}/credits/purchases`, body);\n return {\n purchase_key: purchaseKey,\n credit_purchase: field<Json>(data, \"credit_purchase\", \"purchase\"),\n balance_cents: data.balance_cents,\n duplicate: data.duplicate,\n };\n}\n\n/** What a charge's status means for whoever ran it. A purchase is pending\n * while it is reserved and processing while a bank debit settles (about 4\n * business days); failed, refunded and reversed charged nothing that\n * stands. */\nexport type ChargeOutcome =\n | { kind: \"succeeded\" }\n | { kind: \"pending\"; status: string; note: string }\n | {\n kind: \"declined\";\n status: string;\n message: string;\n reason: string | null;\n };\n\n/** Read a charge's outcome from the `credit_purchase` a buy or pay-now\n * returned. A status this CLI doesn't know is treated as not succeeded. */\nexport function chargeOutcome(\n purchase: Json | null | undefined,\n what: string,\n): ChargeOutcome {\n const status = typeof purchase?.status === \"string\" ? purchase.status : \"\";\n const reason =\n typeof purchase?.failure_message === \"string\" && purchase.failure_message\n ? purchase.failure_message\n : null;\n switch (status) {\n case \"succeeded\":\n return { kind: \"succeeded\" };\n case \"pending\":\n case \"processing\":\n return {\n kind: \"pending\",\n status,\n note: `The ${what} is ${status}: nothing is final yet. A bank debit takes about 4 business days to settle; check \\`fluid billing credits purchases\\` for the outcome.`,\n };\n case \"failed\":\n return {\n kind: \"declined\",\n status,\n reason,\n message: `The ${what} was declined${reason ? `: ${reason.replace(/\\.+$/, \"\")}` : \"\"}. Nothing was charged.`,\n };\n case \"refunded\":\n case \"reversed\":\n return {\n kind: \"declined\",\n status,\n reason,\n message: `The ${what} was ${status}; its money went back and it grants no credits.`,\n };\n default:\n return {\n kind: \"declined\",\n status,\n reason,\n message: `The ${what} came back with status \"${status || \"unknown\"}\", not succeeded. Check \\`fluid billing credits purchases\\`.`,\n };\n }\n}\n\nexport interface PayNowFigures {\n amount_cents: number;\n surcharge_cents: number;\n total_cents: number;\n available: boolean;\n reason: string | null;\n}\n\nexport function payNowQuestion(payNow: PayNowFigures): string {\n const fee =\n payNow.surcharge_cents > 0\n ? ` (the ${formatCents(payNow.amount_cents)} owed plus a ${formatCents(payNow.surcharge_cents)} credit card fee)`\n : \"\";\n return `Charge ${formatCents(payNow.total_cents)}${fee} to the saved payment method now for what the credits are overdrawn by? This spends real money.`;\n}\n\n/** Read what Pay now would charge, refusing early when it can't run. */\nexport async function preparePayNow(\n client: BillingClient,\n): Promise<{ payNow: PayNowFigures; question: string }> {\n const credits = await showCredits(client);\n const creditBilling = credits.credit_billing as\n | { pay_now?: PayNowFigures }\n | null\n | undefined;\n if (!creditBilling) {\n throw new BillingError(\"This company's billing is not paid from credits.\");\n }\n const payNow = creditBilling.pay_now;\n if (!payNow) {\n throw new BillingError(\"The credits response didn't include pay_now.\");\n }\n if (!payNow.available) {\n throw new BillingError(\n `Pay now can't run: ${payNow.reason ?? \"unavailable\"}. Nothing was charged.`,\n );\n }\n return { payNow, question: payNowQuestion(payNow) };\n}\n\n/** Charge the total that was shown; the server refuses any other. */\nexport async function payNow(\n client: BillingClient,\n expectedTotalCents: number,\n): Promise<Json> {\n return client.post<Json>(`${BASE}/credits/pay-now`, {\n expected_total_cents: expectedTotalCents,\n });\n}\n\n// ---------------------------------------------------------------------------\n// Spend limits and auto-reload\n\nexport async function showLimits(client: BillingClient): Promise<Json> {\n const data = await client.get<Json>(`${BASE}/usage-controls`);\n return field<Json>(data, \"usage_controls\", \"usage controls\");\n}\n\n/** Limits and reload figures in cents; null clears a value back to none or\n * Fluid's default. Left out keeps the current value. */\nexport interface LimitChanges {\n limitsEnabled?: boolean;\n autoReload?: boolean;\n ordersMonthly?: number | null;\n aiMonthly?: number | null;\n aiDaily?: number | null;\n hostingMonthly?: number | null;\n studiosMonthly?: number | null;\n studiosDaily?: number | null;\n reloadThreshold?: number | null;\n reloadAmount?: number | null;\n reloadDailyCap?: number | null;\n reloadMonthlyCap?: number | null;\n}\n\nconst LIMIT_FIELDS: Array<[keyof LimitChanges, string, string]> = [\n [\"limitsEnabled\", \"limits_enabled\", \"spend limits\"],\n [\"ordersMonthly\", \"orders_limit_cents\", \"orders monthly limit\"],\n [\"aiMonthly\", \"ai_limit_cents\", \"AI monthly limit\"],\n [\"aiDaily\", \"ai_daily_limit_cents\", \"AI daily limit\"],\n [\"hostingMonthly\", \"hosting_limit_cents\", \"hosting monthly limit\"],\n [\"studiosMonthly\", \"studios_limit_cents\", \"Studios monthly limit\"],\n [\"studiosDaily\", \"studios_daily_limit_cents\", \"Studios daily limit\"],\n [\"autoReload\", \"auto_reload\", \"auto-reload\"],\n [\"reloadThreshold\", \"auto_reload_threshold_cents\", \"reload threshold\"],\n [\"reloadAmount\", \"auto_reload_amount_cents\", \"reload amount\"],\n [\"reloadDailyCap\", \"auto_reload_daily_cap_cents\", \"reload daily cap\"],\n [\"reloadMonthlyCap\", \"auto_reload_monthly_cap_cents\", \"reload monthly cap\"],\n];\n\nfunction describe(value: unknown): string {\n if (typeof value === \"boolean\") return value ? \"on\" : \"off\";\n if (value === null) return \"none (default)\";\n return formatCents(value as number);\n}\n\n/**\n * The PATCH body and confirmation question for a change to the limits.\n *\n * Turning auto-reload on is consent to buy credits with the saved card\n * whenever the balance runs low, so it accepts the authorization in force\n * and names the four reload figures the question showed; the server\n * refuses the save (409) if they are no longer the figures it would store.\n */\nexport function limitsUpdate(\n current: Json,\n changes: LimitChanges,\n): { body: Json; question: string } {\n const body: Json = {};\n const lines: string[] = [];\n for (const [key, wire, label] of LIMIT_FIELDS) {\n const value = changes[key];\n if (value === undefined) continue;\n body[wire] = value;\n lines.push(`${label}: ${describe(value)}`);\n }\n if (lines.length === 0) {\n throw new BillingError(\"Nothing to change; pass at least one option.\");\n }\n let question = `Set ${lines.join(\"; \")}?`;\n\n if (changes.autoReload === true) {\n const auth = current.reload_authorization as\n | { version?: unknown; text?: unknown }\n | undefined;\n if (typeof auth?.version !== \"string\") {\n throw new BillingError(\n \"The usage controls didn't include the auto-reload authorization.\",\n );\n }\n const caps = (current.reload_caps ?? {}) as Json;\n const shown = {\n shown_reload_threshold_cents: pick(\n changes.reloadThreshold,\n current.reload_threshold_cents,\n \"--reload-threshold\",\n ),\n shown_reload_amount_cents: pick(\n changes.reloadAmount,\n current.reload_amount_cents,\n \"--reload-amount\",\n ),\n shown_reload_daily_cap_cents: pick(\n changes.reloadDailyCap,\n caps.daily_cents,\n \"--reload-daily-cap\",\n ),\n shown_reload_monthly_cap_cents: pick(\n changes.reloadMonthlyCap,\n caps.monthly_cents,\n \"--reload-monthly-cap\",\n ),\n };\n body.reload_authorization_version = auth.version;\n Object.assign(body, shown);\n question =\n `${question}\\nAuto-reload buys ${formatCents(shown.shown_reload_amount_cents)} of credits with the saved card ` +\n `whenever the spendable balance falls below ${formatCents(shown.shown_reload_threshold_cents)}, ` +\n `at most ${formatCents(shown.shown_reload_daily_cap_cents)} a day and ${formatCents(shown.shown_reload_monthly_cap_cents)} a month.` +\n (typeof auth.text === \"string\"\n ? `\\n\\nAuthorization ${auth.version}:\\n${auth.text}\\n\\nAccept it?`\n : \"\");\n }\n return { body, question };\n}\n\n/** A reload figure the consent names: the one being set, else the one in\n * force. \"none\" falls back to a default this CLI cannot see, so consent\n * with it is refused rather than given to an unseen figure. */\nfunction pick(\n changed: number | null | undefined,\n current: unknown,\n flag: string,\n): number {\n if (changed === null) {\n throw new BillingError(\n `Turning auto-reload on names the figures it consents to; give ${flag} a dollar amount instead of none.`,\n );\n }\n const value = changed ?? current;\n if (typeof value !== \"number\") {\n throw new BillingError(`The usage controls didn't include ${flag}.`);\n }\n return value;\n}\n\nexport async function setLimits(\n client: BillingClient,\n body: Json,\n): Promise<Json> {\n const data = await client.patch<Json>(`${BASE}/usage-controls`, body);\n return field<Json>(data, \"usage_controls\", \"usage controls update\");\n}\n\n// ---------------------------------------------------------------------------\n// Usage intake\n\nexport interface UsageEventInput {\n sku: string;\n quantity: string;\n occurredAt?: string;\n dedupKey: string;\n evidence?: string;\n}\n\n/** What the server's Time.iso8601 reads, with the zone required so a\n * timestamp never means the caller's local time by accident. */\nconst ISO8601 =\n /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}(:?\\d{2})?)$/;\n\n/** The request body for one usage event, validated as the server would. */\nexport function usageEventBody(\n input: UsageEventInput,\n now: () => Date = () => new Date(),\n): Json {\n const sku = input.sku.trim();\n if (!sku || sku.length > 255) {\n throw new BillingError(\"--sku must be 1 to 255 characters.\");\n }\n const dedupKey = input.dedupKey.trim();\n if (!dedupKey || dedupKey.length > 255) {\n throw new BillingError(\"--dedup-key must be 1 to 255 characters.\");\n }\n const raw = input.quantity.trim();\n // Integers past 2^53 would be rounded on their way into JSON.\n if (!/^\\d+$/.test(raw) || !Number.isSafeInteger(Number(raw))) {\n throw new BillingError(\n `--quantity must be a whole number from 0 to ${Number.MAX_SAFE_INTEGER}, not \"${input.quantity}\".`,\n );\n }\n const occurredAt = input.occurredAt?.trim() || now().toISOString();\n if (!ISO8601.test(occurredAt) || Number.isNaN(Date.parse(occurredAt))) {\n throw new BillingError(\n `--occurred-at must be an ISO 8601 timestamp such as 2026-10-03T14:00:00Z, not \"${input.occurredAt}\".`,\n );\n }\n const event: Json = {\n sku,\n quantity: Number(raw),\n occurred_at: occurredAt,\n dedup_key: dedupKey,\n };\n if (input.evidence !== undefined) {\n let evidence: unknown;\n try {\n evidence = JSON.parse(input.evidence);\n } catch {\n throw new BillingError(\"--evidence must be a JSON object.\");\n }\n if (!evidence || typeof evidence !== \"object\" || Array.isArray(evidence)) {\n throw new BillingError(\"--evidence must be a JSON object.\");\n }\n event.evidence = evidence;\n }\n return { usage_event: event };\n}\n\n/** Record one usage event. A repeat of its SKU and dedup key returns the\n * first event with duplicate: true and records nothing. */\nexport async function recordUsage(\n client: BillingClient,\n input: UsageEventInput,\n): Promise<Json> {\n const data = await client.post<Json>(USAGE_EVENTS, usageEventBody(input));\n return {\n usage_event: field<Json>(data, \"usage_event\", \"usage event\"),\n duplicate: data.duplicate,\n };\n}\n","import { spawn } from \"node:child_process\";\n\n// The same opener the channels plugin uses for `--open`, checked harder:\n// these links come from the API and are handed to the OS opener.\n\n/** Whether the opener ran: true when it finished cleanly, false when it\n * couldn't start or failed, \"attempted\" when it was still running after\n * the wait, so nothing is known either way. */\nexport type Opened = true | false | \"attempted\";\n\nexport interface OpenResult {\n opened: Opened;\n /** Why nothing was opened, when `opened` is false. */\n reason?: string;\n}\n\n/** How long to wait for the opener to finish before reporting \"attempted\". */\nconst OPENER_WAIT_MS = 3000;\n\n/**\n * Every link the billing API hands out is Stripe's: the hosted invoice page\n * (invoice.stripe.com) and the invoice and credit note PDFs\n * (pay.stripe.com). Anything else is refused rather than opened.\n */\nexport function isAllowedBillingUrl(raw: string): boolean {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n return false;\n }\n if (url.protocol !== \"https:\") return false;\n if (url.username || url.password || url.port) return false;\n const host = url.hostname.toLowerCase();\n return host === \"stripe.com\" || host.endsWith(\".stripe.com\");\n}\n\n/** Open a Stripe billing link in the default browser and report whether the\n * opener ran. Never rejects. */\nexport function openInBrowser(\n url: string,\n waitMs: number = OPENER_WAIT_MS,\n): Promise<OpenResult> {\n if (!isAllowedBillingUrl(url)) {\n return Promise.resolve({\n opened: false,\n reason: \"Not a Stripe https link; it was not opened.\",\n });\n }\n const [command, args] =\n process.platform === \"darwin\"\n ? [\"open\", [url]]\n : process.platform === \"win32\"\n ? // Not `cmd /c start`: cmd reads the `&` between query\n // parameters as a command separator and truncates the link.\n [\"rundll32\", [\"url.dll,FileProtocolHandler\", url]]\n : [\"xdg-open\", [url]];\n\n return new Promise<OpenResult>((resolve) => {\n let settled = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const settle = (result: OpenResult) => {\n if (settled) return;\n settled = true;\n if (timer) clearTimeout(timer);\n resolve(result);\n };\n\n let child: ReturnType<typeof spawn>;\n try {\n child = spawn(command as string, args as string[], {\n detached: true,\n stdio: \"ignore\",\n });\n } catch (err) {\n settle({ opened: false, reason: openerFailure(command as string, err) });\n return;\n }\n\n // ENOENT (no opener installed, as on a headless box) arrives here.\n child.on(\"error\", (err) =>\n settle({ opened: false, reason: openerFailure(command as string, err) }),\n );\n // xdg-open with no display, or no browser, exits non-zero.\n child.on(\"exit\", (code) =>\n settle(\n code === 0\n ? { opened: true }\n : {\n opened: false,\n reason: `${command as string} exited with code ${code ?? \"unknown\"}.`,\n },\n ),\n );\n // Started but still running: don't hold the CLI open for it.\n child.on(\"spawn\", () => {\n timer = setTimeout(() => {\n child.unref();\n settle({ opened: \"attempted\" });\n }, waitMs);\n });\n });\n}\n\nfunction openerFailure(command: string, err: unknown): string {\n const code = (err as { code?: unknown }).code;\n return code === \"ENOENT\"\n ? `No browser opener (${command}) was found.`\n : `${command} failed: ${err instanceof Error ? err.message : String(err)}`;\n}\n","import { randomUUID } from \"node:crypto\";\n\nimport type { Command } from \"commander\";\nimport prompts from \"prompts\";\nimport type { CommandContext, FluidPlugin } from \"@fluid-app/fluid-cli\";\nimport { createDomainCommand } from \"@fluid-app/fluid-cli\";\n\nimport {\n BillingError,\n CARD_FUNDING_PENDING_MESSAGE,\n buyCredits,\n chargeOutcome,\n choosePlan,\n documentLink,\n limitsUpdate,\n listCreditMovements,\n listCreditPurchases,\n listDocuments,\n listPayments,\n payNow,\n paymentInvoice,\n purchaseRetryQuestion,\n prepareCreditPurchase,\n preparePlanChoice,\n preparePayNow,\n purchaseInvoice,\n quoteCredits,\n recordUsage,\n setBillingEmail,\n setLimits,\n showBilling,\n showCredits,\n showLimits,\n showStanding,\n useBankAccount,\n type BillingClient,\n type LimitChanges,\n type PlanChoice,\n type StripeLink,\n} from \"./api.js\";\nimport { openInBrowser, type OpenResult } from \"./browser.js\";\nimport { parseDollars, parseDollarsOrNone } from \"./money.js\";\n\nasync function client(ctx: CommandContext): Promise<BillingClient> {\n return (await ctx.getClient()) as unknown as BillingClient;\n}\n\n/**\n * Changes to billing need an explicit yes. Scripts and agents pass `--yes`;\n * an interactive terminal is asked. With neither, nothing runs.\n */\nexport async function confirmChange(\n question: string,\n yes: boolean,\n interactive: boolean = Boolean(process.stdin.isTTY),\n): Promise<void> {\n if (yes) return;\n if (!interactive) {\n throw new BillingError(`${question} Pass --yes to confirm.`);\n }\n const answer = await prompts({\n type: \"confirm\",\n name: \"ok\",\n message: question,\n initial: false,\n });\n if (!answer.ok) throw new BillingError(\"Cancelled.\");\n}\n\n/** Print a failure as JSON on stderr and exit non-zero, so scripts and\n * agents can read it the same way as a success. The billing API names\n * why it refused in `error.details.error` (`details.type` for usage\n * intake); that comes through as `code`. */\nfunction fail(err: unknown): void {\n const status =\n typeof (err as { status?: unknown }).status === \"number\"\n ? (err as { status: number }).status\n : undefined;\n const body = (err as { body?: Record<string, unknown> | null }).body;\n const message =\n typeof body?.error_message === \"string\"\n ? body.error_message\n : err instanceof Error\n ? err.message\n : String(err);\n const details = (body?.error as { details?: Record<string, unknown> } | null)\n ?.details;\n const code =\n details?.error ?? details?.type ?? (err as { code?: unknown }).code;\n const readable =\n typeof code === \"string\" && status === 409\n ? CONFLICT_MESSAGES[code]\n : undefined;\n process.stderr.write(\n JSON.stringify({\n error: readable ?? message,\n ...(status ? { status } : {}),\n ...(typeof code === \"string\" ? { code } : {}),\n }) + \"\\n\",\n );\n process.exitCode = 1;\n}\n\n/** What a plan change's 409 means for whoever ran it. */\nconst CONFLICT_MESSAGES: Record<string, string> = {\n card_funding_pending: CARD_FUNDING_PENDING_MESSAGE,\n plan_charge_changed:\n \"The plan's monthly total changed since it was shown (another change was made meanwhile). Nothing was changed; run the command again to see the new total.\",\n payment_method_changed:\n \"The saved payment method changed while the plan was checked. Nothing was changed; run the command again.\",\n};\n\nfunction run(action: () => Promise<void>): Promise<void> {\n return action().catch(fail);\n}\n\nfunction limitOf(raw: string | undefined): number | undefined {\n if (raw === undefined) return undefined;\n if (!/^\\d+$/.test(raw.trim())) {\n throw new BillingError(`--limit must be a whole number, not \"${raw}\".`);\n }\n return Number(raw.trim());\n}\n\nfunction onOff(raw: string | undefined, flag: string): boolean | undefined {\n if (raw === undefined) return undefined;\n const value = raw.trim().toLowerCase();\n if (value === \"on\") return true;\n if (value === \"off\") return false;\n throw new BillingError(`${flag} is on or off, not \"${raw}\".`);\n}\n\n/** Print a fresh Stripe link and, with --open, open it. A credit note has\n * only a PDF. `opened` is true only when the opener finished cleanly,\n * \"attempted\" when it was still running, and false otherwise. */\nasync function showLink(\n ctx: CommandContext,\n id: string,\n link: StripeLink,\n open: boolean,\n): Promise<void> {\n const target = link.url || link.pdf_url;\n let result: OpenResult = { opened: false };\n if (open) {\n result = target\n ? await openInBrowser(target)\n : { opened: false, reason: \"The response had no link to open.\" };\n }\n ctx.output({\n id: Number(id),\n url: link.url,\n pdf_url: link.pdf_url,\n opened: result.opened,\n ...(result.reason ? { open_error: result.reason } : {}),\n note: \"Stripe links expire; run this again for a fresh one.\",\n });\n}\n\n/**\n * Print what a buy or pay-now returned. The API answers 201 whatever the\n * charge's outcome, so a declined card is read from the purchase's status:\n * the result is printed, then a JSON error on stderr and a non-zero exit.\n * A pending or processing charge exits 0 with a note saying so.\n */\nexport function reportCharge(\n ctx: CommandContext,\n result: Record<string, unknown>,\n what: string,\n): void {\n const outcome = chargeOutcome(\n result.credit_purchase as Record<string, unknown> | null | undefined,\n what,\n );\n if (outcome.kind === \"pending\") {\n ctx.output({ ...result, pending: true, note: outcome.note });\n return;\n }\n ctx.output(result);\n if (outcome.kind === \"declined\") {\n process.stderr.write(\n JSON.stringify({\n error: outcome.message,\n code:\n outcome.status === \"failed\"\n ? \"charge_declined\"\n : `charge_${outcome.status || \"unknown\"}`,\n charge_status: outcome.status || null,\n ...(outcome.reason ? { reason: outcome.reason } : {}),\n }) + \"\\n\",\n );\n process.exitCode = 1;\n }\n}\n\nconst LIMITS_NOTE =\n \"AI limits stop new AI work once reached; orders and hosting limits only notify and never stop anything; Studios limits are recorded and notified, not enforced.\";\n\nexport function registerBillingCommands(\n parent: Command,\n ctx: CommandContext,\n): void {\n parent\n .command(\"show\")\n .description(\n \"Show the billing page: invoices, upcoming charge, payment method and terms\",\n )\n .action(() =>\n run(async () => ctx.output(await showBilling(await client(ctx)))),\n );\n\n parent\n .command(\"update\")\n .description(\n \"Set where invoices and billing emails go (the only billing field a merchant sets)\",\n )\n .requiredOption(\n \"--email <address>\",\n 'Billing email address, or \"none\" to stop billing emails',\n )\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action((opts: { email: string; yes: boolean }) =>\n run(async () => {\n const email =\n opts.email.trim().toLowerCase() === \"none\" ? null : opts.email.trim();\n await confirmChange(\n email\n ? `Send this company's invoices and billing emails to ${email}?`\n : \"Clear the billing email? Fluid then sends no billing email until one is set.\",\n opts.yes,\n );\n ctx.output(await setBillingEmail(await client(ctx), email));\n }),\n );\n\n parent\n .command(\"standing\")\n .description(\n \"Show the company's billing standing (invoiced companies only)\",\n )\n .action(() =>\n run(async () => ctx.output(await showStanding(await client(ctx)))),\n );\n\n parent\n .command(\"instrument <bank-account-id>\")\n .description(\n \"Collect from a saved bank account whose ACH authorization was already given in the admin; a new account or card is set up in the admin\",\n )\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action((bankAccountId: string, opts: { yes: boolean }) =>\n run(async () => {\n await confirmChange(\n `Collect this company's billing from bank account ${bankAccountId}?`,\n opts.yes,\n );\n ctx.output(await useBankAccount(await client(ctx), bankAccountId));\n }),\n );\n\n const payments = parent\n .command(\"payments\")\n .description(\n \"List pulls from the company's bank account or card, newest first\",\n )\n .option(\"--limit <n>\", \"Only the newest n\")\n .action((opts: { limit?: string }) =>\n run(async () =>\n ctx.output(await listPayments(await client(ctx), limitOf(opts.limit))),\n ),\n );\n\n payments\n .command(\"invoice <id>\")\n .description(\"Get a fresh Stripe invoice link for one payment\")\n .option(\"--open\", \"Open the link in the default browser\", false)\n .action((id: string, opts: { open: boolean }) =>\n run(async () =>\n showLink(\n ctx,\n id,\n await paymentInvoice(await client(ctx), id),\n opts.open,\n ),\n ),\n );\n\n const documents = parent\n .command(\"documents\")\n .description(\n \"List Stripe documents (paid invoices and credit notes), newest first\",\n )\n .option(\"--limit <n>\", \"Only the newest n\")\n .action((opts: { limit?: string }) =>\n run(async () =>\n ctx.output(await listDocuments(await client(ctx), limitOf(opts.limit))),\n ),\n );\n\n documents\n .command(\"link <id>\")\n .description(\"Get a fresh Stripe link for one document\")\n .option(\"--open\", \"Open the link in the default browser\", false)\n .action((id: string, opts: { open: boolean }) =>\n run(async () =>\n showLink(ctx, id, await documentLink(await client(ctx), id), opts.open),\n ),\n );\n\n registerCreditCommands(parent, ctx);\n registerLimitCommands(parent, ctx);\n\n const usage = parent.command(\"usage\").description(\"Report billable usage\");\n\n usage\n .command(\"record\")\n .description(\n \"Record one usage event (needs billing.create_usage, with a company admin's or the company's own token; Droplet tokens are refused, and Mist AI usage goes through Mist)\",\n )\n .requiredOption(\"--sku <sku>\", \"Active billing product code\")\n .requiredOption(\"--quantity <n>\", \"Whole number of units\")\n .requiredOption(\n \"--dedup-key <key>\",\n \"Your id for this event; a repeat with the same SKU and key records nothing\",\n )\n .option(\n \"--occurred-at <iso8601>\",\n \"When it happened, with a zone (default: now)\",\n )\n .option(\"--evidence <json>\", \"JSON object stored with the event\")\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action(\n (opts: {\n sku: string;\n quantity: string;\n dedupKey: string;\n occurredAt?: string;\n evidence?: string;\n yes: boolean;\n }) =>\n run(async () => {\n await confirmChange(\n `Record ${opts.quantity} of ${opts.sku} as billable usage?`,\n opts.yes,\n );\n ctx.output(await recordUsage(await client(ctx), opts));\n }),\n );\n}\n\nfunction registerCreditCommands(parent: Command, ctx: CommandContext): void {\n const credits = parent\n .command(\"credits\")\n .description(\"Show the credit balance, the plan and what more credits cost\")\n .action(() =>\n run(async () => ctx.output(await showCredits(await client(ctx)))),\n );\n\n credits\n .command(\"plan [plan-key]\")\n .description(\n \"Choose the plan from the 1st of next month (invoiced companies only)\",\n )\n .option(\"--interval <interval>\", \"monthly or annual\")\n .option(\n \"--monthly-price <dollars>\",\n \"Size the plan to a monthly price between the catalog's\",\n )\n .option(\"--keep-current\", \"Drop the plan chosen for next month\", false)\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action(\n (\n planKey: string | undefined,\n opts: {\n interval?: \"monthly\" | \"annual\";\n monthlyPrice?: string;\n keepCurrent: boolean;\n yes: boolean;\n },\n ) =>\n run(async () => {\n let choice: PlanChoice;\n if (opts.keepCurrent) {\n if (planKey || opts.interval || opts.monthlyPrice) {\n throw new BillingError(\n \"--keep-current takes no plan, --interval or --monthly-price.\",\n );\n }\n choice = { keepCurrentPlan: true };\n } else {\n choice = {\n planKey,\n billingInterval: opts.interval,\n monthlyPriceCents:\n opts.monthlyPrice === undefined\n ? undefined\n : parseDollars(opts.monthlyPrice, {\n label: \"--monthly-price\",\n }),\n };\n }\n const api = await client(ctx);\n const { question, charge } = await preparePlanChoice(api, choice);\n await confirmChange(question, opts.yes);\n ctx.output(await choosePlan(api, choice, charge));\n }),\n );\n\n credits\n .command(\"movements\")\n .description(\"List every change to the credit balance, newest first\")\n .option(\"--limit <n>\", \"Only the newest n\")\n .action((opts: { limit?: string }) =>\n run(async () =>\n ctx.output(\n await listCreditMovements(await client(ctx), limitOf(opts.limit)),\n ),\n ),\n );\n\n credits\n .command(\"quote <dollars>\")\n .description(\n \"Price a purchase of credits: bonus, credits granted, card fee and total (whole dollars)\",\n )\n .action((dollars: string) =>\n run(async () =>\n ctx.output(\n await quoteCredits(\n await client(ctx),\n parseDollars(dollars, { wholeDollars: true }),\n ),\n ),\n ),\n );\n\n credits\n .command(\"buy <dollars>\")\n .description(\n \"Buy credits with the saved payment method (whole dollars; spends real money and accepts the purchase terms)\",\n )\n .option(\n \"--purchase-key <uuid>\",\n \"Retry an earlier buy without charging twice (printed by that buy)\",\n )\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action((dollars: string, opts: { purchaseKey?: string; yes: boolean }) =>\n run(async () => {\n const amountCents = parseDollars(dollars, { wholeDollars: true });\n // A key is generated only when none was given. A blank one (an unset\n // variable in a retry script) is refused: replacing it with a fresh\n // key would turn a retry into a second charge.\n const retrying = opts.purchaseKey !== undefined;\n const purchaseKey = retrying\n ? (opts.purchaseKey as string).trim()\n : randomUUID();\n if (retrying && !purchaseKey) {\n throw new BillingError(\n \"--purchase-key is empty. Pass the key printed by the earlier buy, or leave the flag out for a new purchase.\",\n );\n }\n if (!/^[0-9a-f-]{36}$/i.test(purchaseKey)) {\n throw new BillingError(\"--purchase-key must be a UUID.\");\n }\n const api = await client(ctx);\n const prepared = await prepareCreditPurchase(api, amountCents);\n await confirmChange(\n retrying\n ? purchaseRetryQuestion(purchaseKey, prepared.question)\n : prepared.question,\n opts.yes,\n );\n let bought: Record<string, unknown>;\n try {\n bought = await buyCredits(api, prepared, purchaseKey);\n } catch (err) {\n // Whether the charge went through can be unknown, so the key that\n // makes a retry charge once goes with the error.\n if (err instanceof Error) {\n err.message = `${err.message} (purchase key ${purchaseKey}; check \\`fluid billing credits purchases\\`, and retry with --purchase-key ${purchaseKey} rather than a new buy)`;\n const body = (err as { body?: { error_message?: unknown } | null })\n .body;\n if (body && typeof body.error_message === \"string\") {\n body.error_message = err.message;\n }\n }\n throw err;\n }\n reportCharge(ctx, bought, \"purchase\");\n }),\n );\n\n const purchases = credits\n .command(\"purchases\")\n .description(\"List credit purchases, newest first\")\n .option(\"--limit <n>\", \"Only the newest n\")\n .action((opts: { limit?: string }) =>\n run(async () =>\n ctx.output(\n await listCreditPurchases(await client(ctx), limitOf(opts.limit)),\n ),\n ),\n );\n\n purchases\n .command(\"invoice <id>\")\n .description(\"Get a fresh Stripe invoice link for one purchase\")\n .option(\"--open\", \"Open the link in the default browser\", false)\n .action((id: string, opts: { open: boolean }) =>\n run(async () =>\n showLink(\n ctx,\n id,\n await purchaseInvoice(await client(ctx), id),\n opts.open,\n ),\n ),\n );\n\n credits\n .command(\"pay-now\")\n .description(\n \"Charge the saved payment method now for what the credits are overdrawn by (spends real money)\",\n )\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action((opts: { yes: boolean }) =>\n run(async () => {\n const api = await client(ctx);\n const prepared = await preparePayNow(api);\n await confirmChange(prepared.question, opts.yes);\n reportCharge(\n ctx,\n await payNow(api, prepared.payNow.total_cents),\n \"payment\",\n );\n }),\n );\n}\n\nfunction registerLimitCommands(parent: Command, ctx: CommandContext): void {\n const limits = parent\n .command(\"limits\")\n .description(`Show spend limits and auto-reload. ${LIMITS_NOTE}`)\n .action(() =>\n run(async () => ctx.output(await showLimits(await client(ctx)))),\n );\n\n const dollars = \"<dollars|none>\";\n limits\n .command(\"set\")\n .description(\n `Change spend limits and auto-reload; options left out keep their value. Amounts are dollars; \"none\" clears a limit or restores Fluid's default reload figure. ${LIMITS_NOTE}`,\n )\n .option(\"--limits <on|off>\", \"Turn the spend limits on or off\")\n .option(`--ai-monthly ${dollars}`, \"Monthly AI limit\")\n .option(`--ai-daily ${dollars}`, \"Daily AI limit\")\n .option(`--studios-monthly ${dollars}`, \"Monthly Fluid Studios limit\")\n .option(`--studios-daily ${dollars}`, \"Daily Fluid Studios limit\")\n .option(`--orders-monthly ${dollars}`, \"Monthly orders limit (notifies)\")\n .option(`--hosting-monthly ${dollars}`, \"Monthly hosting limit (notifies)\")\n .option(\n \"--auto-reload <on|off>\",\n \"Buy credits with the saved card when the balance runs low; on accepts the reload authorization\",\n )\n .option(`--reload-threshold ${dollars}`, \"Reload below this balance\")\n .option(`--reload-amount ${dollars}`, \"What one reload buys\")\n .option(`--reload-daily-cap ${dollars}`, \"Most reloaded in a day\")\n .option(`--reload-monthly-cap ${dollars}`, \"Most reloaded in a month\")\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action((opts: Record<string, string | boolean | undefined>) =>\n run(async () => {\n const money = (key: string, whole = false) => {\n const raw = opts[key];\n return typeof raw === \"string\"\n ? parseDollarsOrNone(raw, {\n wholeDollars: whole,\n label: `--${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`,\n })\n : undefined;\n };\n const changes: LimitChanges = {\n limitsEnabled: onOff(opts.limits as string | undefined, \"--limits\"),\n aiMonthly: money(\"aiMonthly\"),\n aiDaily: money(\"aiDaily\"),\n studiosMonthly: money(\"studiosMonthly\"),\n studiosDaily: money(\"studiosDaily\"),\n ordersMonthly: money(\"ordersMonthly\"),\n hostingMonthly: money(\"hostingMonthly\"),\n autoReload: onOff(\n opts.autoReload as string | undefined,\n \"--auto-reload\",\n ),\n reloadThreshold: money(\"reloadThreshold\", true),\n reloadAmount: money(\"reloadAmount\", true),\n reloadDailyCap: money(\"reloadDailyCap\"),\n reloadMonthlyCap: money(\"reloadMonthlyCap\"),\n };\n const api = await client(ctx);\n // Only turning auto-reload on reads the controls in force.\n const current =\n changes.autoReload === true ? await showLimits(api) : {};\n const { body, question } = limitsUpdate(current, changes);\n await confirmChange(question, opts.yes === true);\n ctx.output(await setLimits(api, body));\n }),\n );\n}\n\nconst plugin: FluidPlugin = {\n name: \"@fluid-app/fluid-cli-billing\",\n version: \"0.1.0\",\n register(ctx) {\n ctx.program.addCommand(\n createDomainCommand(\n \"billing\",\n \"Read the company's billing and manage its credits and spend limits\",\n registerBillingCommands,\n ),\n );\n },\n};\n\nexport default plugin;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/money.ts","../src/api.ts","../src/browser.ts","../src/index.ts"],"sourcesContent":["// Amounts as the billing API speaks them: integer cents, never floats.\n//\n// The CLI takes amounts in dollars, the unit a merchant reads on the billing\n// page, and turns them into cents with string arithmetic so \"19.99\" is\n// exactly 1999. What the API returns is printed as it came; these helpers\n// only phrase a figure inside a confirmation question.\n\nexport class AmountError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AmountError\";\n }\n}\n\nconst DOLLARS = /^\\$?(\\d+)(?:\\.(\\d{1,2}))?$/;\n\n/**\n * Parse a dollar amount (\"250\", \"$250\", \"19.99\") into integer cents.\n * Rejects anything else — signs, commas, exponents, a third decimal — rather\n * than guessing. `wholeDollars` refuses cents, as purchases and reload\n * figures require.\n */\nexport function parseDollars(\n input: string,\n opts: { wholeDollars?: boolean; label?: string } = {},\n): number {\n const label = opts.label ?? \"Amount\";\n const match = DOLLARS.exec(input.trim());\n if (!match) {\n throw new AmountError(\n `${label} \"${input}\" isn't a dollar amount. Expected something like 250 or 19.99.`,\n );\n }\n const whole = match[1] ?? \"0\";\n const fraction = (match[2] ?? \"\").padEnd(2, \"0\");\n const cents = BigInt(whole) * 100n + BigInt(fraction || \"0\");\n if (cents <= 0n) {\n throw new AmountError(`${label} must be more than $0.`);\n }\n if (opts.wholeDollars && cents % 100n !== 0n) {\n throw new AmountError(`${label} must be whole dollars, not \"${input}\".`);\n }\n if (cents > BigInt(Number.MAX_SAFE_INTEGER)) {\n throw new AmountError(`${label} \"${input}\" is too large.`);\n }\n return Number(cents);\n}\n\n/** A dollar flag that also takes \"none\" to clear the value (sent as null). */\nexport function parseDollarsOrNone(\n input: string,\n opts: { wholeDollars?: boolean; label?: string } = {},\n): number | null {\n return input.trim().toLowerCase() === \"none\"\n ? null\n : parseDollars(input, opts);\n}\n\n/** Phrase integer cents as US dollars (\"$1,029.00\") without float math. */\nexport function formatCents(cents: number): string {\n if (!Number.isSafeInteger(cents)) {\n throw new AmountError(`Expected whole cents from the API, got ${cents}.`);\n }\n const sign = cents < 0 ? \"-\" : \"\";\n const digits = String(Math.abs(cents)).padStart(3, \"0\");\n const dollars = digits.slice(0, -2).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n return `${sign}$${dollars}.${digits.slice(-2)}`;\n}\n","// Calls to the merchant billing API (`/api/settings/billing`, settings-v0)\n// and to usage intake (`/api/v202604/billing/usage_events`).\n//\n// Every amount stays in the API's integer cents. Responses are returned as\n// the API sent them; only confirmation questions phrase figures in dollars.\n\nimport { formatCents } from \"./money.js\";\n\n/** The slice of the CLI's API client these calls use. */\nexport interface BillingClient {\n get<T>(endpoint: string, params?: Record<string, unknown>): Promise<T>;\n post<T>(endpoint: string, body?: unknown): Promise<T>;\n patch<T>(endpoint: string, body?: unknown): Promise<T>;\n}\n\nexport class BillingError extends Error {\n /** A machine-readable reason, printed as `code` like the API's. */\n readonly code?: string;\n\n constructor(message: string, code?: string) {\n super(message);\n this.name = \"BillingError\";\n this.code = code;\n }\n}\n\n/** Why a plan can't be chosen while the saved card's funding is unknown. */\nexport const CARD_FUNDING_PENDING_MESSAGE =\n \"The saved card's type is still being checked, so whether it carries the credit card fee isn't known yet. Nothing was changed; try again in a few minutes.\";\n\nconst BASE = \"/api/settings/billing\";\nconst USAGE_EVENTS = \"/api/v202604/billing/usage_events\";\n\ntype Json = Record<string, unknown>;\n\nfunction id(value: string): string {\n const trimmed = value.trim();\n if (!/^\\d+$/.test(trimmed)) {\n throw new BillingError(`Expected a numeric id, got \"${value}\".`);\n }\n return encodeURIComponent(trimmed);\n}\n\nfunction field<T>(data: Json | null | undefined, key: string, what: string): T {\n const value = data?.[key];\n if (value === undefined) {\n throw new BillingError(`The ${what} response didn't include \"${key}\".`);\n }\n return value as T;\n}\n\n// The billing page\n\nexport async function showBilling(client: BillingClient): Promise<Json> {\n return field<Json>(await client.get<Json>(BASE), \"billing\", \"billing\");\n}\n\n/** Set where invoices and billing emails go. null clears it, and Fluid then\n * sends no billing email until one is set. The server validates the\n * address. */\nexport async function setBillingEmail(\n client: BillingClient,\n email: string | null,\n): Promise<Json> {\n const address = email?.trim() || null;\n const data = await client.patch<Json>(BASE, { billing_email: address });\n return field<Json>(data, \"billing\", \"billing update\");\n}\n\nexport async function showStanding(client: BillingClient): Promise<Json> {\n const data = await client.get<Json>(`${BASE}/standing`);\n return field<Json>(data, \"standing\", \"standing\");\n}\n\n/** Move collections onto a saved bank account whose ACH authorization the\n * merchant already gave through instrument setup in the admin. */\nexport async function useBankAccount(\n client: BillingClient,\n bankAccountId: string,\n): Promise<Json> {\n const data = await client.patch<Json>(`${BASE}/instrument`, {\n instrument: { bank_account_id: Number(id(bankAccountId)) },\n });\n return field<Json>(data, \"billing\", \"instrument switch\");\n}\n\n// Cursor-paged lists\n\ninterface CursorPage {\n meta?: { pagination?: { next_cursor?: string | null } };\n}\n\n/** The list endpoints cap a page at 100 and default to 25, newest first. */\nconst PAGE_SIZE = 100;\nconst MAX_PAGES = 25;\n\n/**\n * Read every page of a list, newest first. `limit` stops after that many\n * items; without it the whole list is read, and one longer than\n * MAX_PAGES * PAGE_SIZE is refused rather than returned partially.\n */\nexport async function listAll(\n client: BillingClient,\n path: string,\n key: string,\n limit?: number,\n): Promise<Json[]> {\n if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1)) {\n throw new BillingError(\"--limit must be a whole number of at least 1.\");\n }\n const all: Json[] = [];\n let cursor: string | undefined;\n for (let page = 1; page <= MAX_PAGES; page += 1) {\n const want = limit === undefined ? PAGE_SIZE : limit - all.length;\n const data = await client.get<CursorPage & Json>(path, {\n page: { limit: Math.min(PAGE_SIZE, want), cursor },\n });\n const items = (data[key] as Json[] | null | undefined) ?? [];\n all.push(...items);\n const next = data.meta?.pagination?.next_cursor;\n if (limit !== undefined && all.length >= limit) {\n return all.slice(0, limit);\n }\n if (!next || items.length === 0) return all;\n cursor = next;\n }\n throw new BillingError(\n `There are more than ${MAX_PAGES * PAGE_SIZE} ${key.replace(/_/g, \" \")}; pass --limit to read the newest ones.`,\n );\n}\n\nexport const listPayments = (client: BillingClient, limit?: number) =>\n listAll(client, `${BASE}/payments`, \"payments\", limit);\n\nexport const listDocuments = (client: BillingClient, limit?: number) =>\n listAll(client, `${BASE}/documents`, \"documents\", limit);\n\nexport const listCreditMovements = (client: BillingClient, limit?: number) =>\n listAll(client, `${BASE}/credits/movements`, \"credit_movements\", limit);\n\nexport const listCreditPurchases = (client: BillingClient, limit?: number) =>\n listAll(client, `${BASE}/credits/purchases`, \"credit_purchases\", limit);\n\n// Stripe links. Stripe's links expire, so each is fetched when asked for.\n\nexport interface StripeLink {\n url: string;\n pdf_url: string | null;\n}\n\nasync function link(\n client: BillingClient,\n path: string,\n key: \"invoice\" | \"document\",\n): Promise<StripeLink> {\n return field<StripeLink>(await client.get<Json>(path), key, \"link\");\n}\n\nexport const paymentInvoice = async (\n client: BillingClient,\n paymentId: string,\n) => link(client, `${BASE}/payments/${id(paymentId)}/invoice`, \"invoice\");\n\nexport const documentLink = async (client: BillingClient, documentId: string) =>\n link(client, `${BASE}/documents/${id(documentId)}/link`, \"document\");\n\nexport const purchaseInvoice = async (\n client: BillingClient,\n purchaseId: string,\n) =>\n link(\n client,\n `${BASE}/credits/purchases/${id(purchaseId)}/invoice`,\n \"invoice\",\n );\n\n// Credits and the plan\n\nexport async function showCredits(client: BillingClient): Promise<Json> {\n return field<Json>(\n await client.get<Json>(`${BASE}/credits`),\n \"credits\",\n \"credits\",\n );\n}\n\nexport type PlanChoice =\n | { keepCurrentPlan: true }\n | {\n planKey?: string;\n billingInterval?: \"monthly\" | \"annual\";\n monthlyPriceCents?: number;\n };\n\n/** The PATCH body for a plan choice. A first plan needs a key or a price;\n * the server checks the key against its catalog. */\nexport function planChoiceBody(choice: PlanChoice): Json {\n if (\"keepCurrentPlan\" in choice) return { keep_current_plan: true };\n const body: Json = {};\n if (choice.planKey !== undefined) {\n const key = choice.planKey.trim().toLowerCase();\n if (!key) throw new BillingError(\"The plan key is empty.\");\n body.plan_key = key;\n }\n if (choice.billingInterval !== undefined) {\n if (![\"monthly\", \"annual\"].includes(choice.billingInterval)) {\n throw new BillingError(\n `--interval is monthly or annual, not \"${choice.billingInterval}\".`,\n );\n }\n body.billing_interval = choice.billingInterval;\n }\n if (choice.monthlyPriceCents !== undefined) {\n body.monthly_price_cents = choice.monthlyPriceCents;\n }\n if (Object.keys(body).length === 0) {\n throw new BillingError(\n \"Name a plan, a --monthly-price or --interval, or pass --keep-current.\",\n );\n }\n return body;\n}\n\n/** What a plan choice charges each month, as the server will price it. */\nexport interface PlanCharge {\n planKey: string | null;\n billingInterval: string;\n monthlyPriceCents: number;\n monthlyChargeCents: number;\n surchargeCents: number;\n totalCents: number;\n}\n\n/** The card networks' cap on a card fee, in basis points (Billing::CardSurcharge). */\nconst CARD_NETWORK_CAP_BASIS_POINTS = 300;\n\nfunction num(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isFinite(value)\n ? value\n : undefined;\n}\n\n/**\n * Price a plan choice the way the server prices it (Billing::CreditTermsManager\n * and Billing::CreditTermsResolver): the choice is laid over the plan already\n * waiting for next month, or the plan in force; a key alone costs its catalog\n * price; an annual plan's monthly charge takes the annual discount; and a\n * credit card adds its fee (Billing::CardSurcharge). Throws when the price\n * can't be told, so nothing is changed on a figure nobody saw.\n */\nexport function planCharge(credits: Json, choice: PlanChoice): PlanCharge {\n if (\"keepCurrentPlan\" in choice) {\n throw new BillingError(\"Keeping the current plan prices nothing new.\");\n }\n const base = (credits.next_plan ?? credits.plan) as Json | null | undefined;\n // The catalog at the price each key alone gives it, as the server sends it.\n const offered = Array.isArray(credits.catalog_plans)\n ? (credits.catalog_plans as Json[])\n : [];\n const key = choice.planKey?.trim().toLowerCase();\n const catalog = key ? offered.find((plan) => plan.key === key) : undefined;\n if (key && !catalog) {\n const keys = offered.map((plan) => plan.key).filter(Boolean);\n throw new BillingError(\n `Plan \"${key}\" isn't offered to this company${keys.length ? `; choose one of ${keys.join(\", \")}` : \"\"}. Nothing was changed.`,\n );\n }\n const price =\n choice.monthlyPriceCents ??\n num(catalog?.monthly_price_cents) ??\n num(base?.monthly_price_cents);\n if (price === undefined) {\n throw new BillingError(\n \"Can't tell what this plan would cost each month: name a plan or a --monthly-price. Nothing was changed.\",\n );\n }\n const interval =\n choice.billingInterval ??\n (typeof base?.billing_interval === \"string\"\n ? base.billing_interval\n : \"monthly\");\n const discount = num(credits.annual_discount_percent) ?? 0;\n const charge =\n interval === \"annual\"\n ? Math.round((price * (100 - discount)) / 100)\n : price;\n\n const surcharge = credits.card_surcharge as Json | null | undefined;\n let fee = 0;\n if (\n catalog &&\n interval === \"monthly\" &&\n choice.monthlyPriceCents === undefined &&\n num(catalog.monthly_surcharge_cents) !== undefined\n ) {\n // The server priced this one already.\n fee = catalog.monthly_surcharge_cents as number;\n } else if (surcharge?.applies === true) {\n const rate = num(surcharge.rate_basis_points);\n if (rate === undefined) {\n throw new BillingError(\n \"The credits response didn't include the credit card fee rate. Nothing was changed.\",\n );\n }\n // Billing::CardSurcharge.cents_for: the rate rounded half up, held to\n // the fee's own cap and, on a card, to the networks' 3% rounded down.\n const caps = [Math.floor((charge * rate + 5000) / 10000)];\n const cap = num(surcharge.cap_cents);\n if (cap !== undefined) caps.push(cap);\n if (surcharge.kind !== \"ach\") {\n caps.push(Math.floor((charge * CARD_NETWORK_CAP_BASIS_POINTS) / 10000));\n }\n fee = Math.min(...caps);\n }\n return {\n planKey: key ?? (typeof base?.key === \"string\" ? base.key : null),\n billingInterval: interval,\n monthlyPriceCents: price,\n monthlyChargeCents: charge,\n surchargeCents: fee,\n totalCents: charge + fee,\n };\n}\n\nexport function planChoiceQuestion(\n choice: PlanChoice,\n charge?: PlanCharge,\n current?: Json | null,\n): string {\n if (\"keepCurrentPlan\" in choice) {\n const total = num(current?.monthly_total_cents);\n return `Keep the current plan${total !== undefined ? ` (${formatCents(total)} a month)` : \"\"} and drop the plan chosen for next month?`;\n }\n if (!charge) {\n throw new BillingError(\"A plan change needs its monthly charge priced.\");\n }\n const name = charge.planKey ? `plan ${charge.planKey}` : \"a plan\";\n const discount =\n charge.monthlyChargeCents !== charge.monthlyPriceCents\n ? `, ${formatCents(charge.monthlyPriceCents)} less the annual discount`\n : \"\";\n const fee =\n charge.surchargeCents > 0\n ? ` (${formatCents(charge.monthlyChargeCents)} plus a ${formatCents(charge.surchargeCents)} credit card fee)`\n : \"\";\n return `Change to ${name}, billed ${charge.billingInterval}${discount}, from the 1st of next month? It charges ${formatCents(charge.totalCents)}${fee} to the saved payment method each month.`;\n}\n\n/** Read the credits and price a plan choice before asking about it. */\nexport async function preparePlanChoice(\n client: BillingClient,\n choice: PlanChoice,\n): Promise<{ question: string; charge?: PlanCharge }> {\n planChoiceBody(choice); // refuse a malformed choice before reading\n const credits = await showCredits(client);\n if (\"keepCurrentPlan\" in choice) {\n return {\n question: planChoiceQuestion(\n choice,\n undefined,\n credits.plan as Json | null,\n ),\n };\n }\n // The server refuses a plan until the card's fee is known (409\n // card_funding_pending); say so before asking about a total.\n const surcharge = credits.card_surcharge as Json | null | undefined;\n if (surcharge?.funding_pending === true) {\n throw new BillingError(\n CARD_FUNDING_PENDING_MESSAGE,\n \"card_funding_pending\",\n );\n }\n const charge = planCharge(credits, choice);\n return { question: planChoiceQuestion(choice, charge), charge };\n}\n\n/** Make a plan choice. With the monthly charge and total that were shown,\n * the server refuses (409 plan_charge_changed) a choice that would now\n * charge or total anything else, as when another admin chose a plan or\n * switched the saved payment method to a card meanwhile. */\nexport async function choosePlan(\n client: BillingClient,\n choice: PlanChoice,\n shown?: Pick<PlanCharge, \"monthlyChargeCents\" | \"totalCents\">,\n): Promise<Json> {\n const body = planChoiceBody(choice);\n if (shown && !(\"keepCurrentPlan\" in choice)) {\n body.expected_monthly_charge_cents = shown.monthlyChargeCents;\n body.expected_monthly_total_cents = shown.totalCents;\n }\n const data = await client.patch<Json>(`${BASE}/credits`, body);\n return field<Json>(data, \"credits\", \"plan change\");\n}\n\nexport interface CreditQuote {\n amount_cents: number;\n bonus_percent: number;\n bonus_cents: number;\n credit_cents: number;\n surcharge_cents: number;\n total_cents: number;\n}\n\n/** Price a purchase as the purchase itself would be priced. */\nexport async function quoteCredits(\n client: BillingClient,\n amountCents: number,\n): Promise<CreditQuote> {\n const data = await client.get<Json>(`${BASE}/credits/quote`, {\n amount_cents: amountCents,\n });\n return field<CreditQuote>(data, \"quote\", \"quote\");\n}\n\n/** Everything a purchase sends back so the server can refuse it if the\n * price, the terms or the payment method changed since it was shown. */\nexport interface PreparedPurchase {\n quote: CreditQuote;\n termsVersion: string;\n paymentMethodKey: string | null;\n question: string;\n}\n\nexport function purchaseQuestion(\n quote: CreditQuote,\n paymentMethod: string,\n): string {\n const fee =\n quote.surcharge_cents > 0\n ? ` (${formatCents(quote.amount_cents)} plus a ${formatCents(quote.surcharge_cents)} credit card fee)`\n : \"\";\n return `Charge ${formatCents(quote.total_cents)}${fee} to ${paymentMethod} for ${formatCents(quote.credit_cents)} of credits (${quote.bonus_percent}% bonus)? This spends real money.`;\n}\n\nfunction describePaymentMethod(method: Json | null | undefined): string {\n const display = method?.display as Json | null | undefined;\n const last4 = display?.last4;\n const name = display?.brand ?? display?.bank_name;\n if (typeof last4 === \"string\") {\n return `${typeof name === \"string\" ? name : \"the saved method\"} ending ${last4}`;\n }\n return \"the saved payment method\";\n}\n\n/** Read the terms, the payment method and a fresh quote before a purchase. */\nexport async function prepareCreditPurchase(\n client: BillingClient,\n amountCents: number,\n): Promise<PreparedPurchase> {\n const credits = await showCredits(client);\n if (credits.purchasable === false) {\n throw new BillingError(\n \"This company can't buy credits now: there is no usable card or bank account on file.\",\n );\n }\n const terms = credits.purchase_terms as\n | { version?: unknown; text?: unknown }\n | undefined;\n if (typeof terms?.version !== \"string\") {\n throw new BillingError(\n \"The credits response didn't include the purchase terms.\",\n );\n }\n const [billing, quote] = await Promise.all([\n showBilling(client),\n quoteCredits(client, amountCents),\n ]);\n const method = billing.payment_method as Json | null | undefined;\n // A purchase names the terms version it agreed to, so the terms are part\n // of what is confirmed.\n const termsText =\n typeof terms.text === \"string\"\n ? `\\n\\nPurchase terms ${terms.version}:\\n${terms.text}`\n : \"\";\n return {\n quote,\n termsVersion: terms.version,\n paymentMethodKey: typeof method?.key === \"string\" ? method.key : null,\n question:\n purchaseQuestion(quote, describePaymentMethod(method)) + termsText,\n };\n}\n\n/**\n * The question for a retry under an earlier purchase key. Fluid answers a\n * known key with the purchase it already holds: it returns it, or resubmits\n * it as first made, with its own amount and its originally reserved payment\n * method. Only an unknown key makes the purchase described now.\n */\nexport function purchaseRetryQuestion(\n purchaseKey: string,\n newPurchaseQuestion: string,\n): string {\n return (\n `Retry purchase ${purchaseKey}? If Fluid already has a purchase under this key, it returns it, or resubmits it as first made: its original amount, to the payment method it reserved then, which may not be the one saved now. ` +\n `Only if Fluid has no purchase under this key does this make a new one: ${newPurchaseQuestion}`\n );\n}\n\n/** Buy what was prepared. The purchase key makes a retry charge once. */\nexport async function buyCredits(\n client: BillingClient,\n prepared: PreparedPurchase,\n purchaseKey: string,\n): Promise<Json> {\n const body: Json = {\n amount_cents: prepared.quote.amount_cents,\n expected_credit_cents: prepared.quote.credit_cents,\n expected_surcharge_cents: prepared.quote.surcharge_cents,\n purchase_key: purchaseKey,\n terms_version: prepared.termsVersion,\n };\n if (prepared.paymentMethodKey) {\n body.expected_payment_method_key = prepared.paymentMethodKey;\n }\n const data = await client.post<Json>(`${BASE}/credits/purchases`, body);\n return {\n purchase_key: purchaseKey,\n credit_purchase: field<Json>(data, \"credit_purchase\", \"purchase\"),\n balance_cents: data.balance_cents,\n duplicate: data.duplicate,\n };\n}\n\n/** What a charge's status means for whoever ran it. A purchase is pending\n * while it is reserved and processing while a bank debit settles (about 4\n * business days); failed, refunded and reversed charged nothing that\n * stands. */\nexport type ChargeOutcome =\n | { kind: \"succeeded\" }\n | { kind: \"pending\"; status: string; note: string }\n | {\n kind: \"declined\";\n status: string;\n message: string;\n reason: string | null;\n };\n\n/** Read a charge's outcome from the `credit_purchase` a buy or pay-now\n * returned. A status this CLI doesn't know is treated as not succeeded. */\nexport function chargeOutcome(\n purchase: Json | null | undefined,\n what: string,\n): ChargeOutcome {\n const status = typeof purchase?.status === \"string\" ? purchase.status : \"\";\n const reason =\n typeof purchase?.failure_message === \"string\" && purchase.failure_message\n ? purchase.failure_message\n : null;\n switch (status) {\n case \"succeeded\":\n return { kind: \"succeeded\" };\n case \"pending\":\n case \"processing\":\n return {\n kind: \"pending\",\n status,\n note: `The ${what} is ${status}: nothing is final yet. A bank debit takes about 4 business days to settle; check \\`fluid billing credits purchases\\` for the outcome.`,\n };\n case \"failed\":\n return {\n kind: \"declined\",\n status,\n reason,\n message: `The ${what} was declined${reason ? `: ${reason.replace(/\\.+$/, \"\")}` : \"\"}. Nothing was charged.`,\n };\n case \"refunded\":\n case \"reversed\":\n return {\n kind: \"declined\",\n status,\n reason,\n message: `The ${what} was ${status}; its money went back and it grants no credits.`,\n };\n default:\n return {\n kind: \"declined\",\n status,\n reason,\n message: `The ${what} came back with status \"${status || \"unknown\"}\", not succeeded. Check \\`fluid billing credits purchases\\`.`,\n };\n }\n}\n\nexport interface PayNowFigures {\n amount_cents: number;\n surcharge_cents: number;\n total_cents: number;\n available: boolean;\n reason: string | null;\n}\n\nexport function payNowQuestion(payNow: PayNowFigures): string {\n const fee =\n payNow.surcharge_cents > 0\n ? ` (the ${formatCents(payNow.amount_cents)} owed plus a ${formatCents(payNow.surcharge_cents)} credit card fee)`\n : \"\";\n return `Charge ${formatCents(payNow.total_cents)}${fee} to the saved payment method now for what the credits are overdrawn by? This spends real money.`;\n}\n\n/** Read what Pay now would charge, refusing early when it can't run. */\nexport async function preparePayNow(\n client: BillingClient,\n): Promise<{ payNow: PayNowFigures; question: string }> {\n const credits = await showCredits(client);\n const creditBilling = credits.credit_billing as\n | { pay_now?: PayNowFigures }\n | null\n | undefined;\n if (!creditBilling) {\n throw new BillingError(\"This company's billing is not paid from credits.\");\n }\n const payNow = creditBilling.pay_now;\n if (!payNow) {\n throw new BillingError(\"The credits response didn't include pay_now.\");\n }\n if (!payNow.available) {\n throw new BillingError(\n `Pay now can't run: ${payNow.reason ?? \"unavailable\"}. Nothing was charged.`,\n );\n }\n return { payNow, question: payNowQuestion(payNow) };\n}\n\n/** Charge the total that was shown; the server refuses any other. */\nexport async function payNow(\n client: BillingClient,\n expectedTotalCents: number,\n): Promise<Json> {\n return client.post<Json>(`${BASE}/credits/pay-now`, {\n expected_total_cents: expectedTotalCents,\n });\n}\n\n// Spend limits and auto-reload\n\nexport async function showLimits(client: BillingClient): Promise<Json> {\n const data = await client.get<Json>(`${BASE}/usage-controls`);\n return field<Json>(data, \"usage_controls\", \"usage controls\");\n}\n\n/** Limits and reload figures in cents; null clears a value back to none or\n * Fluid's default. Left out keeps the current value. */\nexport interface LimitChanges {\n limitsEnabled?: boolean;\n autoReload?: boolean;\n ordersMonthly?: number | null;\n aiMonthly?: number | null;\n aiDaily?: number | null;\n hostingMonthly?: number | null;\n studiosMonthly?: number | null;\n studiosDaily?: number | null;\n reloadThreshold?: number | null;\n reloadAmount?: number | null;\n reloadDailyCap?: number | null;\n reloadMonthlyCap?: number | null;\n}\n\nconst LIMIT_FIELDS: Array<[keyof LimitChanges, string, string]> = [\n [\"limitsEnabled\", \"limits_enabled\", \"spend limits\"],\n [\"ordersMonthly\", \"orders_limit_cents\", \"orders monthly limit\"],\n [\"aiMonthly\", \"ai_limit_cents\", \"AI monthly limit\"],\n [\"aiDaily\", \"ai_daily_limit_cents\", \"AI daily limit\"],\n [\"hostingMonthly\", \"hosting_limit_cents\", \"hosting monthly limit\"],\n [\"studiosMonthly\", \"studios_limit_cents\", \"Studios monthly limit\"],\n [\"studiosDaily\", \"studios_daily_limit_cents\", \"Studios daily limit\"],\n [\"autoReload\", \"auto_reload\", \"auto-reload\"],\n [\"reloadThreshold\", \"auto_reload_threshold_cents\", \"reload threshold\"],\n [\"reloadAmount\", \"auto_reload_amount_cents\", \"reload amount\"],\n [\"reloadDailyCap\", \"auto_reload_daily_cap_cents\", \"reload daily cap\"],\n [\"reloadMonthlyCap\", \"auto_reload_monthly_cap_cents\", \"reload monthly cap\"],\n];\n\nfunction describe(value: unknown): string {\n if (typeof value === \"boolean\") return value ? \"on\" : \"off\";\n if (value === null) return \"none (default)\";\n return formatCents(value as number);\n}\n\n/**\n * The PATCH body and confirmation question for a change to the limits.\n *\n * Turning auto-reload on is consent to buy credits with the saved card\n * whenever the balance runs low, so it accepts the authorization in force\n * and names the four reload figures the question showed; the server\n * refuses the save (409) if they are no longer the figures it would store.\n */\nexport function limitsUpdate(\n current: Json,\n changes: LimitChanges,\n): { body: Json; question: string } {\n const body: Json = {};\n const lines: string[] = [];\n for (const [key, wire, label] of LIMIT_FIELDS) {\n const value = changes[key];\n if (value === undefined) continue;\n body[wire] = value;\n lines.push(`${label}: ${describe(value)}`);\n }\n if (lines.length === 0) {\n throw new BillingError(\"Nothing to change; pass at least one option.\");\n }\n let question = `Set ${lines.join(\"; \")}?`;\n\n if (changes.autoReload === true) {\n const auth = current.reload_authorization as\n | { version?: unknown; text?: unknown }\n | undefined;\n if (typeof auth?.version !== \"string\") {\n throw new BillingError(\n \"The usage controls didn't include the auto-reload authorization.\",\n );\n }\n const caps = (current.reload_caps ?? {}) as Json;\n const shown = {\n shown_reload_threshold_cents: pick(\n changes.reloadThreshold,\n current.reload_threshold_cents,\n \"--reload-threshold\",\n ),\n shown_reload_amount_cents: pick(\n changes.reloadAmount,\n current.reload_amount_cents,\n \"--reload-amount\",\n ),\n shown_reload_daily_cap_cents: pick(\n changes.reloadDailyCap,\n caps.daily_cents,\n \"--reload-daily-cap\",\n ),\n shown_reload_monthly_cap_cents: pick(\n changes.reloadMonthlyCap,\n caps.monthly_cents,\n \"--reload-monthly-cap\",\n ),\n };\n body.reload_authorization_version = auth.version;\n Object.assign(body, shown);\n question =\n `${question}\\nAuto-reload buys ${formatCents(shown.shown_reload_amount_cents)} of credits with the saved card ` +\n `whenever the spendable balance falls below ${formatCents(shown.shown_reload_threshold_cents)}, ` +\n `at most ${formatCents(shown.shown_reload_daily_cap_cents)} a day and ${formatCents(shown.shown_reload_monthly_cap_cents)} a month.` +\n (typeof auth.text === \"string\"\n ? `\\n\\nAuthorization ${auth.version}:\\n${auth.text}\\n\\nAccept it?`\n : \"\");\n }\n return { body, question };\n}\n\n/** A reload figure the consent names: the one being set, else the one in\n * force. \"none\" falls back to a default this CLI cannot see, so consent\n * with it is refused rather than given to an unseen figure. */\nfunction pick(\n changed: number | null | undefined,\n current: unknown,\n flag: string,\n): number {\n if (changed === null) {\n throw new BillingError(\n `Turning auto-reload on names the figures it consents to; give ${flag} a dollar amount instead of none.`,\n );\n }\n const value = changed ?? current;\n if (typeof value !== \"number\") {\n throw new BillingError(`The usage controls didn't include ${flag}.`);\n }\n return value;\n}\n\nexport async function setLimits(\n client: BillingClient,\n body: Json,\n): Promise<Json> {\n const data = await client.patch<Json>(`${BASE}/usage-controls`, body);\n return field<Json>(data, \"usage_controls\", \"usage controls update\");\n}\n\n// Usage intake\n\nexport interface UsageEventInput {\n sku: string;\n quantity: string;\n occurredAt?: string;\n dedupKey: string;\n evidence?: string;\n}\n\n/** What the server's Time.iso8601 reads, with the zone required so a\n * timestamp never means the caller's local time by accident. */\nconst ISO8601 =\n /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}(:?\\d{2})?)$/;\n\n/** The request body for one usage event, validated as the server would. */\nexport function usageEventBody(\n input: UsageEventInput,\n now: () => Date = () => new Date(),\n): Json {\n const sku = input.sku.trim();\n if (!sku || sku.length > 255) {\n throw new BillingError(\"--sku must be 1 to 255 characters.\");\n }\n const dedupKey = input.dedupKey.trim();\n if (!dedupKey || dedupKey.length > 255) {\n throw new BillingError(\"--dedup-key must be 1 to 255 characters.\");\n }\n const raw = input.quantity.trim();\n // Integers past 2^53 would be rounded on their way into JSON.\n if (!/^\\d+$/.test(raw) || !Number.isSafeInteger(Number(raw))) {\n throw new BillingError(\n `--quantity must be a whole number from 0 to ${Number.MAX_SAFE_INTEGER}, not \"${input.quantity}\".`,\n );\n }\n const occurredAt = input.occurredAt?.trim() || now().toISOString();\n if (!ISO8601.test(occurredAt) || Number.isNaN(Date.parse(occurredAt))) {\n throw new BillingError(\n `--occurred-at must be an ISO 8601 timestamp such as 2026-10-03T14:00:00Z, not \"${input.occurredAt}\".`,\n );\n }\n const event: Json = {\n sku,\n quantity: Number(raw),\n occurred_at: occurredAt,\n dedup_key: dedupKey,\n };\n if (input.evidence !== undefined) {\n let evidence: unknown;\n try {\n evidence = JSON.parse(input.evidence);\n } catch {\n throw new BillingError(\"--evidence must be a JSON object.\");\n }\n if (!evidence || typeof evidence !== \"object\" || Array.isArray(evidence)) {\n throw new BillingError(\"--evidence must be a JSON object.\");\n }\n event.evidence = evidence;\n }\n return { usage_event: event };\n}\n\n/** Record one usage event. A repeat of its SKU and dedup key returns the\n * first event with duplicate: true and records nothing. */\nexport async function recordUsage(\n client: BillingClient,\n input: UsageEventInput,\n): Promise<Json> {\n const data = await client.post<Json>(USAGE_EVENTS, usageEventBody(input));\n return {\n usage_event: field<Json>(data, \"usage_event\", \"usage event\"),\n duplicate: data.duplicate,\n };\n}\n","import { spawn } from \"node:child_process\";\n\n// The same opener the channels plugin uses for `--open`, checked harder:\n// these links come from the API and are handed to the OS opener.\n\n/** Whether the opener ran: true when it finished cleanly, false when it\n * couldn't start or failed, \"attempted\" when it was still running after\n * the wait, so nothing is known either way. */\nexport type Opened = true | false | \"attempted\";\n\nexport interface OpenResult {\n opened: Opened;\n /** Why nothing was opened, when `opened` is false. */\n reason?: string;\n}\n\n/** How long to wait for the opener to finish before reporting \"attempted\". */\nconst OPENER_WAIT_MS = 3000;\n\n/**\n * Every link the billing API hands out is Stripe's: the hosted invoice page\n * (invoice.stripe.com) and the invoice and credit note PDFs\n * (pay.stripe.com). Anything else is refused rather than opened.\n */\nexport function isAllowedBillingUrl(raw: string): boolean {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n return false;\n }\n if (url.protocol !== \"https:\") return false;\n if (url.username || url.password || url.port) return false;\n const host = url.hostname.toLowerCase();\n return host === \"stripe.com\" || host.endsWith(\".stripe.com\");\n}\n\n/** Open a Stripe billing link in the default browser and report whether the\n * opener ran. Never rejects. */\nexport function openInBrowser(\n url: string,\n waitMs: number = OPENER_WAIT_MS,\n): Promise<OpenResult> {\n if (!isAllowedBillingUrl(url)) {\n return Promise.resolve({\n opened: false,\n reason: \"Not a Stripe https link; it was not opened.\",\n });\n }\n const [command, args] =\n process.platform === \"darwin\"\n ? [\"open\", [url]]\n : process.platform === \"win32\"\n ? // Not `cmd /c start`: cmd reads the `&` between query\n // parameters as a command separator and truncates the link.\n [\"rundll32\", [\"url.dll,FileProtocolHandler\", url]]\n : [\"xdg-open\", [url]];\n\n return new Promise<OpenResult>((resolve) => {\n let settled = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const settle = (result: OpenResult) => {\n if (settled) return;\n settled = true;\n if (timer) clearTimeout(timer);\n resolve(result);\n };\n\n let child: ReturnType<typeof spawn>;\n try {\n child = spawn(command as string, args as string[], {\n detached: true,\n stdio: \"ignore\",\n });\n } catch (err) {\n settle({ opened: false, reason: openerFailure(command as string, err) });\n return;\n }\n\n // ENOENT (no opener installed, as on a headless box) arrives here.\n child.on(\"error\", (err) =>\n settle({ opened: false, reason: openerFailure(command as string, err) }),\n );\n // xdg-open with no display, or no browser, exits non-zero.\n child.on(\"exit\", (code) =>\n settle(\n code === 0\n ? { opened: true }\n : {\n opened: false,\n reason: `${command as string} exited with code ${code ?? \"unknown\"}.`,\n },\n ),\n );\n // Started but still running: don't hold the CLI open for it.\n child.on(\"spawn\", () => {\n timer = setTimeout(() => {\n child.unref();\n settle({ opened: \"attempted\" });\n }, waitMs);\n });\n });\n}\n\nfunction openerFailure(command: string, err: unknown): string {\n const code = (err as { code?: unknown }).code;\n return code === \"ENOENT\"\n ? `No browser opener (${command}) was found.`\n : `${command} failed: ${err instanceof Error ? err.message : String(err)}`;\n}\n","import { randomUUID } from \"node:crypto\";\n\nimport type { Command } from \"commander\";\nimport prompts from \"prompts\";\nimport type { CommandContext, FluidPlugin } from \"@fluid-app/fluid-cli\";\nimport { createDomainCommand } from \"@fluid-app/fluid-cli\";\n\nimport {\n BillingError,\n CARD_FUNDING_PENDING_MESSAGE,\n buyCredits,\n chargeOutcome,\n choosePlan,\n documentLink,\n limitsUpdate,\n listCreditMovements,\n listCreditPurchases,\n listDocuments,\n listPayments,\n payNow,\n paymentInvoice,\n purchaseRetryQuestion,\n prepareCreditPurchase,\n preparePlanChoice,\n preparePayNow,\n purchaseInvoice,\n quoteCredits,\n recordUsage,\n setBillingEmail,\n setLimits,\n showBilling,\n showCredits,\n showLimits,\n showStanding,\n useBankAccount,\n type BillingClient,\n type LimitChanges,\n type PlanChoice,\n type StripeLink,\n} from \"./api.js\";\nimport { openInBrowser, type OpenResult } from \"./browser.js\";\nimport { parseDollars, parseDollarsOrNone } from \"./money.js\";\n\nasync function client(ctx: CommandContext): Promise<BillingClient> {\n return (await ctx.getClient()) as unknown as BillingClient;\n}\n\n/**\n * Changes to billing need an explicit yes. Scripts and agents pass `--yes`;\n * an interactive terminal is asked. With neither, nothing runs.\n */\nexport async function confirmChange(\n question: string,\n yes: boolean,\n interactive: boolean = Boolean(process.stdin.isTTY),\n): Promise<void> {\n if (yes) return;\n if (!interactive) {\n throw new BillingError(`${question} Pass --yes to confirm.`);\n }\n const answer = await prompts({\n type: \"confirm\",\n name: \"ok\",\n message: question,\n initial: false,\n });\n if (!answer.ok) throw new BillingError(\"Cancelled.\");\n}\n\n/** Print a failure as JSON on stderr and exit non-zero, so scripts and\n * agents can read it the same way as a success. The billing API names\n * why it refused in `error.details.error` (`details.type` for usage\n * intake); that comes through as `code`. */\nfunction fail(err: unknown): void {\n const status =\n typeof (err as { status?: unknown }).status === \"number\"\n ? (err as { status: number }).status\n : undefined;\n const body = (err as { body?: Record<string, unknown> | null }).body;\n const message =\n typeof body?.error_message === \"string\"\n ? body.error_message\n : err instanceof Error\n ? err.message\n : String(err);\n const details = (body?.error as { details?: Record<string, unknown> } | null)\n ?.details;\n const code =\n details?.error ?? details?.type ?? (err as { code?: unknown }).code;\n const readable =\n typeof code === \"string\" && status === 409\n ? CONFLICT_MESSAGES[code]\n : undefined;\n process.stderr.write(\n JSON.stringify({\n error: readable ?? message,\n ...(status ? { status } : {}),\n ...(typeof code === \"string\" ? { code } : {}),\n }) + \"\\n\",\n );\n process.exitCode = 1;\n}\n\n/** What a plan change's 409 means for whoever ran it. */\nconst CONFLICT_MESSAGES: Record<string, string> = {\n card_funding_pending: CARD_FUNDING_PENDING_MESSAGE,\n plan_charge_changed:\n \"The plan's monthly total changed since it was shown (another change was made meanwhile). Nothing was changed; run the command again to see the new total.\",\n payment_method_changed:\n \"The saved payment method changed while the plan was checked. Nothing was changed; run the command again.\",\n};\n\nfunction run(action: () => Promise<void>): Promise<void> {\n return action().catch(fail);\n}\n\nfunction limitOf(raw: string | undefined): number | undefined {\n if (raw === undefined) return undefined;\n if (!/^\\d+$/.test(raw.trim())) {\n throw new BillingError(`--limit must be a whole number, not \"${raw}\".`);\n }\n return Number(raw.trim());\n}\n\nfunction onOff(raw: string | undefined, flag: string): boolean | undefined {\n if (raw === undefined) return undefined;\n const value = raw.trim().toLowerCase();\n if (value === \"on\") return true;\n if (value === \"off\") return false;\n throw new BillingError(`${flag} is on or off, not \"${raw}\".`);\n}\n\n/** Print a fresh Stripe link and, with --open, open it. A credit note has\n * only a PDF. `opened` is true only when the opener finished cleanly,\n * \"attempted\" when it was still running, and false otherwise. */\nasync function showLink(\n ctx: CommandContext,\n id: string,\n link: StripeLink,\n open: boolean,\n): Promise<void> {\n const target = link.url || link.pdf_url;\n let result: OpenResult = { opened: false };\n if (open) {\n result = target\n ? await openInBrowser(target)\n : { opened: false, reason: \"The response had no link to open.\" };\n }\n ctx.output({\n id: Number(id),\n url: link.url,\n pdf_url: link.pdf_url,\n opened: result.opened,\n ...(result.reason ? { open_error: result.reason } : {}),\n note: \"Stripe links expire; run this again for a fresh one.\",\n });\n}\n\n/**\n * Print what a buy or pay-now returned. The API answers 201 whatever the\n * charge's outcome, so a declined card is read from the purchase's status:\n * the result is printed, then a JSON error on stderr and a non-zero exit.\n * A pending or processing charge exits 0 with a note saying so.\n */\nexport function reportCharge(\n ctx: CommandContext,\n result: Record<string, unknown>,\n what: string,\n): void {\n const outcome = chargeOutcome(\n result.credit_purchase as Record<string, unknown> | null | undefined,\n what,\n );\n if (outcome.kind === \"pending\") {\n ctx.output({ ...result, pending: true, note: outcome.note });\n return;\n }\n ctx.output(result);\n if (outcome.kind === \"declined\") {\n process.stderr.write(\n JSON.stringify({\n error: outcome.message,\n code:\n outcome.status === \"failed\"\n ? \"charge_declined\"\n : `charge_${outcome.status || \"unknown\"}`,\n charge_status: outcome.status || null,\n ...(outcome.reason ? { reason: outcome.reason } : {}),\n }) + \"\\n\",\n );\n process.exitCode = 1;\n }\n}\n\nconst LIMITS_NOTE =\n \"AI limits stop new AI work once reached; orders and hosting limits only notify and never stop anything; Studios limits are recorded and notified, not enforced.\";\n\nexport function registerBillingCommands(\n parent: Command,\n ctx: CommandContext,\n): void {\n parent\n .command(\"show\")\n .description(\n \"Show the billing page: invoices, upcoming charge, payment method and terms\",\n )\n .action(() =>\n run(async () => ctx.output(await showBilling(await client(ctx)))),\n );\n\n parent\n .command(\"update\")\n .description(\n \"Set where invoices and billing emails go (the only billing field a merchant sets)\",\n )\n .requiredOption(\n \"--email <address>\",\n 'Billing email address, or \"none\" to stop billing emails',\n )\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action((opts: { email: string; yes: boolean }) =>\n run(async () => {\n const email =\n opts.email.trim().toLowerCase() === \"none\" ? null : opts.email.trim();\n await confirmChange(\n email\n ? `Send this company's invoices and billing emails to ${email}?`\n : \"Clear the billing email? Fluid then sends no billing email until one is set.\",\n opts.yes,\n );\n ctx.output(await setBillingEmail(await client(ctx), email));\n }),\n );\n\n parent\n .command(\"standing\")\n .description(\n \"Show the company's billing standing (invoiced companies only)\",\n )\n .action(() =>\n run(async () => ctx.output(await showStanding(await client(ctx)))),\n );\n\n parent\n .command(\"instrument <bank-account-id>\")\n .description(\n \"Collect from a saved bank account whose ACH authorization was already given in the admin; a new account or card is set up in the admin\",\n )\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action((bankAccountId: string, opts: { yes: boolean }) =>\n run(async () => {\n await confirmChange(\n `Collect this company's billing from bank account ${bankAccountId}?`,\n opts.yes,\n );\n ctx.output(await useBankAccount(await client(ctx), bankAccountId));\n }),\n );\n\n const payments = parent\n .command(\"payments\")\n .description(\n \"List pulls from the company's bank account or card, newest first\",\n )\n .option(\"--limit <n>\", \"Only the newest n\")\n .action((opts: { limit?: string }) =>\n run(async () =>\n ctx.output(await listPayments(await client(ctx), limitOf(opts.limit))),\n ),\n );\n\n payments\n .command(\"invoice <id>\")\n .description(\"Get a fresh Stripe invoice link for one payment\")\n .option(\"--open\", \"Open the link in the default browser\", false)\n .action((id: string, opts: { open: boolean }) =>\n run(async () =>\n showLink(\n ctx,\n id,\n await paymentInvoice(await client(ctx), id),\n opts.open,\n ),\n ),\n );\n\n const documents = parent\n .command(\"documents\")\n .description(\n \"List Stripe documents (paid invoices and credit notes), newest first\",\n )\n .option(\"--limit <n>\", \"Only the newest n\")\n .action((opts: { limit?: string }) =>\n run(async () =>\n ctx.output(await listDocuments(await client(ctx), limitOf(opts.limit))),\n ),\n );\n\n documents\n .command(\"link <id>\")\n .description(\"Get a fresh Stripe link for one document\")\n .option(\"--open\", \"Open the link in the default browser\", false)\n .action((id: string, opts: { open: boolean }) =>\n run(async () =>\n showLink(ctx, id, await documentLink(await client(ctx), id), opts.open),\n ),\n );\n\n registerCreditCommands(parent, ctx);\n registerLimitCommands(parent, ctx);\n\n const usage = parent.command(\"usage\").description(\"Report billable usage\");\n\n usage\n .command(\"record\")\n .description(\n \"Record one usage event (needs billing.create_usage, with a company admin's or the company's own token; Droplet tokens are refused, and Mist AI usage goes through Mist)\",\n )\n .requiredOption(\"--sku <sku>\", \"Active billing product code\")\n .requiredOption(\"--quantity <n>\", \"Whole number of units\")\n .requiredOption(\n \"--dedup-key <key>\",\n \"Your id for this event; a repeat with the same SKU and key records nothing\",\n )\n .option(\n \"--occurred-at <iso8601>\",\n \"When it happened, with a zone (default: now)\",\n )\n .option(\"--evidence <json>\", \"JSON object stored with the event\")\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action(\n (opts: {\n sku: string;\n quantity: string;\n dedupKey: string;\n occurredAt?: string;\n evidence?: string;\n yes: boolean;\n }) =>\n run(async () => {\n await confirmChange(\n `Record ${opts.quantity} of ${opts.sku} as billable usage?`,\n opts.yes,\n );\n ctx.output(await recordUsage(await client(ctx), opts));\n }),\n );\n}\n\nfunction registerCreditCommands(parent: Command, ctx: CommandContext): void {\n const credits = parent\n .command(\"credits\")\n .description(\"Show the credit balance, the plan and what more credits cost\")\n .action(() =>\n run(async () => ctx.output(await showCredits(await client(ctx)))),\n );\n\n credits\n .command(\"plan [plan-key]\")\n .description(\n \"Choose the plan from the 1st of next month (invoiced companies only)\",\n )\n .option(\"--interval <interval>\", \"monthly or annual\")\n .option(\n \"--monthly-price <dollars>\",\n \"Size the plan to a monthly price between the catalog's\",\n )\n .option(\"--keep-current\", \"Drop the plan chosen for next month\", false)\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action(\n (\n planKey: string | undefined,\n opts: {\n interval?: \"monthly\" | \"annual\";\n monthlyPrice?: string;\n keepCurrent: boolean;\n yes: boolean;\n },\n ) =>\n run(async () => {\n let choice: PlanChoice;\n if (opts.keepCurrent) {\n if (planKey || opts.interval || opts.monthlyPrice) {\n throw new BillingError(\n \"--keep-current takes no plan, --interval or --monthly-price.\",\n );\n }\n choice = { keepCurrentPlan: true };\n } else {\n choice = {\n planKey,\n billingInterval: opts.interval,\n monthlyPriceCents:\n opts.monthlyPrice === undefined\n ? undefined\n : parseDollars(opts.monthlyPrice, {\n label: \"--monthly-price\",\n }),\n };\n }\n const api = await client(ctx);\n const { question, charge } = await preparePlanChoice(api, choice);\n await confirmChange(question, opts.yes);\n ctx.output(await choosePlan(api, choice, charge));\n }),\n );\n\n credits\n .command(\"movements\")\n .description(\"List every change to the credit balance, newest first\")\n .option(\"--limit <n>\", \"Only the newest n\")\n .action((opts: { limit?: string }) =>\n run(async () =>\n ctx.output(\n await listCreditMovements(await client(ctx), limitOf(opts.limit)),\n ),\n ),\n );\n\n credits\n .command(\"quote <dollars>\")\n .description(\n \"Price a purchase of credits: bonus, credits granted, card fee and total (whole dollars)\",\n )\n .action((dollars: string) =>\n run(async () =>\n ctx.output(\n await quoteCredits(\n await client(ctx),\n parseDollars(dollars, { wholeDollars: true }),\n ),\n ),\n ),\n );\n\n credits\n .command(\"buy <dollars>\")\n .description(\n \"Buy credits with the saved payment method (whole dollars; spends real money and accepts the purchase terms)\",\n )\n .option(\n \"--purchase-key <uuid>\",\n \"Retry an earlier buy without charging twice (printed by that buy)\",\n )\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action((dollars: string, opts: { purchaseKey?: string; yes: boolean }) =>\n run(async () => {\n const amountCents = parseDollars(dollars, { wholeDollars: true });\n // A key is generated only when none was given. A blank one (an unset\n // variable in a retry script) is refused: replacing it with a fresh\n // key would turn a retry into a second charge.\n const retrying = opts.purchaseKey !== undefined;\n const purchaseKey = retrying\n ? (opts.purchaseKey as string).trim()\n : randomUUID();\n if (retrying && !purchaseKey) {\n throw new BillingError(\n \"--purchase-key is empty. Pass the key printed by the earlier buy, or leave the flag out for a new purchase.\",\n );\n }\n if (!/^[0-9a-f-]{36}$/i.test(purchaseKey)) {\n throw new BillingError(\"--purchase-key must be a UUID.\");\n }\n const api = await client(ctx);\n const prepared = await prepareCreditPurchase(api, amountCents);\n await confirmChange(\n retrying\n ? purchaseRetryQuestion(purchaseKey, prepared.question)\n : prepared.question,\n opts.yes,\n );\n let bought: Record<string, unknown>;\n try {\n bought = await buyCredits(api, prepared, purchaseKey);\n } catch (err) {\n // Whether the charge went through can be unknown, so the key that\n // makes a retry charge once goes with the error.\n if (err instanceof Error) {\n err.message = `${err.message} (purchase key ${purchaseKey}; check \\`fluid billing credits purchases\\`, and retry with --purchase-key ${purchaseKey} rather than a new buy)`;\n const body = (err as { body?: { error_message?: unknown } | null })\n .body;\n if (body && typeof body.error_message === \"string\") {\n body.error_message = err.message;\n }\n }\n throw err;\n }\n reportCharge(ctx, bought, \"purchase\");\n }),\n );\n\n const purchases = credits\n .command(\"purchases\")\n .description(\"List credit purchases, newest first\")\n .option(\"--limit <n>\", \"Only the newest n\")\n .action((opts: { limit?: string }) =>\n run(async () =>\n ctx.output(\n await listCreditPurchases(await client(ctx), limitOf(opts.limit)),\n ),\n ),\n );\n\n purchases\n .command(\"invoice <id>\")\n .description(\"Get a fresh Stripe invoice link for one purchase\")\n .option(\"--open\", \"Open the link in the default browser\", false)\n .action((id: string, opts: { open: boolean }) =>\n run(async () =>\n showLink(\n ctx,\n id,\n await purchaseInvoice(await client(ctx), id),\n opts.open,\n ),\n ),\n );\n\n credits\n .command(\"pay-now\")\n .description(\n \"Charge the saved payment method now for what the credits are overdrawn by (spends real money)\",\n )\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action((opts: { yes: boolean }) =>\n run(async () => {\n const api = await client(ctx);\n const prepared = await preparePayNow(api);\n await confirmChange(prepared.question, opts.yes);\n reportCharge(\n ctx,\n await payNow(api, prepared.payNow.total_cents),\n \"payment\",\n );\n }),\n );\n}\n\nfunction registerLimitCommands(parent: Command, ctx: CommandContext): void {\n const limits = parent\n .command(\"limits\")\n .description(`Show spend limits and auto-reload. ${LIMITS_NOTE}`)\n .action(() =>\n run(async () => ctx.output(await showLimits(await client(ctx)))),\n );\n\n const dollars = \"<dollars|none>\";\n limits\n .command(\"set\")\n .description(\n `Change spend limits and auto-reload; options left out keep their value. Amounts are dollars; \"none\" clears a limit or restores Fluid's default reload figure. ${LIMITS_NOTE}`,\n )\n .option(\"--limits <on|off>\", \"Turn the spend limits on or off\")\n .option(`--ai-monthly ${dollars}`, \"Monthly AI limit\")\n .option(`--ai-daily ${dollars}`, \"Daily AI limit\")\n .option(`--studios-monthly ${dollars}`, \"Monthly Fluid Studios limit\")\n .option(`--studios-daily ${dollars}`, \"Daily Fluid Studios limit\")\n .option(`--orders-monthly ${dollars}`, \"Monthly orders limit (notifies)\")\n .option(`--hosting-monthly ${dollars}`, \"Monthly hosting limit (notifies)\")\n .option(\n \"--auto-reload <on|off>\",\n \"Buy credits with the saved card when the balance runs low; on accepts the reload authorization\",\n )\n .option(`--reload-threshold ${dollars}`, \"Reload below this balance\")\n .option(`--reload-amount ${dollars}`, \"What one reload buys\")\n .option(`--reload-daily-cap ${dollars}`, \"Most reloaded in a day\")\n .option(`--reload-monthly-cap ${dollars}`, \"Most reloaded in a month\")\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action((opts: Record<string, string | boolean | undefined>) =>\n run(async () => {\n const money = (key: string, whole = false) => {\n const raw = opts[key];\n return typeof raw === \"string\"\n ? parseDollarsOrNone(raw, {\n wholeDollars: whole,\n label: `--${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`,\n })\n : undefined;\n };\n const changes: LimitChanges = {\n limitsEnabled: onOff(opts.limits as string | undefined, \"--limits\"),\n aiMonthly: money(\"aiMonthly\"),\n aiDaily: money(\"aiDaily\"),\n studiosMonthly: money(\"studiosMonthly\"),\n studiosDaily: money(\"studiosDaily\"),\n ordersMonthly: money(\"ordersMonthly\"),\n hostingMonthly: money(\"hostingMonthly\"),\n autoReload: onOff(\n opts.autoReload as string | undefined,\n \"--auto-reload\",\n ),\n reloadThreshold: money(\"reloadThreshold\", true),\n reloadAmount: money(\"reloadAmount\", true),\n reloadDailyCap: money(\"reloadDailyCap\"),\n reloadMonthlyCap: money(\"reloadMonthlyCap\"),\n };\n const api = await client(ctx);\n // Only turning auto-reload on reads the controls in force.\n const current =\n changes.autoReload === true ? await showLimits(api) : {};\n const { body, question } = limitsUpdate(current, changes);\n await confirmChange(question, opts.yes === true);\n ctx.output(await setLimits(api, body));\n }),\n );\n}\n\nconst plugin: FluidPlugin = {\n name: \"@fluid-app/fluid-cli-billing\",\n version: \"0.1.0\",\n register(ctx) {\n ctx.program.addCommand(\n createDomainCommand(\n \"billing\",\n \"Read the company's billing and manage its credits and spend limits\",\n registerBillingCommands,\n ),\n );\n },\n};\n\nexport default plugin;\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluid-app/fluid-cli-billing",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Fluid CLI plugin for company billing — invoices, payments, credits and spend limits",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@fluid-app/fluid-cli": "0.1.
|
|
24
|
+
"@fluid-app/fluid-cli": "0.1.52",
|
|
25
25
|
"commander": "^12.0.0",
|
|
26
26
|
"prompts": "^2.4.2"
|
|
27
27
|
},
|