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 workspace | 1 | /** |
| 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 | |
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 5 | * from the form; each change shows a confirmation first, and a credit over |
| 6 | * $100 needs the workspace's slug typed out. | |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 7 | */ |
| 8 | import type { Terms } from "@g1t/contracts"; | |
| 9 | import { data, redirect } from "react-router"; | |
| 10 | ||
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 11 | import { parseCreditForm } from "./credits"; |
| 12 | import { fields, parseAllowances, parseEmail, parseGoodwill, parseNote, parsePayment, parseSlug, parseTerms, text } from "./forms"; | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 13 | import { FORGIVE_COST_MICROS, goodwillWarning } from "./pricing"; |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 14 | import type { ActionData } from "./review"; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 15 | import { 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 page | 16 | import { settle } from "./settle"; |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 17 | import type { Staff } from "./staff"; |
| 18 | import type { Enterprise } from "./workspaces"; | |
| 19 | ||
| 20 | /** What a page acts on. `accountId` is billing's internal id, never shown. */ | |
| 21 | export type Subject = | |
| 22 | | { kind: "workspace"; slug: string; accountId: string; terms: Terms; billedTo: Enterprise | null } | |
| Stripe webhooks, enterprise invoices, and sudo for both | 23 | | { kind: "enterprise"; accountId: string; name: string; terms: Terms; workspaces: string[]; billingEmail: string | null }; |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 24 | |
| 25 | function failed(section: string, error: string, values?: Record<string, string>) { | |
| 26 | return data<ActionData>({ error, section, values }, { status: 422 }); | |
| 27 | } | |
| 28 | ||
| 29 | export async function billingAction(request: Request, staff: Staff, subject: Subject, path: string) { | |
| 30 | const form = await request.formData(); | |
| 31 | const intent = text(form, "intent"); | |
| 32 | const confirmed = text(form, "confirm") === "yes"; | |
| 33 | const back = (done: string) => redirect(`${path}?done=${done}#top`); | |
| 34 | const isEnterprise = subject.kind === "enterprise"; | |
| 35 | ||
| 36 | if (intent === "terms") { | |
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 37 | const values = fields(form, "preset", "discount", "ceiling", "note", "until"); |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 38 | if (subject.kind === "workspace" && subject.billedTo) { |
| 39 | return failed("terms", `This workspace is charged on ${subject.billedTo.name}'s terms. Change them on the enterprise.`, values); | |
| 40 | } | |
| 41 | const terms = parseTerms(form, staff.email); | |
| 42 | if (!terms.ok) return failed("terms", terms.error, values); | |
| 43 | if (!confirmed) return { review: { intent, before: subject.terms, after: terms.value, fields: values } } satisfies ActionData; | |
| 44 | const result = await admin.setTerms(subject.accountId, terms.value, staff.email); | |
| 45 | if (!result.ok) return failed("terms", result.error.message, values); | |
| 46 | return back("terms"); | |
| 47 | } | |
| 48 | ||
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 49 | if (intent === "allowances") { |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 50 | // The plan without its price, the account's share of g1t's pools, and staff's overrides. |
| 51 | 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 put | 52 | if (subject.kind === "workspace" && subject.billedTo) { |
| 53 | return failed("allowances", `This workspace is on ${subject.billedTo.name}. Set its plan and pools on the enterprise.`, values); | |
| 54 | } | |
| 55 | const allowances = parseAllowances(form); | |
| 56 | if (!allowances.ok) return failed("allowances", allowances.error, values); | |
| 57 | const note = parseNote(values.note); | |
| 58 | if (!note.ok) return failed("allowances", note.error, values); | |
| 59 | const result = await admin.setAllowances(subject.accountId, allowances.value, note.value, staff.email); | |
| 60 | if (!result.ok) return failed("allowances", result.error.message, values); | |
| 61 | return back("allowances"); | |
| 62 | } | |
| 63 | ||
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 64 | if (intent === "attach") { |
| 65 | const values = fields(form, "workspace", "target"); | |
| 66 | const section = isEnterprise ? "members" : "billed-to"; | |
| 67 | let slug: string; | |
| 68 | let target: Enterprise; | |
| 69 | if (subject.kind === "enterprise") { | |
| 70 | const parsed = parseSlug(values.workspace); | |
| 71 | if (!parsed.ok) return failed(section, parsed.error, values); | |
| 72 | slug = parsed.value; | |
| 73 | if (subject.workspaces.includes(slug)) return failed(section, `${slug} is already on this enterprise.`, values); | |
| 74 | if (!(await identity.workspace(slug))) return failed(section, `There is no workspace called ${slug}.`, values); | |
| 75 | target = { id: subject.accountId, name: subject.name }; | |
| 76 | } else { | |
| 77 | slug = subject.slug; | |
| 78 | const enterprise = (await admin.accounts()).find((row) => row.account.kind === "enterprise" && row.account.id === values.target); | |
| 79 | if (!enterprise) return failed(section, "Choose an enterprise to move onto.", values); | |
| 80 | target = { id: enterprise.account.id, name: enterprise.account.name }; | |
| 81 | } | |
| 82 | if (!confirmed) return { review: { intent, workspace: slug, targetName: target.name, fields: values } } satisfies ActionData; | |
| 83 | const result = await admin.attach(slug, target.id, staff.email); | |
| 84 | if (!result.ok) return failed(section, result.error.message, values); | |
| 85 | return back("attach"); | |
| 86 | } | |
| 87 | ||
| 88 | if (intent === "detach") { | |
| 89 | const values = fields(form, "workspace"); | |
| 90 | const section = isEnterprise ? "members" : "billed-to"; | |
| 91 | let slug: string; | |
| 92 | let from: string; | |
| 93 | if (subject.kind === "enterprise") { | |
| 94 | slug = values.workspace; | |
| 95 | if (!subject.workspaces.includes(slug)) return failed(section, `${slug} is not on this enterprise.`); | |
| 96 | from = subject.name; | |
| 97 | } else { | |
| 98 | if (!subject.billedTo) return failed(section, `${subject.slug} already pays for itself.`); | |
| 99 | slug = subject.slug; | |
| 100 | from = subject.billedTo.name; | |
| 101 | } | |
| 102 | if (!confirmed) return { review: { intent, workspace: slug, from, fields: values } } satisfies ActionData; | |
| 103 | const result = await admin.attach(slug, null, staff.email); | |
| 104 | if (!result.ok) return failed(section, result.error.message); | |
| 105 | return back("detach"); | |
| 106 | } | |
| 107 | ||
| 108 | if (intent === "credit") { | |
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 109 | const values = fields(form, "workspace", "preset", "amount", "kind", "expires", "expiresOn", "refundFor", "refundDay", "note"); |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 110 | const workspace = subject.kind === "enterprise" ? values.workspace : subject.slug; |
| 111 | if (subject.kind === "enterprise" && !subject.workspaces.includes(workspace)) { | |
| 112 | return failed("credit", "Choose one of this enterprise's workspaces.", values); | |
| 113 | } | |
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 114 | const credit = parseCreditForm(form, workspace); |
| 115 | if (!credit.ok) return failed("credit", credit.error, values); | |
| 116 | const { amountMicros, note, ...options } = credit.value; | |
| 117 | const result = await admin.credit(workspace, amountMicros, note, staff.email, options); | |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 118 | if (!result.ok) return failed("credit", result.error.message, values); |
| 119 | return back("credit"); | |
| 120 | } | |
| 121 | ||
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 122 | if (intent === "revoke-credit") { |
| 123 | // What is left of a grant, taken back. The grant must be one of the page's. | |
| 124 | const values = fields(form, "id", "reason"); | |
| 125 | const workspaces = subject.kind === "enterprise" ? subject.workspaces : [subject.slug]; | |
| 126 | const reason = parseNote(values.reason); | |
| 127 | if (!reason.ok) return failed("credits", reason.error, values); | |
| 128 | const { grants } = await admin.credits(subject.kind === "workspace" ? { workspace: subject.slug } : {}); | |
| 129 | const grant = grants.find((row) => row.id === values.id && workspaces.includes(row.workspace)); | |
| 130 | if (!grant) return failed("credits", "That credit is not this page's, or is gone.", values); | |
| 131 | const result = await admin.revokeCredit(grant.id, reason.value, staff.email); | |
| 132 | if (!result.ok) return failed("credits", result.error.message, values); | |
| 133 | return back("revoked"); | |
| 134 | } | |
| 135 | ||
| 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's | 136 | if (intent === "reset") { |
| 137 | // A test workspace's billing wiped. Billing refuses it on a live Stripe | |
| 138 | // key, for comped workspaces and for an enterprise's. | |
| 139 | const values = fields(form, "note", "confirmation"); | |
| 140 | if (subject.kind !== "workspace") return failed("top", "Reset a workspace, not an enterprise."); | |
| 141 | const note = parseNote(values.note); | |
| 142 | if (!note.ok) return failed("reset", note.error, values); | |
| 143 | if (values.confirmation !== subject.slug) { | |
| 144 | return failed("reset", `Type the workspace's slug, ${subject.slug}, exactly, to reset it.`, { ...values, confirmation: "" }); | |
| 145 | } | |
| 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 page | 146 | const result = await settle(admin.resetBilling(subject.slug, values.confirmation, note.value, staff.email)); |
| 147 | if (!result.ok) return failed("reset", `Billing did not answer: ${result.error}`, values); | |
| 148 | if (!result.value.ok) return failed("reset", result.value.error.message, values); | |
| sudo: a billing reset runs the costs analysis again so every figure is fresh; every submit button shows it is working (CSS only); no margin percentage on less than a cent sold | 149 | return back(result.value.value.refreshed === false ? "reset-stale" : "reset"); |
| 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's | 150 | } |
| 151 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 152 | if (intent === "payment") { |
| 153 | // A bank transfer that reached g1t outside Stripe's page. | |
| 154 | const values = fields(form, "amount", "reference", "note", "confirmation"); | |
| 155 | if (subject.kind !== "workspace") return failed("top", "Record a payment on the workspace it is for."); | |
| 156 | const payment = parsePayment(form, subject.slug); | |
| 157 | if (!payment.ok) return failed("payment", payment.error, { ...values, confirmation: "" }); | |
| 158 | const { amountMicros, reference, note } = payment.value; | |
| 159 | const result = await admin.recordPayment(subject.slug, amountMicros, reference, note, staff.email); | |
| 160 | if (!result.ok) return failed("payment", result.error.message, values); | |
| 161 | return back("payment"); | |
| 162 | } | |
| 163 | ||
| 164 | if (intent === "goodwill") { | |
| 165 | // A goodwill credit from the workspace's page: the overage, as billing quotes it now. | |
| 166 | const values = fields(form, "amount", "reason", "day"); | |
| 167 | if (subject.kind !== "workspace") return failed("top", "Goodwill is given to one workspace."); | |
| 168 | const quote = await goodwillQuote(subject.slug); | |
| 169 | const parsed = parseGoodwill(form, (amount) => | |
| 170 | // A reason past the one-click credit, a second within 12 months, or past the cap on real cost. | |
| 171 | quote ? ((w) => w.needsReason || w.overCap)(goodwillWarning(amount, quote.goodwill, quote.lastGoodwillAt, quote.cap)) : amount != null, | |
| 172 | ); | |
| 173 | if (!parsed.ok) return failed("goodwill", parsed.error, values); | |
| 174 | const result = await admin.goodwill(subject.slug, parsed.value.amountMicros, parsed.value.reason, staff.email, parsed.value.day); | |
| 175 | if (!result.ok) return failed("goodwill", result.error.message, values); | |
| 176 | return back("goodwill"); | |
| 177 | } | |
| 178 | ||
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 179 | if (intent === "billing-link") { |
| 180 | if (subject.kind !== "workspace") return failed("top", "Billing links are made from a workspace's page."); | |
| 181 | if (!confirmed) return { review: { intent, workspace: subject.slug, fields: {} } } satisfies ActionData; | |
| 182 | const result = await admin.billingLink(subject.slug, staff.email); | |
| 183 | if (!result.ok) return failed("billing-link", result.error.message); | |
| 184 | // Shown once, in this response only: it is never stored or redirected to. | |
| 185 | return { link: result.value, workspace: subject.slug } satisfies ActionData; | |
| 186 | } | |
| 187 | ||
| Stripe webhooks, enterprise invoices, and sudo for both | 188 | if (intent === "billing-email") { |
| 189 | const values = fields(form, "email"); | |
| 190 | if (subject.kind !== "enterprise") return failed("top", "Only an enterprise has an invoice email."); | |
| 191 | const email = parseEmail(values.email); | |
| 192 | if (!email.ok) return failed("invoices", email.error, values); | |
| 193 | if (email.value === subject.billingEmail) return failed("invoices", "That is already where its invoices go.", values); | |
| 194 | if (!confirmed) { | |
| 195 | return { | |
| 196 | review: { intent, name: subject.name, before: subject.billingEmail, after: email.value, fields: { email: email.value } }, | |
| 197 | } satisfies ActionData; | |
| 198 | } | |
| 199 | const result = await admin.enterpriseBilling(subject.accountId, email.value, staff.email); | |
| 200 | if (!result.ok) return failed("invoices", result.error.message, values); | |
| 201 | return back("billing-email"); | |
| 202 | } | |
| 203 | ||
| 204 | if (intent === "invoice") { | |
| 205 | if (subject.kind !== "enterprise") return failed("top", "Only an enterprise is invoiced from sudo."); | |
| 206 | if (!confirmed) { | |
| 207 | return { | |
| 208 | review: { intent, name: subject.name, email: subject.billingEmail, workspaces: subject.workspaces.length, fields: {} }, | |
| 209 | } satisfies ActionData; | |
| 210 | } | |
| 211 | const result = await admin.invoiceEnterprise(subject.accountId, staff.email); | |
| 212 | if (!result.ok) return failed("invoices", result.error.message); | |
| 213 | // Shown in this response; the invoice is also in the list from now on. | |
| 214 | return { invoice: result.value } satisfies ActionData; | |
| 215 | } | |
| 216 | ||
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 217 | return failed("top", "Unknown action."); |
| 218 | } | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 219 | |
| 220 | /** The workspace's goodwill quote from the Overages queue, if it is in it, and the cap on real cost. */ | |
| 221 | export async function goodwillQuote( | |
| 222 | workspace: string, | |
| 223 | ): Promise<{ goodwill: { creditMicros: number; marginMicros: number }; lastGoodwillAt: string | null; cap: number } | null> { | |
| 224 | const [queue, book] = await Promise.all([admin.overages().catch(() => []), priceBook().catch(() => null)]); | |
| 225 | const row = queue.find((overage) => overage.workspace === workspace); | |
| 226 | const cap = book?.free?.overageForgiveCostMicros || FORGIVE_COST_MICROS; | |
| 227 | return row ? { goodwill: row.goodwill, lastGoodwillAt: row.lastGoodwillAt, cap } : null; | |
| 228 | } |