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.
| The plan says what g1t is for without measuring it against other products | 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. | |
| The plan says what g1t is for without measuring it against other products | 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"; | |
| The plan says what g1t is for without measuring it against other products | 13 | import { FORGIVE_COST_MICROS, goodwillWarning } from "./pricing"; |
| 14 | import type { ActionData } from "./review"; | |
| 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"; |
| The plan says what g1t is for without measuring it against other products | 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 } | |
| 23 | | { kind: "enterprise"; accountId: string; name: string; terms: Terms; workspaces: string[]; billingEmail: string | null }; | |
| 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"); |
| The plan says what g1t is for without measuring it against other products | 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 | ||
| 49 | if (intent === "allowances") { | |
| 50 | // The plan without its price, the account's share of g1t's pools, and staff's overrides. | |
| The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404. | 51 | const values = fields(form, "plan", "oss", "trial", "hold", "note"); |
| The plan says what g1t is for without measuring it against other products | 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 | ||
| 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"); |
| The plan says what g1t is for without measuring it against other products | 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); | |
| The plan says what g1t is for without measuring it against other products | 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 | ||
| The plan says what g1t is for without measuring it against other products | 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 | ||
| 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 | ||
| 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 | ||
| Merge Stripe Tax, the card fee on card payments, and one free workspace per person | 204 | if (intent === "billing-address") { |
| 205 | // Where Stripe Tax places the enterprise: an invoice is not sent without it. | |
| 206 | const values = fields(form, "line1", "line2", "city", "state", "postalCode", "country", "taxIdType", "taxId"); | |
| 207 | if (subject.kind !== "enterprise") return failed("top", "Only an enterprise's address is set here; a workspace's owners set their own."); | |
| 208 | if (!subject.billingEmail) return failed("address", "Set where its invoices go first: that makes its Stripe customer.", values); | |
| 209 | const country = values.country.trim().toUpperCase(); | |
| 210 | if (!/^[A-Z]{2}$/.test(country)) return failed("address", "The country is two letters, such as US or DE.", values); | |
| 211 | if (country === "US" && !values.postalCode.trim()) return failed("address", "In the US, Stripe Tax needs the ZIP code.", values); | |
| 212 | const taxIdType = values.taxIdType.trim(); | |
| 213 | const taxId = values.taxId.trim(); | |
| 214 | if (Boolean(taxIdType) !== Boolean(taxId)) return failed("address", "Give the tax ID's kind and its number together, or neither.", values); | |
| 215 | const address = { | |
| 216 | line1: values.line1.trim(), | |
| 217 | line2: values.line2.trim(), | |
| 218 | city: values.city.trim(), | |
| 219 | state: values.state.trim(), | |
| 220 | postalCode: values.postalCode.trim(), | |
| 221 | country, | |
| 222 | }; | |
| 223 | const result = await admin.enterpriseAddress(subject.accountId, address, taxIdType || null, taxId || null, staff.email); | |
| 224 | if (!result.ok) return failed("address", result.error.message, values); | |
| 225 | return back("billing-address"); | |
| 226 | } | |
| 227 | ||
| The plan says what g1t is for without measuring it against other products | 228 | if (intent === "invoice") { |
| 229 | if (subject.kind !== "enterprise") return failed("top", "Only an enterprise is invoiced from sudo."); | |
| 230 | if (!confirmed) { | |
| 231 | return { | |
| 232 | review: { intent, name: subject.name, email: subject.billingEmail, workspaces: subject.workspaces.length, fields: {} }, | |
| 233 | } satisfies ActionData; | |
| 234 | } | |
| 235 | const result = await admin.invoiceEnterprise(subject.accountId, staff.email); | |
| 236 | if (!result.ok) return failed("invoices", result.error.message); | |
| 237 | // Shown in this response; the invoice is also in the list from now on. | |
| 238 | return { invoice: result.value } satisfies ActionData; | |
| 239 | } | |
| 240 | ||
| 241 | return failed("top", "Unknown action."); | |
| 242 | } | |
| 243 | ||
| 244 | /** The workspace's goodwill quote from the Overages queue, if it is in it, and the cap on real cost. */ | |
| 245 | export async function goodwillQuote( | |
| 246 | workspace: string, | |
| 247 | ): Promise<{ goodwill: { creditMicros: number; marginMicros: number }; lastGoodwillAt: string | null; cap: number } | null> { | |
| 248 | const [queue, book] = await Promise.all([admin.overages().catch(() => []), priceBook().catch(() => null)]); | |
| 249 | const row = queue.find((overage) => overage.workspace === workspace); | |
| 250 | const cap = book?.free?.overageForgiveCostMicros || FORGIVE_COST_MICROS; | |
| 251 | return row ? { goodwill: row.goodwill, lastGoodwillAt: row.lastGoodwillAt, cap } : null; | |
| 252 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.