@fluid-app/fluid-cli-billing 0.0.0-stage → 0.1.1
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/README.md +89 -2
- package/dist/index.d.mts +21 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +785 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +81 -3
package/README.md
CHANGED
|
@@ -1,3 +1,90 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @fluid-app/fluid-cli-billing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Fluid CLI plugin for company billing: invoices, payments, credits and spend
|
|
4
|
+
limits.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install -g @fluid-app/fluid-cli @fluid-app/fluid-cli-billing
|
|
8
|
+
fluid login
|
|
9
|
+
fluid billing show # invoices, upcoming charge, payment method
|
|
10
|
+
fluid billing update --email ap@example.com --yes
|
|
11
|
+
fluid billing standing
|
|
12
|
+
fluid billing payments --limit 20
|
|
13
|
+
fluid billing payments invoice 512 --open # fresh Stripe link; they expire
|
|
14
|
+
fluid billing documents
|
|
15
|
+
fluid billing documents link 409 --open
|
|
16
|
+
fluid billing credits # balance, plan, prices
|
|
17
|
+
fluid billing credits quote 1000 # whole dollars
|
|
18
|
+
fluid billing credits buy 1000 --yes # spends real money
|
|
19
|
+
fluid billing credits purchases
|
|
20
|
+
fluid billing credits movements --limit 50
|
|
21
|
+
fluid billing credits pay-now --yes # spends real money
|
|
22
|
+
fluid billing credits plan growth --interval annual --yes
|
|
23
|
+
fluid billing limits
|
|
24
|
+
fluid billing limits set --limits on --ai-monthly 1500 --ai-daily 100 --yes
|
|
25
|
+
fluid billing usage record --sku sms_send --quantity 42 --dedup-key batch-7 --yes
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Every command prints JSON; amounts are the API's integer cents, as it sent
|
|
29
|
+
them. Failures print a JSON `error` (and the API's `code`, such as
|
|
30
|
+
`quote_changed`) on stderr and exit non-zero. Anything that changes billing or
|
|
31
|
+
charges the saved payment method asks for confirmation, or needs `--yes` when
|
|
32
|
+
there is no terminal. `buy` and `pay-now` show the quoted total, card fee
|
|
33
|
+
included, in that question; `buy` sends the quote back so the server refuses
|
|
34
|
+
it if the price, terms or payment method changed. `credits plan` asks with
|
|
35
|
+
the monthly total the plan will charge from the 1st, priced as Fluid prices
|
|
36
|
+
it (the catalog or chosen price, the annual discount and the card fee), and
|
|
37
|
+
changes nothing when it can't price the choice. It sends the monthly charge
|
|
38
|
+
and total it showed, so if another admin changes the plan, or the saved
|
|
39
|
+
payment method becomes a card, meanwhile, the server refuses the choice
|
|
40
|
+
(`code: "plan_charge_changed"`) and nothing is saved. While the saved card's type is still being checked, no plan can be
|
|
41
|
+
chosen (`code: "card_funding_pending"`); try again in a few minutes. If a `buy` fails without a
|
|
42
|
+
clear answer, retry with the `--purchase-key` it printed rather than buying
|
|
43
|
+
again.
|
|
44
|
+
|
|
45
|
+
`buy` and `pay-now` print the purchase the API returned. The API answers
|
|
46
|
+
the same way whatever the charge's outcome, so the CLI reads its `status`.
|
|
47
|
+
Any other outcome than success or pending prints the purchase, then a JSON
|
|
48
|
+
error on stderr with `charge_status` and a `code`, and exits non-zero:
|
|
49
|
+
`charge_declined` for `failed` (with the decline `reason` when Stripe gave
|
|
50
|
+
one), `charge_refunded` for `refunded`, `charge_reversed` for `reversed`, and
|
|
51
|
+
`charge_<status>` for any other status (`charge_unknown` when there is none). A `pending` or
|
|
52
|
+
`processing` charge (a bank debit takes about 4 business days) exits 0 with
|
|
53
|
+
`pending: true` and a note; `fluid billing credits purchases` shows the
|
|
54
|
+
outcome later.
|
|
55
|
+
|
|
56
|
+
`--open` opens only Stripe https links (the hosted invoice page and the
|
|
57
|
+
invoice and credit note PDFs). `opened` is `true` once the system opener
|
|
58
|
+
finished cleanly, `"attempted"` if it was still running after a few seconds,
|
|
59
|
+
and `false` with an `open_error` when there is no opener (a headless box
|
|
60
|
+
without `xdg-open`, say) or it failed; the link is printed either way.
|
|
61
|
+
|
|
62
|
+
## Commands that need `fluid login`
|
|
63
|
+
|
|
64
|
+
Changes that record who made them need a signed-in user. A company (`C-`)
|
|
65
|
+
or partner token is refused with a 403 for these, even when its role holds
|
|
66
|
+
`billing.update`:
|
|
67
|
+
|
|
68
|
+
- `fluid billing credits buy`
|
|
69
|
+
- `fluid billing credits pay-now`
|
|
70
|
+
- `fluid billing credits plan`
|
|
71
|
+
- `fluid billing limits set`
|
|
72
|
+
|
|
73
|
+
Run `fluid login` as a company admin for them. Reading billing, credits,
|
|
74
|
+
limits, payments and documents works with a company token whose role holds
|
|
75
|
+
`billing.view`.
|
|
76
|
+
|
|
77
|
+
Amount arguments and flags are dollars (`1000`, `19.99`). Purchases and the
|
|
78
|
+
auto-reload threshold and amount must be whole dollars.
|
|
79
|
+
|
|
80
|
+
Spend limits: AI limits stop new AI work once reached; orders and hosting
|
|
81
|
+
limits only notify; Studios limits are recorded and notified but not enforced.
|
|
82
|
+
Turning `--auto-reload on` accepts the reload authorization it prints.
|
|
83
|
+
|
|
84
|
+
`usage record` needs `billing.create_usage` and a company admin's or the
|
|
85
|
+
company's own token; Droplet tokens are refused, and Mist AI usage is reported
|
|
86
|
+
through Mist instead.
|
|
87
|
+
|
|
88
|
+
Setting up a new card or bank account needs the browser flow in the admin;
|
|
89
|
+
`fluid billing instrument <bank-account-id>` only switches back to a bank
|
|
90
|
+
account whose ACH authorization was already given.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CommandContext, FluidPlugin } from "@fluid-app/fluid-cli";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
|
|
4
|
+
//#region src/index.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Changes to billing need an explicit yes. Scripts and agents pass `--yes`;
|
|
7
|
+
* an interactive terminal is asked. With neither, nothing runs.
|
|
8
|
+
*/
|
|
9
|
+
declare function confirmChange(question: string, yes: boolean, interactive?: boolean): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Print what a buy or pay-now returned. The API answers 201 whatever the
|
|
12
|
+
* charge's outcome, so a declined card is read from the purchase's status:
|
|
13
|
+
* the result is printed, then a JSON error on stderr and a non-zero exit.
|
|
14
|
+
* A pending or processing charge exits 0 with a note saying so.
|
|
15
|
+
*/
|
|
16
|
+
declare function reportCharge(ctx: CommandContext, result: Record<string, unknown>, what: string): void;
|
|
17
|
+
declare function registerBillingCommands(parent: Command, ctx: CommandContext): void;
|
|
18
|
+
declare const plugin: FluidPlugin;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { confirmChange, plugin as default, registerBillingCommands, reportCharge };
|
|
21
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;AAmDA;;iBAAsB,aAAA,CACpB,QAAA,UACA,GAAA,WACA,WAAA,aACC,OAAA;;;;;;;iBA6Ga,YAAA,CACd,GAAA,EAAK,cAAA,EACL,MAAA,EAAQ,MAAA,mBACR,IAAA;AAAA,iBA8Bc,uBAAA,CACd,MAAA,EAAQ,OAAA,EACR,GAAA,EAAK,cAAA;AAAA,cAwZD,MAAA,EAAQ,WAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import prompts from "prompts";
|
|
3
|
+
import { createDomainCommand } from "@fluid-app/fluid-cli";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
//#region src/money.ts
|
|
6
|
+
var AmountError = class extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "AmountError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const DOLLARS = /^\$?(\d+)(?:\.(\d{1,2}))?$/;
|
|
13
|
+
/**
|
|
14
|
+
* Parse a dollar amount ("250", "$250", "19.99") into integer cents.
|
|
15
|
+
* Rejects anything else — signs, commas, exponents, a third decimal — rather
|
|
16
|
+
* than guessing. `wholeDollars` refuses cents, as purchases and reload
|
|
17
|
+
* figures require.
|
|
18
|
+
*/
|
|
19
|
+
function parseDollars(input, opts = {}) {
|
|
20
|
+
const label = opts.label ?? "Amount";
|
|
21
|
+
const match = DOLLARS.exec(input.trim());
|
|
22
|
+
if (!match) throw new AmountError(`${label} "${input}" isn't a dollar amount. Expected something like 250 or 19.99.`);
|
|
23
|
+
const whole = match[1] ?? "0";
|
|
24
|
+
const fraction = (match[2] ?? "").padEnd(2, "0");
|
|
25
|
+
const cents = BigInt(whole) * 100n + BigInt(fraction || "0");
|
|
26
|
+
if (cents <= 0n) throw new AmountError(`${label} must be more than $0.`);
|
|
27
|
+
if (opts.wholeDollars && cents % 100n !== 0n) throw new AmountError(`${label} must be whole dollars, not "${input}".`);
|
|
28
|
+
if (cents > BigInt(Number.MAX_SAFE_INTEGER)) throw new AmountError(`${label} "${input}" is too large.`);
|
|
29
|
+
return Number(cents);
|
|
30
|
+
}
|
|
31
|
+
/** A dollar flag that also takes "none" to clear the value (sent as null). */
|
|
32
|
+
function parseDollarsOrNone(input, opts = {}) {
|
|
33
|
+
return input.trim().toLowerCase() === "none" ? null : parseDollars(input, opts);
|
|
34
|
+
}
|
|
35
|
+
/** Phrase integer cents as US dollars ("$1,029.00") without float math. */
|
|
36
|
+
function formatCents(cents) {
|
|
37
|
+
if (!Number.isSafeInteger(cents)) throw new AmountError(`Expected whole cents from the API, got ${cents}.`);
|
|
38
|
+
const sign = cents < 0 ? "-" : "";
|
|
39
|
+
const digits = String(Math.abs(cents)).padStart(3, "0");
|
|
40
|
+
return `${sign}$${digits.slice(0, -2).replace(/\B(?=(\d{3})+(?!\d))/g, ",")}.${digits.slice(-2)}`;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/api.ts
|
|
44
|
+
var BillingError = class extends Error {
|
|
45
|
+
/** A machine-readable reason, printed as `code` like the API's. */
|
|
46
|
+
code;
|
|
47
|
+
constructor(message, code) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "BillingError";
|
|
50
|
+
this.code = code;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
/** Why a plan can't be chosen while the saved card's funding is unknown. */
|
|
54
|
+
const CARD_FUNDING_PENDING_MESSAGE = "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.";
|
|
55
|
+
const BASE = "/api/settings/billing";
|
|
56
|
+
const USAGE_EVENTS = "/api/v202604/billing/usage_events";
|
|
57
|
+
function id(value) {
|
|
58
|
+
const trimmed = value.trim();
|
|
59
|
+
if (!/^\d+$/.test(trimmed)) throw new BillingError(`Expected a numeric id, got "${value}".`);
|
|
60
|
+
return encodeURIComponent(trimmed);
|
|
61
|
+
}
|
|
62
|
+
function field(data, key, what) {
|
|
63
|
+
const value = data?.[key];
|
|
64
|
+
if (value === void 0) throw new BillingError(`The ${what} response didn't include "${key}".`);
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
async function showBilling(client) {
|
|
68
|
+
return field(await client.get(BASE), "billing", "billing");
|
|
69
|
+
}
|
|
70
|
+
/** Set where invoices and billing emails go. null clears it, and Fluid then
|
|
71
|
+
* sends no billing email until one is set. The server validates the
|
|
72
|
+
* address. */
|
|
73
|
+
async function setBillingEmail(client, email) {
|
|
74
|
+
const address = email?.trim() || null;
|
|
75
|
+
return field(await client.patch(BASE, { billing_email: address }), "billing", "billing update");
|
|
76
|
+
}
|
|
77
|
+
async function showStanding(client) {
|
|
78
|
+
return field(await client.get(`${BASE}/standing`), "standing", "standing");
|
|
79
|
+
}
|
|
80
|
+
/** Move collections onto a saved bank account whose ACH authorization the
|
|
81
|
+
* merchant already gave through instrument setup in the admin. */
|
|
82
|
+
async function useBankAccount(client, bankAccountId) {
|
|
83
|
+
return field(await client.patch(`${BASE}/instrument`, { instrument: { bank_account_id: Number(id(bankAccountId)) } }), "billing", "instrument switch");
|
|
84
|
+
}
|
|
85
|
+
/** The list endpoints cap a page at 100 and default to 25, newest first. */
|
|
86
|
+
const PAGE_SIZE = 100;
|
|
87
|
+
const MAX_PAGES = 25;
|
|
88
|
+
/**
|
|
89
|
+
* Read every page of a list, newest first. `limit` stops after that many
|
|
90
|
+
* items; without it the whole list is read, and one longer than
|
|
91
|
+
* MAX_PAGES * PAGE_SIZE is refused rather than returned partially.
|
|
92
|
+
*/
|
|
93
|
+
async function listAll(client, path, key, limit) {
|
|
94
|
+
if (limit !== void 0 && (!Number.isSafeInteger(limit) || limit < 1)) throw new BillingError("--limit must be a whole number of at least 1.");
|
|
95
|
+
const all = [];
|
|
96
|
+
let cursor;
|
|
97
|
+
for (let page = 1; page <= MAX_PAGES; page += 1) {
|
|
98
|
+
const want = limit === void 0 ? PAGE_SIZE : limit - all.length;
|
|
99
|
+
const data = await client.get(path, { page: {
|
|
100
|
+
limit: Math.min(PAGE_SIZE, want),
|
|
101
|
+
cursor
|
|
102
|
+
} });
|
|
103
|
+
const items = data[key] ?? [];
|
|
104
|
+
all.push(...items);
|
|
105
|
+
const next = data.meta?.pagination?.next_cursor;
|
|
106
|
+
if (limit !== void 0 && all.length >= limit) return all.slice(0, limit);
|
|
107
|
+
if (!next || items.length === 0) return all;
|
|
108
|
+
cursor = next;
|
|
109
|
+
}
|
|
110
|
+
throw new BillingError(`There are more than ${MAX_PAGES * PAGE_SIZE} ${key.replace(/_/g, " ")}; pass --limit to read the newest ones.`);
|
|
111
|
+
}
|
|
112
|
+
const listPayments = (client, limit) => listAll(client, `${BASE}/payments`, "payments", limit);
|
|
113
|
+
const listDocuments = (client, limit) => listAll(client, `${BASE}/documents`, "documents", limit);
|
|
114
|
+
const listCreditMovements = (client, limit) => listAll(client, `${BASE}/credits/movements`, "credit_movements", limit);
|
|
115
|
+
const listCreditPurchases = (client, limit) => listAll(client, `${BASE}/credits/purchases`, "credit_purchases", limit);
|
|
116
|
+
async function link(client, path, key) {
|
|
117
|
+
return field(await client.get(path), key, "link");
|
|
118
|
+
}
|
|
119
|
+
const paymentInvoice = async (client, paymentId) => link(client, `${BASE}/payments/${id(paymentId)}/invoice`, "invoice");
|
|
120
|
+
const documentLink = async (client, documentId) => link(client, `${BASE}/documents/${id(documentId)}/link`, "document");
|
|
121
|
+
const purchaseInvoice = async (client, purchaseId) => link(client, `${BASE}/credits/purchases/${id(purchaseId)}/invoice`, "invoice");
|
|
122
|
+
async function showCredits(client) {
|
|
123
|
+
return field(await client.get(`${BASE}/credits`), "credits", "credits");
|
|
124
|
+
}
|
|
125
|
+
/** The PATCH body for a plan choice. A first plan needs a key or a price;
|
|
126
|
+
* the server checks the key against its catalog. */
|
|
127
|
+
function planChoiceBody(choice) {
|
|
128
|
+
if ("keepCurrentPlan" in choice) return { keep_current_plan: true };
|
|
129
|
+
const body = {};
|
|
130
|
+
if (choice.planKey !== void 0) {
|
|
131
|
+
const key = choice.planKey.trim().toLowerCase();
|
|
132
|
+
if (!key) throw new BillingError("The plan key is empty.");
|
|
133
|
+
body.plan_key = key;
|
|
134
|
+
}
|
|
135
|
+
if (choice.billingInterval !== void 0) {
|
|
136
|
+
if (!["monthly", "annual"].includes(choice.billingInterval)) throw new BillingError(`--interval is monthly or annual, not "${choice.billingInterval}".`);
|
|
137
|
+
body.billing_interval = choice.billingInterval;
|
|
138
|
+
}
|
|
139
|
+
if (choice.monthlyPriceCents !== void 0) body.monthly_price_cents = choice.monthlyPriceCents;
|
|
140
|
+
if (Object.keys(body).length === 0) throw new BillingError("Name a plan, a --monthly-price or --interval, or pass --keep-current.");
|
|
141
|
+
return body;
|
|
142
|
+
}
|
|
143
|
+
/** The card networks' cap on a card fee, in basis points (Billing::CardSurcharge). */
|
|
144
|
+
const CARD_NETWORK_CAP_BASIS_POINTS = 300;
|
|
145
|
+
function num(value) {
|
|
146
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Price a plan choice the way the server prices it (Billing::CreditTermsManager
|
|
150
|
+
* and Billing::CreditTermsResolver): the choice is laid over the plan already
|
|
151
|
+
* waiting for next month, or the plan in force; a key alone costs its catalog
|
|
152
|
+
* price; an annual plan's monthly charge takes the annual discount; and a
|
|
153
|
+
* credit card adds its fee (Billing::CardSurcharge). Throws when the price
|
|
154
|
+
* can't be told, so nothing is changed on a figure nobody saw.
|
|
155
|
+
*/
|
|
156
|
+
function planCharge(credits, choice) {
|
|
157
|
+
if ("keepCurrentPlan" in choice) throw new BillingError("Keeping the current plan prices nothing new.");
|
|
158
|
+
const base = credits.next_plan ?? credits.plan;
|
|
159
|
+
const offered = Array.isArray(credits.catalog_plans) ? credits.catalog_plans : [];
|
|
160
|
+
const key = choice.planKey?.trim().toLowerCase();
|
|
161
|
+
const catalog = key ? offered.find((plan) => plan.key === key) : void 0;
|
|
162
|
+
if (key && !catalog) {
|
|
163
|
+
const keys = offered.map((plan) => plan.key).filter(Boolean);
|
|
164
|
+
throw new BillingError(`Plan "${key}" isn't offered to this company${keys.length ? `; choose one of ${keys.join(", ")}` : ""}. Nothing was changed.`);
|
|
165
|
+
}
|
|
166
|
+
const price = choice.monthlyPriceCents ?? num(catalog?.monthly_price_cents) ?? num(base?.monthly_price_cents);
|
|
167
|
+
if (price === void 0) throw new BillingError("Can't tell what this plan would cost each month: name a plan or a --monthly-price. Nothing was changed.");
|
|
168
|
+
const interval = choice.billingInterval ?? (typeof base?.billing_interval === "string" ? base.billing_interval : "monthly");
|
|
169
|
+
const discount = num(credits.annual_discount_percent) ?? 0;
|
|
170
|
+
const charge = interval === "annual" ? Math.round(price * (100 - discount) / 100) : price;
|
|
171
|
+
const surcharge = credits.card_surcharge;
|
|
172
|
+
let fee = 0;
|
|
173
|
+
if (catalog && interval === "monthly" && choice.monthlyPriceCents === void 0 && num(catalog.monthly_surcharge_cents) !== void 0) fee = catalog.monthly_surcharge_cents;
|
|
174
|
+
else if (surcharge?.applies === true) {
|
|
175
|
+
const rate = num(surcharge.rate_basis_points);
|
|
176
|
+
if (rate === void 0) throw new BillingError("The credits response didn't include the credit card fee rate. Nothing was changed.");
|
|
177
|
+
const caps = [Math.floor((charge * rate + 5e3) / 1e4)];
|
|
178
|
+
const cap = num(surcharge.cap_cents);
|
|
179
|
+
if (cap !== void 0) caps.push(cap);
|
|
180
|
+
if (surcharge.kind !== "ach") caps.push(Math.floor(charge * CARD_NETWORK_CAP_BASIS_POINTS / 1e4));
|
|
181
|
+
fee = Math.min(...caps);
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
planKey: key ?? (typeof base?.key === "string" ? base.key : null),
|
|
185
|
+
billingInterval: interval,
|
|
186
|
+
monthlyPriceCents: price,
|
|
187
|
+
monthlyChargeCents: charge,
|
|
188
|
+
surchargeCents: fee,
|
|
189
|
+
totalCents: charge + fee
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function planChoiceQuestion(choice, charge, current) {
|
|
193
|
+
if ("keepCurrentPlan" in choice) {
|
|
194
|
+
const total = num(current?.monthly_total_cents);
|
|
195
|
+
return `Keep the current plan${total !== void 0 ? ` (${formatCents(total)} a month)` : ""} and drop the plan chosen for next month?`;
|
|
196
|
+
}
|
|
197
|
+
if (!charge) throw new BillingError("A plan change needs its monthly charge priced.");
|
|
198
|
+
const name = charge.planKey ? `plan ${charge.planKey}` : "a plan";
|
|
199
|
+
const discount = charge.monthlyChargeCents !== charge.monthlyPriceCents ? `, ${formatCents(charge.monthlyPriceCents)} less the annual discount` : "";
|
|
200
|
+
const fee = charge.surchargeCents > 0 ? ` (${formatCents(charge.monthlyChargeCents)} plus a ${formatCents(charge.surchargeCents)} credit card fee)` : "";
|
|
201
|
+
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.`;
|
|
202
|
+
}
|
|
203
|
+
/** Read the credits and price a plan choice before asking about it. */
|
|
204
|
+
async function preparePlanChoice(client, choice) {
|
|
205
|
+
planChoiceBody(choice);
|
|
206
|
+
const credits = await showCredits(client);
|
|
207
|
+
if ("keepCurrentPlan" in choice) return { question: planChoiceQuestion(choice, void 0, credits.plan) };
|
|
208
|
+
if (credits.card_surcharge?.funding_pending === true) throw new BillingError(CARD_FUNDING_PENDING_MESSAGE, "card_funding_pending");
|
|
209
|
+
const charge = planCharge(credits, choice);
|
|
210
|
+
return {
|
|
211
|
+
question: planChoiceQuestion(choice, charge),
|
|
212
|
+
charge
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** Make a plan choice. With the monthly charge and total that were shown,
|
|
216
|
+
* the server refuses (409 plan_charge_changed) a choice that would now
|
|
217
|
+
* charge or total anything else, as when another admin chose a plan or
|
|
218
|
+
* switched the saved payment method to a card meanwhile. */
|
|
219
|
+
async function choosePlan(client, choice, shown) {
|
|
220
|
+
const body = planChoiceBody(choice);
|
|
221
|
+
if (shown && !("keepCurrentPlan" in choice)) {
|
|
222
|
+
body.expected_monthly_charge_cents = shown.monthlyChargeCents;
|
|
223
|
+
body.expected_monthly_total_cents = shown.totalCents;
|
|
224
|
+
}
|
|
225
|
+
return field(await client.patch(`${BASE}/credits`, body), "credits", "plan change");
|
|
226
|
+
}
|
|
227
|
+
/** Price a purchase as the purchase itself would be priced. */
|
|
228
|
+
async function quoteCredits(client, amountCents) {
|
|
229
|
+
return field(await client.get(`${BASE}/credits/quote`, { amount_cents: amountCents }), "quote", "quote");
|
|
230
|
+
}
|
|
231
|
+
function purchaseQuestion(quote, paymentMethod) {
|
|
232
|
+
const fee = quote.surcharge_cents > 0 ? ` (${formatCents(quote.amount_cents)} plus a ${formatCents(quote.surcharge_cents)} credit card fee)` : "";
|
|
233
|
+
return `Charge ${formatCents(quote.total_cents)}${fee} to ${paymentMethod} for ${formatCents(quote.credit_cents)} of credits (${quote.bonus_percent}% bonus)? This spends real money.`;
|
|
234
|
+
}
|
|
235
|
+
function describePaymentMethod(method) {
|
|
236
|
+
const display = method?.display;
|
|
237
|
+
const last4 = display?.last4;
|
|
238
|
+
const name = display?.brand ?? display?.bank_name;
|
|
239
|
+
if (typeof last4 === "string") return `${typeof name === "string" ? name : "the saved method"} ending ${last4}`;
|
|
240
|
+
return "the saved payment method";
|
|
241
|
+
}
|
|
242
|
+
/** Read the terms, the payment method and a fresh quote before a purchase. */
|
|
243
|
+
async function prepareCreditPurchase(client, amountCents) {
|
|
244
|
+
const credits = await showCredits(client);
|
|
245
|
+
if (credits.purchasable === false) throw new BillingError("This company can't buy credits now: there is no usable card or bank account on file.");
|
|
246
|
+
const terms = credits.purchase_terms;
|
|
247
|
+
if (typeof terms?.version !== "string") throw new BillingError("The credits response didn't include the purchase terms.");
|
|
248
|
+
const [billing, quote] = await Promise.all([showBilling(client), quoteCredits(client, amountCents)]);
|
|
249
|
+
const method = billing.payment_method;
|
|
250
|
+
const termsText = typeof terms.text === "string" ? `\n\nPurchase terms ${terms.version}:\n${terms.text}` : "";
|
|
251
|
+
return {
|
|
252
|
+
quote,
|
|
253
|
+
termsVersion: terms.version,
|
|
254
|
+
paymentMethodKey: typeof method?.key === "string" ? method.key : null,
|
|
255
|
+
question: purchaseQuestion(quote, describePaymentMethod(method)) + termsText
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* The question for a retry under an earlier purchase key. Fluid answers a
|
|
260
|
+
* known key with the purchase it already holds: it returns it, or resubmits
|
|
261
|
+
* it as first made, with its own amount and its originally reserved payment
|
|
262
|
+
* method. Only an unknown key makes the purchase described now.
|
|
263
|
+
*/
|
|
264
|
+
function purchaseRetryQuestion(purchaseKey, newPurchaseQuestion) {
|
|
265
|
+
return `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. Only if Fluid has no purchase under this key does this make a new one: ${newPurchaseQuestion}`;
|
|
266
|
+
}
|
|
267
|
+
/** Buy what was prepared. The purchase key makes a retry charge once. */
|
|
268
|
+
async function buyCredits(client, prepared, purchaseKey) {
|
|
269
|
+
const body = {
|
|
270
|
+
amount_cents: prepared.quote.amount_cents,
|
|
271
|
+
expected_credit_cents: prepared.quote.credit_cents,
|
|
272
|
+
expected_surcharge_cents: prepared.quote.surcharge_cents,
|
|
273
|
+
purchase_key: purchaseKey,
|
|
274
|
+
terms_version: prepared.termsVersion
|
|
275
|
+
};
|
|
276
|
+
if (prepared.paymentMethodKey) body.expected_payment_method_key = prepared.paymentMethodKey;
|
|
277
|
+
const data = await client.post(`${BASE}/credits/purchases`, body);
|
|
278
|
+
return {
|
|
279
|
+
purchase_key: purchaseKey,
|
|
280
|
+
credit_purchase: field(data, "credit_purchase", "purchase"),
|
|
281
|
+
balance_cents: data.balance_cents,
|
|
282
|
+
duplicate: data.duplicate
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/** Read a charge's outcome from the `credit_purchase` a buy or pay-now
|
|
286
|
+
* returned. A status this CLI doesn't know is treated as not succeeded. */
|
|
287
|
+
function chargeOutcome(purchase, what) {
|
|
288
|
+
const status = typeof purchase?.status === "string" ? purchase.status : "";
|
|
289
|
+
const reason = typeof purchase?.failure_message === "string" && purchase.failure_message ? purchase.failure_message : null;
|
|
290
|
+
switch (status) {
|
|
291
|
+
case "succeeded": return { kind: "succeeded" };
|
|
292
|
+
case "pending":
|
|
293
|
+
case "processing": return {
|
|
294
|
+
kind: "pending",
|
|
295
|
+
status,
|
|
296
|
+
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.`
|
|
297
|
+
};
|
|
298
|
+
case "failed": return {
|
|
299
|
+
kind: "declined",
|
|
300
|
+
status,
|
|
301
|
+
reason,
|
|
302
|
+
message: `The ${what} was declined${reason ? `: ${reason.replace(/\.+$/, "")}` : ""}. Nothing was charged.`
|
|
303
|
+
};
|
|
304
|
+
case "refunded":
|
|
305
|
+
case "reversed": return {
|
|
306
|
+
kind: "declined",
|
|
307
|
+
status,
|
|
308
|
+
reason,
|
|
309
|
+
message: `The ${what} was ${status}; its money went back and it grants no credits.`
|
|
310
|
+
};
|
|
311
|
+
default: return {
|
|
312
|
+
kind: "declined",
|
|
313
|
+
status,
|
|
314
|
+
reason,
|
|
315
|
+
message: `The ${what} came back with status "${status || "unknown"}", not succeeded. Check \`fluid billing credits purchases\`.`
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function payNowQuestion(payNow) {
|
|
320
|
+
const fee = payNow.surcharge_cents > 0 ? ` (the ${formatCents(payNow.amount_cents)} owed plus a ${formatCents(payNow.surcharge_cents)} credit card fee)` : "";
|
|
321
|
+
return `Charge ${formatCents(payNow.total_cents)}${fee} to the saved payment method now for what the credits are overdrawn by? This spends real money.`;
|
|
322
|
+
}
|
|
323
|
+
/** Read what Pay now would charge, refusing early when it can't run. */
|
|
324
|
+
async function preparePayNow(client) {
|
|
325
|
+
const creditBilling = (await showCredits(client)).credit_billing;
|
|
326
|
+
if (!creditBilling) throw new BillingError("This company's billing is not paid from credits.");
|
|
327
|
+
const payNow = creditBilling.pay_now;
|
|
328
|
+
if (!payNow) throw new BillingError("The credits response didn't include pay_now.");
|
|
329
|
+
if (!payNow.available) throw new BillingError(`Pay now can't run: ${payNow.reason ?? "unavailable"}. Nothing was charged.`);
|
|
330
|
+
return {
|
|
331
|
+
payNow,
|
|
332
|
+
question: payNowQuestion(payNow)
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
/** Charge the total that was shown; the server refuses any other. */
|
|
336
|
+
async function payNow(client, expectedTotalCents) {
|
|
337
|
+
return client.post(`${BASE}/credits/pay-now`, { expected_total_cents: expectedTotalCents });
|
|
338
|
+
}
|
|
339
|
+
async function showLimits(client) {
|
|
340
|
+
return field(await client.get(`${BASE}/usage-controls`), "usage_controls", "usage controls");
|
|
341
|
+
}
|
|
342
|
+
const LIMIT_FIELDS = [
|
|
343
|
+
[
|
|
344
|
+
"limitsEnabled",
|
|
345
|
+
"limits_enabled",
|
|
346
|
+
"spend limits"
|
|
347
|
+
],
|
|
348
|
+
[
|
|
349
|
+
"ordersMonthly",
|
|
350
|
+
"orders_limit_cents",
|
|
351
|
+
"orders monthly limit"
|
|
352
|
+
],
|
|
353
|
+
[
|
|
354
|
+
"aiMonthly",
|
|
355
|
+
"ai_limit_cents",
|
|
356
|
+
"AI monthly limit"
|
|
357
|
+
],
|
|
358
|
+
[
|
|
359
|
+
"aiDaily",
|
|
360
|
+
"ai_daily_limit_cents",
|
|
361
|
+
"AI daily limit"
|
|
362
|
+
],
|
|
363
|
+
[
|
|
364
|
+
"hostingMonthly",
|
|
365
|
+
"hosting_limit_cents",
|
|
366
|
+
"hosting monthly limit"
|
|
367
|
+
],
|
|
368
|
+
[
|
|
369
|
+
"studiosMonthly",
|
|
370
|
+
"studios_limit_cents",
|
|
371
|
+
"Studios monthly limit"
|
|
372
|
+
],
|
|
373
|
+
[
|
|
374
|
+
"studiosDaily",
|
|
375
|
+
"studios_daily_limit_cents",
|
|
376
|
+
"Studios daily limit"
|
|
377
|
+
],
|
|
378
|
+
[
|
|
379
|
+
"autoReload",
|
|
380
|
+
"auto_reload",
|
|
381
|
+
"auto-reload"
|
|
382
|
+
],
|
|
383
|
+
[
|
|
384
|
+
"reloadThreshold",
|
|
385
|
+
"auto_reload_threshold_cents",
|
|
386
|
+
"reload threshold"
|
|
387
|
+
],
|
|
388
|
+
[
|
|
389
|
+
"reloadAmount",
|
|
390
|
+
"auto_reload_amount_cents",
|
|
391
|
+
"reload amount"
|
|
392
|
+
],
|
|
393
|
+
[
|
|
394
|
+
"reloadDailyCap",
|
|
395
|
+
"auto_reload_daily_cap_cents",
|
|
396
|
+
"reload daily cap"
|
|
397
|
+
],
|
|
398
|
+
[
|
|
399
|
+
"reloadMonthlyCap",
|
|
400
|
+
"auto_reload_monthly_cap_cents",
|
|
401
|
+
"reload monthly cap"
|
|
402
|
+
]
|
|
403
|
+
];
|
|
404
|
+
function describe(value) {
|
|
405
|
+
if (typeof value === "boolean") return value ? "on" : "off";
|
|
406
|
+
if (value === null) return "none (default)";
|
|
407
|
+
return formatCents(value);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* The PATCH body and confirmation question for a change to the limits.
|
|
411
|
+
*
|
|
412
|
+
* Turning auto-reload on is consent to buy credits with the saved card
|
|
413
|
+
* whenever the balance runs low, so it accepts the authorization in force
|
|
414
|
+
* and names the four reload figures the question showed; the server
|
|
415
|
+
* refuses the save (409) if they are no longer the figures it would store.
|
|
416
|
+
*/
|
|
417
|
+
function limitsUpdate(current, changes) {
|
|
418
|
+
const body = {};
|
|
419
|
+
const lines = [];
|
|
420
|
+
for (const [key, wire, label] of LIMIT_FIELDS) {
|
|
421
|
+
const value = changes[key];
|
|
422
|
+
if (value === void 0) continue;
|
|
423
|
+
body[wire] = value;
|
|
424
|
+
lines.push(`${label}: ${describe(value)}`);
|
|
425
|
+
}
|
|
426
|
+
if (lines.length === 0) throw new BillingError("Nothing to change; pass at least one option.");
|
|
427
|
+
let question = `Set ${lines.join("; ")}?`;
|
|
428
|
+
if (changes.autoReload === true) {
|
|
429
|
+
const auth = current.reload_authorization;
|
|
430
|
+
if (typeof auth?.version !== "string") throw new BillingError("The usage controls didn't include the auto-reload authorization.");
|
|
431
|
+
const caps = current.reload_caps ?? {};
|
|
432
|
+
const shown = {
|
|
433
|
+
shown_reload_threshold_cents: pick(changes.reloadThreshold, current.reload_threshold_cents, "--reload-threshold"),
|
|
434
|
+
shown_reload_amount_cents: pick(changes.reloadAmount, current.reload_amount_cents, "--reload-amount"),
|
|
435
|
+
shown_reload_daily_cap_cents: pick(changes.reloadDailyCap, caps.daily_cents, "--reload-daily-cap"),
|
|
436
|
+
shown_reload_monthly_cap_cents: pick(changes.reloadMonthlyCap, caps.monthly_cents, "--reload-monthly-cap")
|
|
437
|
+
};
|
|
438
|
+
body.reload_authorization_version = auth.version;
|
|
439
|
+
Object.assign(body, shown);
|
|
440
|
+
question = `${question}\nAuto-reload buys ${formatCents(shown.shown_reload_amount_cents)} of credits with the saved card whenever the spendable balance falls below ${formatCents(shown.shown_reload_threshold_cents)}, at most ${formatCents(shown.shown_reload_daily_cap_cents)} a day and ${formatCents(shown.shown_reload_monthly_cap_cents)} a month.` + (typeof auth.text === "string" ? `\n\nAuthorization ${auth.version}:\n${auth.text}\n\nAccept it?` : "");
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
body,
|
|
444
|
+
question
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
/** A reload figure the consent names: the one being set, else the one in
|
|
448
|
+
* force. "none" falls back to a default this CLI cannot see, so consent
|
|
449
|
+
* with it is refused rather than given to an unseen figure. */
|
|
450
|
+
function pick(changed, current, flag) {
|
|
451
|
+
if (changed === null) throw new BillingError(`Turning auto-reload on names the figures it consents to; give ${flag} a dollar amount instead of none.`);
|
|
452
|
+
const value = changed ?? current;
|
|
453
|
+
if (typeof value !== "number") throw new BillingError(`The usage controls didn't include ${flag}.`);
|
|
454
|
+
return value;
|
|
455
|
+
}
|
|
456
|
+
async function setLimits(client, body) {
|
|
457
|
+
return field(await client.patch(`${BASE}/usage-controls`, body), "usage_controls", "usage controls update");
|
|
458
|
+
}
|
|
459
|
+
/** What the server's Time.iso8601 reads, with the zone required so a
|
|
460
|
+
* timestamp never means the caller's local time by accident. */
|
|
461
|
+
const ISO8601 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}(:?\d{2})?)$/;
|
|
462
|
+
/** The request body for one usage event, validated as the server would. */
|
|
463
|
+
function usageEventBody(input, now = () => /* @__PURE__ */ new Date()) {
|
|
464
|
+
const sku = input.sku.trim();
|
|
465
|
+
if (!sku || sku.length > 255) throw new BillingError("--sku must be 1 to 255 characters.");
|
|
466
|
+
const dedupKey = input.dedupKey.trim();
|
|
467
|
+
if (!dedupKey || dedupKey.length > 255) throw new BillingError("--dedup-key must be 1 to 255 characters.");
|
|
468
|
+
const raw = input.quantity.trim();
|
|
469
|
+
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(Number(raw))) throw new BillingError(`--quantity must be a whole number from 0 to ${Number.MAX_SAFE_INTEGER}, not "${input.quantity}".`);
|
|
470
|
+
const occurredAt = input.occurredAt?.trim() || now().toISOString();
|
|
471
|
+
if (!ISO8601.test(occurredAt) || Number.isNaN(Date.parse(occurredAt))) throw new BillingError(`--occurred-at must be an ISO 8601 timestamp such as 2026-10-03T14:00:00Z, not "${input.occurredAt}".`);
|
|
472
|
+
const event = {
|
|
473
|
+
sku,
|
|
474
|
+
quantity: Number(raw),
|
|
475
|
+
occurred_at: occurredAt,
|
|
476
|
+
dedup_key: dedupKey
|
|
477
|
+
};
|
|
478
|
+
if (input.evidence !== void 0) {
|
|
479
|
+
let evidence;
|
|
480
|
+
try {
|
|
481
|
+
evidence = JSON.parse(input.evidence);
|
|
482
|
+
} catch {
|
|
483
|
+
throw new BillingError("--evidence must be a JSON object.");
|
|
484
|
+
}
|
|
485
|
+
if (!evidence || typeof evidence !== "object" || Array.isArray(evidence)) throw new BillingError("--evidence must be a JSON object.");
|
|
486
|
+
event.evidence = evidence;
|
|
487
|
+
}
|
|
488
|
+
return { usage_event: event };
|
|
489
|
+
}
|
|
490
|
+
/** Record one usage event. A repeat of its SKU and dedup key returns the
|
|
491
|
+
* first event with duplicate: true and records nothing. */
|
|
492
|
+
async function recordUsage(client, input) {
|
|
493
|
+
const data = await client.post(USAGE_EVENTS, usageEventBody(input));
|
|
494
|
+
return {
|
|
495
|
+
usage_event: field(data, "usage_event", "usage event"),
|
|
496
|
+
duplicate: data.duplicate
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/browser.ts
|
|
501
|
+
/** How long to wait for the opener to finish before reporting "attempted". */
|
|
502
|
+
const OPENER_WAIT_MS = 3e3;
|
|
503
|
+
/**
|
|
504
|
+
* Every link the billing API hands out is Stripe's: the hosted invoice page
|
|
505
|
+
* (invoice.stripe.com) and the invoice and credit note PDFs
|
|
506
|
+
* (pay.stripe.com). Anything else is refused rather than opened.
|
|
507
|
+
*/
|
|
508
|
+
function isAllowedBillingUrl(raw) {
|
|
509
|
+
let url;
|
|
510
|
+
try {
|
|
511
|
+
url = new URL(raw);
|
|
512
|
+
} catch {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
if (url.protocol !== "https:") return false;
|
|
516
|
+
if (url.username || url.password || url.port) return false;
|
|
517
|
+
const host = url.hostname.toLowerCase();
|
|
518
|
+
return host === "stripe.com" || host.endsWith(".stripe.com");
|
|
519
|
+
}
|
|
520
|
+
/** Open a Stripe billing link in the default browser and report whether the
|
|
521
|
+
* opener ran. Never rejects. */
|
|
522
|
+
function openInBrowser(url, waitMs = OPENER_WAIT_MS) {
|
|
523
|
+
if (!isAllowedBillingUrl(url)) return Promise.resolve({
|
|
524
|
+
opened: false,
|
|
525
|
+
reason: "Not a Stripe https link; it was not opened."
|
|
526
|
+
});
|
|
527
|
+
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
|
|
528
|
+
return new Promise((resolve) => {
|
|
529
|
+
let settled = false;
|
|
530
|
+
let timer;
|
|
531
|
+
const settle = (result) => {
|
|
532
|
+
if (settled) return;
|
|
533
|
+
settled = true;
|
|
534
|
+
if (timer) clearTimeout(timer);
|
|
535
|
+
resolve(result);
|
|
536
|
+
};
|
|
537
|
+
let child;
|
|
538
|
+
try {
|
|
539
|
+
child = spawn(command, args, {
|
|
540
|
+
detached: true,
|
|
541
|
+
stdio: "ignore"
|
|
542
|
+
});
|
|
543
|
+
} catch (err) {
|
|
544
|
+
settle({
|
|
545
|
+
opened: false,
|
|
546
|
+
reason: openerFailure(command, err)
|
|
547
|
+
});
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
child.on("error", (err) => settle({
|
|
551
|
+
opened: false,
|
|
552
|
+
reason: openerFailure(command, err)
|
|
553
|
+
}));
|
|
554
|
+
child.on("exit", (code) => settle(code === 0 ? { opened: true } : {
|
|
555
|
+
opened: false,
|
|
556
|
+
reason: `${command} exited with code ${code ?? "unknown"}.`
|
|
557
|
+
}));
|
|
558
|
+
child.on("spawn", () => {
|
|
559
|
+
timer = setTimeout(() => {
|
|
560
|
+
child.unref();
|
|
561
|
+
settle({ opened: "attempted" });
|
|
562
|
+
}, waitMs);
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
function openerFailure(command, err) {
|
|
567
|
+
return err.code === "ENOENT" ? `No browser opener (${command}) was found.` : `${command} failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
568
|
+
}
|
|
569
|
+
//#endregion
|
|
570
|
+
//#region src/index.ts
|
|
571
|
+
async function client(ctx) {
|
|
572
|
+
return await ctx.getClient();
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Changes to billing need an explicit yes. Scripts and agents pass `--yes`;
|
|
576
|
+
* an interactive terminal is asked. With neither, nothing runs.
|
|
577
|
+
*/
|
|
578
|
+
async function confirmChange(question, yes, interactive = Boolean(process.stdin.isTTY)) {
|
|
579
|
+
if (yes) return;
|
|
580
|
+
if (!interactive) throw new BillingError(`${question} Pass --yes to confirm.`);
|
|
581
|
+
if (!(await prompts({
|
|
582
|
+
type: "confirm",
|
|
583
|
+
name: "ok",
|
|
584
|
+
message: question,
|
|
585
|
+
initial: false
|
|
586
|
+
})).ok) throw new BillingError("Cancelled.");
|
|
587
|
+
}
|
|
588
|
+
/** Print a failure as JSON on stderr and exit non-zero, so scripts and
|
|
589
|
+
* agents can read it the same way as a success. The billing API names
|
|
590
|
+
* why it refused in `error.details.error` (`details.type` for usage
|
|
591
|
+
* intake); that comes through as `code`. */
|
|
592
|
+
function fail(err) {
|
|
593
|
+
const status = typeof err.status === "number" ? err.status : void 0;
|
|
594
|
+
const body = err.body;
|
|
595
|
+
const message = typeof body?.error_message === "string" ? body.error_message : err instanceof Error ? err.message : String(err);
|
|
596
|
+
const details = (body?.error)?.details;
|
|
597
|
+
const code = details?.error ?? details?.type ?? err.code;
|
|
598
|
+
const readable = typeof code === "string" && status === 409 ? CONFLICT_MESSAGES[code] : void 0;
|
|
599
|
+
process.stderr.write(JSON.stringify({
|
|
600
|
+
error: readable ?? message,
|
|
601
|
+
...status ? { status } : {},
|
|
602
|
+
...typeof code === "string" ? { code } : {}
|
|
603
|
+
}) + "\n");
|
|
604
|
+
process.exitCode = 1;
|
|
605
|
+
}
|
|
606
|
+
/** What a plan change's 409 means for whoever ran it. */
|
|
607
|
+
const CONFLICT_MESSAGES = {
|
|
608
|
+
card_funding_pending: CARD_FUNDING_PENDING_MESSAGE,
|
|
609
|
+
plan_charge_changed: "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.",
|
|
610
|
+
payment_method_changed: "The saved payment method changed while the plan was checked. Nothing was changed; run the command again."
|
|
611
|
+
};
|
|
612
|
+
function run(action) {
|
|
613
|
+
return action().catch(fail);
|
|
614
|
+
}
|
|
615
|
+
function limitOf(raw) {
|
|
616
|
+
if (raw === void 0) return void 0;
|
|
617
|
+
if (!/^\d+$/.test(raw.trim())) throw new BillingError(`--limit must be a whole number, not "${raw}".`);
|
|
618
|
+
return Number(raw.trim());
|
|
619
|
+
}
|
|
620
|
+
function onOff(raw, flag) {
|
|
621
|
+
if (raw === void 0) return void 0;
|
|
622
|
+
const value = raw.trim().toLowerCase();
|
|
623
|
+
if (value === "on") return true;
|
|
624
|
+
if (value === "off") return false;
|
|
625
|
+
throw new BillingError(`${flag} is on or off, not "${raw}".`);
|
|
626
|
+
}
|
|
627
|
+
/** Print a fresh Stripe link and, with --open, open it. A credit note has
|
|
628
|
+
* only a PDF. `opened` is true only when the opener finished cleanly,
|
|
629
|
+
* "attempted" when it was still running, and false otherwise. */
|
|
630
|
+
async function showLink(ctx, id, link, open) {
|
|
631
|
+
const target = link.url || link.pdf_url;
|
|
632
|
+
let result = { opened: false };
|
|
633
|
+
if (open) result = target ? await openInBrowser(target) : {
|
|
634
|
+
opened: false,
|
|
635
|
+
reason: "The response had no link to open."
|
|
636
|
+
};
|
|
637
|
+
ctx.output({
|
|
638
|
+
id: Number(id),
|
|
639
|
+
url: link.url,
|
|
640
|
+
pdf_url: link.pdf_url,
|
|
641
|
+
opened: result.opened,
|
|
642
|
+
...result.reason ? { open_error: result.reason } : {},
|
|
643
|
+
note: "Stripe links expire; run this again for a fresh one."
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Print what a buy or pay-now returned. The API answers 201 whatever the
|
|
648
|
+
* charge's outcome, so a declined card is read from the purchase's status:
|
|
649
|
+
* the result is printed, then a JSON error on stderr and a non-zero exit.
|
|
650
|
+
* A pending or processing charge exits 0 with a note saying so.
|
|
651
|
+
*/
|
|
652
|
+
function reportCharge(ctx, result, what) {
|
|
653
|
+
const outcome = chargeOutcome(result.credit_purchase, what);
|
|
654
|
+
if (outcome.kind === "pending") {
|
|
655
|
+
ctx.output({
|
|
656
|
+
...result,
|
|
657
|
+
pending: true,
|
|
658
|
+
note: outcome.note
|
|
659
|
+
});
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
ctx.output(result);
|
|
663
|
+
if (outcome.kind === "declined") {
|
|
664
|
+
process.stderr.write(JSON.stringify({
|
|
665
|
+
error: outcome.message,
|
|
666
|
+
code: outcome.status === "failed" ? "charge_declined" : `charge_${outcome.status || "unknown"}`,
|
|
667
|
+
charge_status: outcome.status || null,
|
|
668
|
+
...outcome.reason ? { reason: outcome.reason } : {}
|
|
669
|
+
}) + "\n");
|
|
670
|
+
process.exitCode = 1;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const LIMITS_NOTE = "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.";
|
|
674
|
+
function registerBillingCommands(parent, ctx) {
|
|
675
|
+
parent.command("show").description("Show the billing page: invoices, upcoming charge, payment method and terms").action(() => run(async () => ctx.output(await showBilling(await client(ctx)))));
|
|
676
|
+
parent.command("update").description("Set where invoices and billing emails go (the only billing field a merchant sets)").requiredOption("--email <address>", "Billing email address, or \"none\" to stop billing emails").option("--yes", "Confirm without a prompt", false).action((opts) => run(async () => {
|
|
677
|
+
const email = opts.email.trim().toLowerCase() === "none" ? null : opts.email.trim();
|
|
678
|
+
await confirmChange(email ? `Send this company's invoices and billing emails to ${email}?` : "Clear the billing email? Fluid then sends no billing email until one is set.", opts.yes);
|
|
679
|
+
ctx.output(await setBillingEmail(await client(ctx), email));
|
|
680
|
+
}));
|
|
681
|
+
parent.command("standing").description("Show the company's billing standing (invoiced companies only)").action(() => run(async () => ctx.output(await showStanding(await client(ctx)))));
|
|
682
|
+
parent.command("instrument <bank-account-id>").description("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").option("--yes", "Confirm without a prompt", false).action((bankAccountId, opts) => run(async () => {
|
|
683
|
+
await confirmChange(`Collect this company's billing from bank account ${bankAccountId}?`, opts.yes);
|
|
684
|
+
ctx.output(await useBankAccount(await client(ctx), bankAccountId));
|
|
685
|
+
}));
|
|
686
|
+
parent.command("payments").description("List pulls from the company's bank account or card, newest first").option("--limit <n>", "Only the newest n").action((opts) => run(async () => ctx.output(await listPayments(await client(ctx), limitOf(opts.limit))))).command("invoice <id>").description("Get a fresh Stripe invoice link for one payment").option("--open", "Open the link in the default browser", false).action((id, opts) => run(async () => showLink(ctx, id, await paymentInvoice(await client(ctx), id), opts.open)));
|
|
687
|
+
parent.command("documents").description("List Stripe documents (paid invoices and credit notes), newest first").option("--limit <n>", "Only the newest n").action((opts) => run(async () => ctx.output(await listDocuments(await client(ctx), limitOf(opts.limit))))).command("link <id>").description("Get a fresh Stripe link for one document").option("--open", "Open the link in the default browser", false).action((id, opts) => run(async () => showLink(ctx, id, await documentLink(await client(ctx), id), opts.open)));
|
|
688
|
+
registerCreditCommands(parent, ctx);
|
|
689
|
+
registerLimitCommands(parent, ctx);
|
|
690
|
+
parent.command("usage").description("Report billable usage").command("record").description("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)").requiredOption("--sku <sku>", "Active billing product code").requiredOption("--quantity <n>", "Whole number of units").requiredOption("--dedup-key <key>", "Your id for this event; a repeat with the same SKU and key records nothing").option("--occurred-at <iso8601>", "When it happened, with a zone (default: now)").option("--evidence <json>", "JSON object stored with the event").option("--yes", "Confirm without a prompt", false).action((opts) => run(async () => {
|
|
691
|
+
await confirmChange(`Record ${opts.quantity} of ${opts.sku} as billable usage?`, opts.yes);
|
|
692
|
+
ctx.output(await recordUsage(await client(ctx), opts));
|
|
693
|
+
}));
|
|
694
|
+
}
|
|
695
|
+
function registerCreditCommands(parent, ctx) {
|
|
696
|
+
const credits = parent.command("credits").description("Show the credit balance, the plan and what more credits cost").action(() => run(async () => ctx.output(await showCredits(await client(ctx)))));
|
|
697
|
+
credits.command("plan [plan-key]").description("Choose the plan from the 1st of next month (invoiced companies only)").option("--interval <interval>", "monthly or annual").option("--monthly-price <dollars>", "Size the plan to a monthly price between the catalog's").option("--keep-current", "Drop the plan chosen for next month", false).option("--yes", "Confirm without a prompt", false).action((planKey, opts) => run(async () => {
|
|
698
|
+
let choice;
|
|
699
|
+
if (opts.keepCurrent) {
|
|
700
|
+
if (planKey || opts.interval || opts.monthlyPrice) throw new BillingError("--keep-current takes no plan, --interval or --monthly-price.");
|
|
701
|
+
choice = { keepCurrentPlan: true };
|
|
702
|
+
} else choice = {
|
|
703
|
+
planKey,
|
|
704
|
+
billingInterval: opts.interval,
|
|
705
|
+
monthlyPriceCents: opts.monthlyPrice === void 0 ? void 0 : parseDollars(opts.monthlyPrice, { label: "--monthly-price" })
|
|
706
|
+
};
|
|
707
|
+
const api = await client(ctx);
|
|
708
|
+
const { question, charge } = await preparePlanChoice(api, choice);
|
|
709
|
+
await confirmChange(question, opts.yes);
|
|
710
|
+
ctx.output(await choosePlan(api, choice, charge));
|
|
711
|
+
}));
|
|
712
|
+
credits.command("movements").description("List every change to the credit balance, newest first").option("--limit <n>", "Only the newest n").action((opts) => run(async () => ctx.output(await listCreditMovements(await client(ctx), limitOf(opts.limit)))));
|
|
713
|
+
credits.command("quote <dollars>").description("Price a purchase of credits: bonus, credits granted, card fee and total (whole dollars)").action((dollars) => run(async () => ctx.output(await quoteCredits(await client(ctx), parseDollars(dollars, { wholeDollars: true })))));
|
|
714
|
+
credits.command("buy <dollars>").description("Buy credits with the saved payment method (whole dollars; spends real money and accepts the purchase terms)").option("--purchase-key <uuid>", "Retry an earlier buy without charging twice (printed by that buy)").option("--yes", "Confirm without a prompt", false).action((dollars, opts) => run(async () => {
|
|
715
|
+
const amountCents = parseDollars(dollars, { wholeDollars: true });
|
|
716
|
+
const retrying = opts.purchaseKey !== void 0;
|
|
717
|
+
const purchaseKey = retrying ? opts.purchaseKey.trim() : randomUUID();
|
|
718
|
+
if (retrying && !purchaseKey) throw new BillingError("--purchase-key is empty. Pass the key printed by the earlier buy, or leave the flag out for a new purchase.");
|
|
719
|
+
if (!/^[0-9a-f-]{36}$/i.test(purchaseKey)) throw new BillingError("--purchase-key must be a UUID.");
|
|
720
|
+
const api = await client(ctx);
|
|
721
|
+
const prepared = await prepareCreditPurchase(api, amountCents);
|
|
722
|
+
await confirmChange(retrying ? purchaseRetryQuestion(purchaseKey, prepared.question) : prepared.question, opts.yes);
|
|
723
|
+
let bought;
|
|
724
|
+
try {
|
|
725
|
+
bought = await buyCredits(api, prepared, purchaseKey);
|
|
726
|
+
} catch (err) {
|
|
727
|
+
if (err instanceof Error) {
|
|
728
|
+
err.message = `${err.message} (purchase key ${purchaseKey}; check \`fluid billing credits purchases\`, and retry with --purchase-key ${purchaseKey} rather than a new buy)`;
|
|
729
|
+
const body = err.body;
|
|
730
|
+
if (body && typeof body.error_message === "string") body.error_message = err.message;
|
|
731
|
+
}
|
|
732
|
+
throw err;
|
|
733
|
+
}
|
|
734
|
+
reportCharge(ctx, bought, "purchase");
|
|
735
|
+
}));
|
|
736
|
+
credits.command("purchases").description("List credit purchases, newest first").option("--limit <n>", "Only the newest n").action((opts) => run(async () => ctx.output(await listCreditPurchases(await client(ctx), limitOf(opts.limit))))).command("invoice <id>").description("Get a fresh Stripe invoice link for one purchase").option("--open", "Open the link in the default browser", false).action((id, opts) => run(async () => showLink(ctx, id, await purchaseInvoice(await client(ctx), id), opts.open)));
|
|
737
|
+
credits.command("pay-now").description("Charge the saved payment method now for what the credits are overdrawn by (spends real money)").option("--yes", "Confirm without a prompt", false).action((opts) => run(async () => {
|
|
738
|
+
const api = await client(ctx);
|
|
739
|
+
const prepared = await preparePayNow(api);
|
|
740
|
+
await confirmChange(prepared.question, opts.yes);
|
|
741
|
+
reportCharge(ctx, await payNow(api, prepared.payNow.total_cents), "payment");
|
|
742
|
+
}));
|
|
743
|
+
}
|
|
744
|
+
function registerLimitCommands(parent, ctx) {
|
|
745
|
+
const limits = parent.command("limits").description(`Show spend limits and auto-reload. ${LIMITS_NOTE}`).action(() => run(async () => ctx.output(await showLimits(await client(ctx)))));
|
|
746
|
+
const dollars = "<dollars|none>";
|
|
747
|
+
limits.command("set").description(`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}`).option("--limits <on|off>", "Turn the spend limits on or off").option(`--ai-monthly ${dollars}`, "Monthly AI limit").option(`--ai-daily ${dollars}`, "Daily AI limit").option(`--studios-monthly ${dollars}`, "Monthly Fluid Studios limit").option(`--studios-daily ${dollars}`, "Daily Fluid Studios limit").option(`--orders-monthly ${dollars}`, "Monthly orders limit (notifies)").option(`--hosting-monthly ${dollars}`, "Monthly hosting limit (notifies)").option("--auto-reload <on|off>", "Buy credits with the saved card when the balance runs low; on accepts the reload authorization").option(`--reload-threshold ${dollars}`, "Reload below this balance").option(`--reload-amount ${dollars}`, "What one reload buys").option(`--reload-daily-cap ${dollars}`, "Most reloaded in a day").option(`--reload-monthly-cap ${dollars}`, "Most reloaded in a month").option("--yes", "Confirm without a prompt", false).action((opts) => run(async () => {
|
|
748
|
+
const money = (key, whole = false) => {
|
|
749
|
+
const raw = opts[key];
|
|
750
|
+
return typeof raw === "string" ? parseDollarsOrNone(raw, {
|
|
751
|
+
wholeDollars: whole,
|
|
752
|
+
label: `--${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`
|
|
753
|
+
}) : void 0;
|
|
754
|
+
};
|
|
755
|
+
const changes = {
|
|
756
|
+
limitsEnabled: onOff(opts.limits, "--limits"),
|
|
757
|
+
aiMonthly: money("aiMonthly"),
|
|
758
|
+
aiDaily: money("aiDaily"),
|
|
759
|
+
studiosMonthly: money("studiosMonthly"),
|
|
760
|
+
studiosDaily: money("studiosDaily"),
|
|
761
|
+
ordersMonthly: money("ordersMonthly"),
|
|
762
|
+
hostingMonthly: money("hostingMonthly"),
|
|
763
|
+
autoReload: onOff(opts.autoReload, "--auto-reload"),
|
|
764
|
+
reloadThreshold: money("reloadThreshold", true),
|
|
765
|
+
reloadAmount: money("reloadAmount", true),
|
|
766
|
+
reloadDailyCap: money("reloadDailyCap"),
|
|
767
|
+
reloadMonthlyCap: money("reloadMonthlyCap")
|
|
768
|
+
};
|
|
769
|
+
const api = await client(ctx);
|
|
770
|
+
const { body, question } = limitsUpdate(changes.autoReload === true ? await showLimits(api) : {}, changes);
|
|
771
|
+
await confirmChange(question, opts.yes === true);
|
|
772
|
+
ctx.output(await setLimits(api, body));
|
|
773
|
+
}));
|
|
774
|
+
}
|
|
775
|
+
const plugin = {
|
|
776
|
+
name: "@fluid-app/fluid-cli-billing",
|
|
777
|
+
version: "0.1.0",
|
|
778
|
+
register(ctx) {
|
|
779
|
+
ctx.program.addCommand(createDomainCommand("billing", "Read the company's billing and manage its credits and spend limits", registerBillingCommands));
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
//#endregion
|
|
783
|
+
export { confirmChange, plugin as default, registerBillingCommands, reportCharge };
|
|
784
|
+
|
|
785
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,84 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluid-app/fluid-cli-billing",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Fluid CLI plugin for company billing — invoices, payments, credits and spend limits",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist",
|
|
7
|
+
"README.md"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.mjs",
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": {
|
|
15
|
+
"types": "./dist/index.d.mts",
|
|
16
|
+
"default": "./dist/index.mjs"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@fluid-app/fluid-cli": "0.1.50",
|
|
25
|
+
"commander": "^12.0.0",
|
|
26
|
+
"prompts": "^2.4.2"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@fluid-app/typescript-config": "0.0.0",
|
|
30
|
+
"@swc/core": "^1.15.18",
|
|
31
|
+
"@swc/jest": "^0.2.39",
|
|
32
|
+
"@types/jest": "^29.5.14",
|
|
33
|
+
"@types/node": "24.10.12",
|
|
34
|
+
"@types/prompts": "^2.4.9",
|
|
35
|
+
"jest": "^29.7.0",
|
|
36
|
+
"tsdown": "^0.21.0",
|
|
37
|
+
"typescript": "^5"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=24.0.0"
|
|
41
|
+
},
|
|
42
|
+
"nx": {
|
|
43
|
+
"targets": {
|
|
44
|
+
"typecheck": {
|
|
45
|
+
"dependsOn": [
|
|
46
|
+
{
|
|
47
|
+
"target": "build",
|
|
48
|
+
"projects": [
|
|
49
|
+
"@fluid-app/fluid-cli"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"test": {
|
|
55
|
+
"dependsOn": [
|
|
56
|
+
{
|
|
57
|
+
"target": "build",
|
|
58
|
+
"projects": [
|
|
59
|
+
"@fluid-app/fluid-cli"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
"build": {
|
|
65
|
+
"dependsOn": [
|
|
66
|
+
"typecheck",
|
|
67
|
+
{
|
|
68
|
+
"target": "build",
|
|
69
|
+
"projects": [
|
|
70
|
+
"@fluid-app/fluid-cli"
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "tsdown",
|
|
79
|
+
"dev": "tsdown --watch",
|
|
80
|
+
"typecheck": "tsgo --noEmit",
|
|
81
|
+
"lint": "oxlint",
|
|
82
|
+
"test": "jest"
|
|
83
|
+
}
|
|
6
84
|
}
|