flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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

162 lines8,372 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
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put11import { fields, parseAllowances, parseCredit, parseEmail, parseNote, parseSlug, parseTerms, text } from "./forms";
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace12import type { ActionData } from "./review";
13import { admin, identity } from "./services.server";
14import type { Staff } from "./staff";
15import type { Enterprise } from "./workspaces";
16
17/** What a page acts on. `accountId` is billing's internal id, never shown. */
18export type Subject =
19 | { kind: "workspace"; slug: string; accountId: string; terms: Terms; billedTo: Enterprise | null }
Stripe webhooks, enterprise invoices, and sudo for both20 | { kind: "enterprise"; accountId: string; name: string; terms: Terms; workspaces: string[]; billingEmail: string | null };
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace21
22function failed(section: string, error: string, values?: Record<string, string>) {
23 return data<ActionData>({ error, section, values }, { status: 422 });
24}
25
26export async function billingAction(request: Request, staff: Staff, subject: Subject, path: string) {
27 const form = await request.formData();
28 const intent = text(form, "intent");
29 const confirmed = text(form, "confirm") === "yes";
30 const back = (done: string) => redirect(`${path}?done=${done}#top`);
31 const isEnterprise = subject.kind === "enterprise";
32
33 if (intent === "terms") {
34 const values = fields(form, "kind", "discount", "ceiling", "note", "until");
35 if (subject.kind === "workspace" && subject.billedTo) {
36 return failed("terms", `This workspace is charged on ${subject.billedTo.name}'s terms. Change them on the enterprise.`, values);
37 }
38 const terms = parseTerms(form, staff.email);
39 if (!terms.ok) return failed("terms", terms.error, values);
40 if (!confirmed) return { review: { intent, before: subject.terms, after: terms.value, fields: values } } satisfies ActionData;
41 const result = await admin.setTerms(subject.accountId, terms.value, staff.email);
42 if (!result.ok) return failed("terms", result.error.message, values);
43 return back("terms");
44 }
45
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put46 if (intent === "allowances") {
47 // Team without charge, and the account's share of g1t's pools.
48 const values = fields(form, "team", "oss", "trial", "note");
49 if (subject.kind === "workspace" && subject.billedTo) {
50 return failed("allowances", `This workspace is on ${subject.billedTo.name}. Set its plan and pools on the enterprise.`, values);
51 }
52 const allowances = parseAllowances(form);
53 if (!allowances.ok) return failed("allowances", allowances.error, values);
54 const note = parseNote(values.note);
55 if (!note.ok) return failed("allowances", note.error, values);
56 const result = await admin.setAllowances(subject.accountId, allowances.value, note.value, staff.email);
57 if (!result.ok) return failed("allowances", result.error.message, values);
58 return back("allowances");
59 }
60
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace61 if (intent === "attach") {
62 const values = fields(form, "workspace", "target");
63 const section = isEnterprise ? "members" : "billed-to";
64 let slug: string;
65 let target: Enterprise;
66 if (subject.kind === "enterprise") {
67 const parsed = parseSlug(values.workspace);
68 if (!parsed.ok) return failed(section, parsed.error, values);
69 slug = parsed.value;
70 if (subject.workspaces.includes(slug)) return failed(section, `${slug} is already on this enterprise.`, values);
71 if (!(await identity.workspace(slug))) return failed(section, `There is no workspace called ${slug}.`, values);
72 target = { id: subject.accountId, name: subject.name };
73 } else {
74 slug = subject.slug;
75 const enterprise = (await admin.accounts()).find((row) => row.account.kind === "enterprise" && row.account.id === values.target);
76 if (!enterprise) return failed(section, "Choose an enterprise to move onto.", values);
77 target = { id: enterprise.account.id, name: enterprise.account.name };
78 }
79 if (!confirmed) return { review: { intent, workspace: slug, targetName: target.name, fields: values } } satisfies ActionData;
80 const result = await admin.attach(slug, target.id, staff.email);
81 if (!result.ok) return failed(section, result.error.message, values);
82 return back("attach");
83 }
84
85 if (intent === "detach") {
86 const values = fields(form, "workspace");
87 const section = isEnterprise ? "members" : "billed-to";
88 let slug: string;
89 let from: string;
90 if (subject.kind === "enterprise") {
91 slug = values.workspace;
92 if (!subject.workspaces.includes(slug)) return failed(section, `${slug} is not on this enterprise.`);
93 from = subject.name;
94 } else {
95 if (!subject.billedTo) return failed(section, `${subject.slug} already pays for itself.`);
96 slug = subject.slug;
97 from = subject.billedTo.name;
98 }
99 if (!confirmed) return { review: { intent, workspace: slug, from, fields: values } } satisfies ActionData;
100 const result = await admin.attach(slug, null, staff.email);
101 if (!result.ok) return failed(section, result.error.message);
102 return back("detach");
103 }
104
105 if (intent === "credit") {
106 const values = fields(form, "workspace", "amount", "note", "confirmation");
107 const workspace = subject.kind === "enterprise" ? values.workspace : subject.slug;
108 if (subject.kind === "enterprise" && !subject.workspaces.includes(workspace)) {
109 return failed("credit", "Choose one of this enterprise's workspaces.", values);
110 }
111 const amount = parseCredit(values.amount);
112 if (!amount.ok) return failed("credit", amount.error, values);
113 const note = parseNote(values.note);
114 if (!note.ok) return failed("credit", note.error, values);
115 if (values.confirmation !== workspace) {
116 return failed("credit", `Type the workspace's slug, ${workspace}, exactly, to issue the credit.`, { ...values, confirmation: "" });
117 }
118 const result = await admin.credit(workspace, amount.value, note.value, staff.email);
119 if (!result.ok) return failed("credit", result.error.message, values);
120 return back("credit");
121 }
122
123 if (intent === "billing-link") {
124 if (subject.kind !== "workspace") return failed("top", "Billing links are made from a workspace's page.");
125 if (!confirmed) return { review: { intent, workspace: subject.slug, fields: {} } } satisfies ActionData;
126 const result = await admin.billingLink(subject.slug, staff.email);
127 if (!result.ok) return failed("billing-link", result.error.message);
128 // Shown once, in this response only: it is never stored or redirected to.
129 return { link: result.value, workspace: subject.slug } satisfies ActionData;
130 }
131
Stripe webhooks, enterprise invoices, and sudo for both132 if (intent === "billing-email") {
133 const values = fields(form, "email");
134 if (subject.kind !== "enterprise") return failed("top", "Only an enterprise has an invoice email.");
135 const email = parseEmail(values.email);
136 if (!email.ok) return failed("invoices", email.error, values);
137 if (email.value === subject.billingEmail) return failed("invoices", "That is already where its invoices go.", values);
138 if (!confirmed) {
139 return {
140 review: { intent, name: subject.name, before: subject.billingEmail, after: email.value, fields: { email: email.value } },
141 } satisfies ActionData;
142 }
143 const result = await admin.enterpriseBilling(subject.accountId, email.value, staff.email);
144 if (!result.ok) return failed("invoices", result.error.message, values);
145 return back("billing-email");
146 }
147
148 if (intent === "invoice") {
149 if (subject.kind !== "enterprise") return failed("top", "Only an enterprise is invoiced from sudo.");
150 if (!confirmed) {
151 return {
152 review: { intent, name: subject.name, email: subject.billingEmail, workspaces: subject.workspaces.length, fields: {} },
153 } satisfies ActionData;
154 }
155 const result = await admin.invoiceEnterprise(subject.accountId, staff.email);
156 if (!result.ok) return failed("invoices", result.error.message);
157 // Shown in this response; the invoice is also in the list from now on.
158 return { invoice: result.value } satisfies ActionData;
159 }
160
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace161 return failed("top", "Unknown action.");
162}