g1t/apps/sudo/app/lib/billing-actions.server.ts

217 lines11,809 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Billing on Stripe's pages, month-end charges, warnings; sudo by workspace1/**
2 * The changes staff make to how a workspace or an enterprise pays: terms,
3 * moving workspaces on and off enterprises, credits, and Stripe billing
4 * links. What is acted on comes from the billing service and identity, not
5 * from the form; each change shows a confirmation first, and a credit
6 * needs the workspace's slug typed out.
7 */
8import type { Terms } from "@g1t/contracts";
9import { data, redirect } from "react-router";
10
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look11import { fields, parseAllowances, parseCredit, parseEmail, parseGoodwill, parseNote, parsePayment, parseSlug, parseTerms, text } from "./forms";
12import { FORGIVE_COST_MICROS, goodwillWarning } from "./pricing";
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace13import type { ActionData } from "./review";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look14import { admin, identity, priceBook } from "./services.server";
sudo: the billing reset no longer names sandbox_months (dropped in 0015), checked against the migrations by a test; a failed reset says why instead of an error page15import { settle } from "./settle";
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace16import type { Staff } from "./staff";
17import type { Enterprise } from "./workspaces";
18
19/** What a page acts on. `accountId` is billing's internal id, never shown. */
20export type Subject =
21 | { kind: "workspace"; slug: string; accountId: string; terms: Terms; billedTo: Enterprise | null }
Stripe webhooks, enterprise invoices, and sudo for both22 | { kind: "enterprise"; accountId: string; name: string; terms: Terms; workspaces: string[]; billingEmail: string | null };
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace23
24function failed(section: string, error: string, values?: Record<string, string>) {
25 return data<ActionData>({ error, section, values }, { status: 422 });
26}
27
28export async function billingAction(request: Request, staff: Staff, subject: Subject, path: string) {
29 const form = await request.formData();
30 const intent = text(form, "intent");
31 const confirmed = text(form, "confirm") === "yes";
32 const back = (done: string) => redirect(`${path}?done=${done}#top`);
33 const isEnterprise = subject.kind === "enterprise";
34
35 if (intent === "terms") {
36 const values = fields(form, "kind", "discount", "ceiling", "note", "until");
37 if (subject.kind === "workspace" && subject.billedTo) {
38 return failed("terms", `This workspace is charged on ${subject.billedTo.name}'s terms. Change them on the enterprise.`, values);
39 }
40 const terms = parseTerms(form, staff.email);
41 if (!terms.ok) return failed("terms", terms.error, values);
42 if (!confirmed) return { review: { intent, before: subject.terms, after: terms.value, fields: values } } satisfies ActionData;
43 const result = await admin.setTerms(subject.accountId, terms.value, staff.email);
44 if (!result.ok) return failed("terms", result.error.message, values);
45 return back("terms");
46 }
47
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put48 if (intent === "allowances") {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look49 // The plan without its price, the account's share of g1t's pools, and staff's overrides.
50 const values = fields(form, "plan", "oss", "trial", "agents", "runCap", "issueCap", "hold", "note");
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put51 if (subject.kind === "workspace" && subject.billedTo) {
52 return failed("allowances", `This workspace is on ${subject.billedTo.name}. Set its plan and pools on the enterprise.`, values);
53 }
54 const allowances = parseAllowances(form);
55 if (!allowances.ok) return failed("allowances", allowances.error, values);
56 const note = parseNote(values.note);
57 if (!note.ok) return failed("allowances", note.error, values);
58 const result = await admin.setAllowances(subject.accountId, allowances.value, note.value, staff.email);
59 if (!result.ok) return failed("allowances", result.error.message, values);
60 return back("allowances");
61 }
62
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace63 if (intent === "attach") {
64 const values = fields(form, "workspace", "target");
65 const section = isEnterprise ? "members" : "billed-to";
66 let slug: string;
67 let target: Enterprise;
68 if (subject.kind === "enterprise") {
69 const parsed = parseSlug(values.workspace);
70 if (!parsed.ok) return failed(section, parsed.error, values);
71 slug = parsed.value;
72 if (subject.workspaces.includes(slug)) return failed(section, `${slug} is already on this enterprise.`, values);
73 if (!(await identity.workspace(slug))) return failed(section, `There is no workspace called ${slug}.`, values);
74 target = { id: subject.accountId, name: subject.name };
75 } else {
76 slug = subject.slug;
77 const enterprise = (await admin.accounts()).find((row) => row.account.kind === "enterprise" && row.account.id === values.target);
78 if (!enterprise) return failed(section, "Choose an enterprise to move onto.", values);
79 target = { id: enterprise.account.id, name: enterprise.account.name };
80 }
81 if (!confirmed) return { review: { intent, workspace: slug, targetName: target.name, fields: values } } satisfies ActionData;
82 const result = await admin.attach(slug, target.id, staff.email);
83 if (!result.ok) return failed(section, result.error.message, values);
84 return back("attach");
85 }
86
87 if (intent === "detach") {
88 const values = fields(form, "workspace");
89 const section = isEnterprise ? "members" : "billed-to";
90 let slug: string;
91 let from: string;
92 if (subject.kind === "enterprise") {
93 slug = values.workspace;
94 if (!subject.workspaces.includes(slug)) return failed(section, `${slug} is not on this enterprise.`);
95 from = subject.name;
96 } else {
97 if (!subject.billedTo) return failed(section, `${subject.slug} already pays for itself.`);
98 slug = subject.slug;
99 from = subject.billedTo.name;
100 }
101 if (!confirmed) return { review: { intent, workspace: slug, from, fields: values } } satisfies ActionData;
102 const result = await admin.attach(slug, null, staff.email);
103 if (!result.ok) return failed(section, result.error.message);
104 return back("detach");
105 }
106
107 if (intent === "credit") {
108 const values = fields(form, "workspace", "amount", "note", "confirmation");
109 const workspace = subject.kind === "enterprise" ? values.workspace : subject.slug;
110 if (subject.kind === "enterprise" && !subject.workspaces.includes(workspace)) {
111 return failed("credit", "Choose one of this enterprise's workspaces.", values);
112 }
113 const amount = parseCredit(values.amount);
114 if (!amount.ok) return failed("credit", amount.error, values);
115 const note = parseNote(values.note);
116 if (!note.ok) return failed("credit", note.error, values);
117 if (values.confirmation !== workspace) {
118 return failed("credit", `Type the workspace's slug, ${workspace}, exactly, to issue the credit.`, { ...values, confirmation: "" });
119 }
120 const result = await admin.credit(workspace, amount.value, note.value, staff.email);
121 if (!result.ok) return failed("credit", result.error.message, values);
122 return back("credit");
123 }
124
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's125 if (intent === "reset") {
126 // A test workspace's billing wiped. Billing refuses it on a live Stripe
127 // key, for comped workspaces and for an enterprise's.
128 const values = fields(form, "note", "confirmation");
129 if (subject.kind !== "workspace") return failed("top", "Reset a workspace, not an enterprise.");
130 const note = parseNote(values.note);
131 if (!note.ok) return failed("reset", note.error, values);
132 if (values.confirmation !== subject.slug) {
133 return failed("reset", `Type the workspace's slug, ${subject.slug}, exactly, to reset it.`, { ...values, confirmation: "" });
134 }
sudo: the billing reset no longer names sandbox_months (dropped in 0015), checked against the migrations by a test; a failed reset says why instead of an error page135 const result = await settle(admin.resetBilling(subject.slug, values.confirmation, note.value, staff.email));
136 if (!result.ok) return failed("reset", `Billing did not answer: ${result.error}`, values);
137 if (!result.value.ok) return failed("reset", result.value.error.message, values);
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's138 return back("reset");
139 }
140
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look141 if (intent === "payment") {
142 // A bank transfer that reached g1t outside Stripe's page.
143 const values = fields(form, "amount", "reference", "note", "confirmation");
144 if (subject.kind !== "workspace") return failed("top", "Record a payment on the workspace it is for.");
145 const payment = parsePayment(form, subject.slug);
146 if (!payment.ok) return failed("payment", payment.error, { ...values, confirmation: "" });
147 const { amountMicros, reference, note } = payment.value;
148 const result = await admin.recordPayment(subject.slug, amountMicros, reference, note, staff.email);
149 if (!result.ok) return failed("payment", result.error.message, values);
150 return back("payment");
151 }
152
153 if (intent === "goodwill") {
154 // A goodwill credit from the workspace's page: the overage, as billing quotes it now.
155 const values = fields(form, "amount", "reason", "day");
156 if (subject.kind !== "workspace") return failed("top", "Goodwill is given to one workspace.");
157 const quote = await goodwillQuote(subject.slug);
158 const parsed = parseGoodwill(form, (amount) =>
159 // A reason past the one-click credit, a second within 12 months, or past the cap on real cost.
160 quote ? ((w) => w.needsReason || w.overCap)(goodwillWarning(amount, quote.goodwill, quote.lastGoodwillAt, quote.cap)) : amount != null,
161 );
162 if (!parsed.ok) return failed("goodwill", parsed.error, values);
163 const result = await admin.goodwill(subject.slug, parsed.value.amountMicros, parsed.value.reason, staff.email, parsed.value.day);
164 if (!result.ok) return failed("goodwill", result.error.message, values);
165 return back("goodwill");
166 }
167
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace168 if (intent === "billing-link") {
169 if (subject.kind !== "workspace") return failed("top", "Billing links are made from a workspace's page.");
170 if (!confirmed) return { review: { intent, workspace: subject.slug, fields: {} } } satisfies ActionData;
171 const result = await admin.billingLink(subject.slug, staff.email);
172 if (!result.ok) return failed("billing-link", result.error.message);
173 // Shown once, in this response only: it is never stored or redirected to.
174 return { link: result.value, workspace: subject.slug } satisfies ActionData;
175 }
176
Stripe webhooks, enterprise invoices, and sudo for both177 if (intent === "billing-email") {
178 const values = fields(form, "email");
179 if (subject.kind !== "enterprise") return failed("top", "Only an enterprise has an invoice email.");
180 const email = parseEmail(values.email);
181 if (!email.ok) return failed("invoices", email.error, values);
182 if (email.value === subject.billingEmail) return failed("invoices", "That is already where its invoices go.", values);
183 if (!confirmed) {
184 return {
185 review: { intent, name: subject.name, before: subject.billingEmail, after: email.value, fields: { email: email.value } },
186 } satisfies ActionData;
187 }
188 const result = await admin.enterpriseBilling(subject.accountId, email.value, staff.email);
189 if (!result.ok) return failed("invoices", result.error.message, values);
190 return back("billing-email");
191 }
192
193 if (intent === "invoice") {
194 if (subject.kind !== "enterprise") return failed("top", "Only an enterprise is invoiced from sudo.");
195 if (!confirmed) {
196 return {
197 review: { intent, name: subject.name, email: subject.billingEmail, workspaces: subject.workspaces.length, fields: {} },
198 } satisfies ActionData;
199 }
200 const result = await admin.invoiceEnterprise(subject.accountId, staff.email);
201 if (!result.ok) return failed("invoices", result.error.message);
202 // Shown in this response; the invoice is also in the list from now on.
203 return { invoice: result.value } satisfies ActionData;
204 }
205
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace206 return failed("top", "Unknown action.");
207}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look208
209/** The workspace's goodwill quote from the Overages queue, if it is in it, and the cap on real cost. */
210export async function goodwillQuote(
211 workspace: string,
212): Promise<{ goodwill: { creditMicros: number; marginMicros: number }; lastGoodwillAt: string | null; cap: number } | null> {
213 const [queue, book] = await Promise.all([admin.overages().catch(() => []), priceBook().catch(() => null)]);
214 const row = queue.find((overage) => overage.workspace === workspace);
215 const cap = book?.free?.overageForgiveCostMicros || FORGIVE_COST_MICROS;
216 return row ? { goodwill: row.goodwill, lastGoodwillAt: row.lastGoodwillAt, cap } : null;
217}