g1t/apps/sudo/app/lib/workspaces.ts
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 | * sudo is organised around workspaces, as customers know them. Identity | |
| 3 | * says which workspaces exist and who owns them; billing says who pays for | |
| 4 | * each and where it stands. This joins the two, by slug. No Workers | |
| 5 | * imports, so it can be tested under Node. | |
| 6 | * | |
| 7 | * Billing's account ids (`ws_<slug>`, `ent_…`) are internal: they are used | |
| 8 | * to look things up and never shown as a name. | |
| 9 | */ | |
| 10 | import type { AccountSummary, AdminOwner, AdminWorkspace, Limit, Terms } from "@g1t/contracts"; | |
| 11 | ||
| 12 | /** Terms every workspace starts on. */ | |
| 13 | export const STANDARD_TERMS: Terms = { | |
| 14 | kind: "standard", | |
| 15 | discountPercent: 0, | |
| 16 | ceilingMicros: null, | |
| 17 | note: "", | |
| 18 | until: null, | |
| 19 | setBy: null, | |
| 20 | setAt: null, | |
| 21 | }; | |
| 22 | ||
| 23 | export type Enterprise = { id: string; name: string }; | |
| 24 | ||
| 25 | /** Where a workspace stands with billing. */ | |
| 26 | export type WorkspaceBilling = { | |
| 27 | /** The enterprise that pays for it, or null when it pays for itself. */ | |
| 28 | billedTo: Enterprise | null; | |
| 29 | /** The terms it is charged on: its own, or its enterprise's. */ | |
| 30 | terms: Terms; | |
| 31 | /** | |
| 32 | * Its limit this month: its own, or for a workspace on an enterprise the | |
| 33 | * enterprise's, which all its workspaces share. Null with no billing | |
| 34 | * activity yet. | |
| 35 | */ | |
| 36 | limit: Limit | null; | |
| 37 | /** This workspace's own figures this month (charged, cost) and ever (paid). */ | |
| 38 | chargedMicros: number; | |
| 39 | costMicros: number; | |
| 40 | paidMicros: number; | |
| 41 | }; | |
| 42 | ||
| 43 | export type WorkspaceRow = { | |
| 44 | slug: string; | |
| 45 | name: string; | |
| Stripe webhooks, enterprise invoices, and sudo for both | 46 | createdAt: string; |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 47 | owners: AdminOwner[]; |
| Stripe webhooks, enterprise invoices, and sudo for both | 48 | memberCount: number; |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 49 | billing: WorkspaceBilling; |
| 50 | }; | |
| 51 | ||
| 52 | /** The billing side of every workspace billing knows, by slug. */ | |
| 53 | export function billingBySlug(accounts: AccountSummary[]): Map<string, WorkspaceBilling> { | |
| 54 | const bySlug = new Map<string, WorkspaceBilling>(); | |
| 55 | for (const summary of accounts) { | |
| 56 | const { account } = summary; | |
| 57 | const shares = new Map((summary.byWorkspace ?? []).map((share) => [share.workspace, share])); | |
| 58 | if (account.kind === "enterprise") { | |
| 59 | for (const slug of account.workspaces) { | |
| 60 | const share = shares.get(slug); | |
| 61 | bySlug.set(slug, { | |
| 62 | billedTo: { id: account.id, name: account.name }, | |
| 63 | terms: account.terms, | |
| 64 | limit: summary.limit, | |
| 65 | chargedMicros: share?.chargedMicros ?? 0, | |
| 66 | costMicros: share?.costMicros ?? 0, | |
| 67 | paidMicros: share?.paidMicros ?? 0, | |
| 68 | }); | |
| 69 | } | |
| 70 | } else { | |
| 71 | const slug = account.workspaces[0] ?? summary.limit.workspace; | |
| 72 | // An enterprise's claim on a workspace wins over a stale own account. | |
| 73 | if (bySlug.get(slug)?.billedTo) continue; | |
| 74 | bySlug.set(slug, { | |
| 75 | billedTo: null, | |
| 76 | terms: account.terms, | |
| 77 | limit: summary.limit, | |
| 78 | chargedMicros: summary.chargedMicros, | |
| 79 | costMicros: summary.costMicros, | |
| 80 | paidMicros: summary.paidMicros, | |
| 81 | }); | |
| 82 | } | |
| 83 | } | |
| 84 | return bySlug; | |
| 85 | } | |
| 86 | ||
| 87 | /** A workspace with no billing activity: standard terms, nothing charged. */ | |
| 88 | export function noBilling(): WorkspaceBilling { | |
| 89 | return { billedTo: null, terms: STANDARD_TERMS, limit: null, chargedMicros: 0, costMicros: 0, paidMicros: 0 }; | |
| 90 | } | |
| 91 | ||
| 92 | /** | |
| Stripe webhooks, enterprise invoices, and sudo for both | 93 | * The workspaces identity listed, in its order, each with its billing. Only |
| 94 | * these: an enterprise's other workspaces, which billing returns with it, | |
| 95 | * are not added. A workspace with no billing activity is still listed, on | |
| 96 | * standard terms at $0. | |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 97 | */ |
| 98 | export function joinWorkspaces(workspaces: AdminWorkspace[], accounts: AccountSummary[]): WorkspaceRow[] { | |
| 99 | const billing = billingBySlug(accounts); | |
| Stripe webhooks, enterprise invoices, and sudo for both | 100 | return workspaces.map((workspace) => ({ |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 101 | slug: workspace.slug, |
| 102 | name: workspace.name, | |
| 103 | createdAt: workspace.createdAt, | |
| 104 | owners: workspace.owners, | |
| 105 | memberCount: workspace.memberCount, | |
| 106 | billing: billing.get(workspace.slug) ?? noBilling(), | |
| 107 | })); | |
| 108 | } | |
| 109 | ||
| Stripe webhooks, enterprise invoices, and sudo for both | 110 | export const PAGE_SIZE = 50; |
| 111 | ||
| 112 | /** One page of a list: the page asked for, kept within the pages there are. */ | |
| 113 | export function paginate<T>(items: T[], requested: string | null, size = PAGE_SIZE) { | |
| 114 | const pages = Math.max(1, Math.ceil(items.length / size)); | |
| 115 | const asked = Number.parseInt(requested ?? "1", 10); | |
| 116 | const page = Number.isFinite(asked) ? Math.min(Math.max(asked, 1), pages) : 1; | |
| 117 | return { page, pages, items: items.slice((page - 1) * size, page * size) }; | |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 118 | } |
| 119 | ||
| 120 | /** | |
| 121 | * Where an old `/accounts/<id>` link now lives: a workspace's own account | |
| 122 | * (`ws_<slug>`, or a bare slug) at its workspace, an enterprise at its own | |
| 123 | * page. Null for anything else. | |
| 124 | */ | |
| 125 | export function legacyAccountPath(id: string): string | null { | |
| 126 | const value = id.trim().toLowerCase(); | |
| 127 | if (/^ent_[a-z0-9_-]{1,80}$/.test(value)) return `/enterprises/${encodeURIComponent(value)}`; | |
| 128 | const slug = value.startsWith("ws_") ? value.slice(3) : value; | |
| 129 | if (/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/.test(slug)) return `/workspaces/${encodeURIComponent(slug)}`; | |
| 130 | return null; | |
| 131 | } | |
| 132 | ||
| 133 | /** A row's owners' usernames, as one line. */ | |
| 134 | export function ownerNames(owners: AdminOwner[]): string { | |
| 135 | return owners.map((owner) => owner.username).join(", "); | |
| 136 | } |