Skip to content
136 linesCodeBlameRaw

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 sidebar asks billing whether it is on, not for the account, so no page waits on the card processor1/**
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 */
10import type { AccountSummary, AdminOwner, AdminWorkspace, Limit, Terms } from "@g1t/contracts";
11
12/** Terms every workspace starts on. */
13export 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
23export type Enterprise = { id: string; name: string };
24
25/** Where a workspace stands with billing. */
26export 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
43export type WorkspaceRow = {
44 slug: string;
45 name: string;
46 createdAt: string;
47 owners: AdminOwner[];
48 memberCount: number;
49 billing: WorkspaceBilling;
50};
51
52/** The billing side of every workspace billing knows, by slug. */
53export 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. */
88export function noBilling(): WorkspaceBilling {
89 return { billedTo: null, terms: STANDARD_TERMS, limit: null, chargedMicros: 0, costMicros: 0, paidMicros: 0 };
90}
91
92/**
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.
97 */
98export function joinWorkspaces(workspaces: AdminWorkspace[], accounts: AccountSummary[]): WorkspaceRow[] {
99 const billing = billingBySlug(accounts);
100 return workspaces.map((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
110export const PAGE_SIZE = 50;
111
112/** One page of a list: the page asked for, kept within the pages there are. */
113export 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) };
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 */
125export 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. */
134export function ownerNames(owners: AdminOwner[]): string {
135 return owners.map((owner) => owner.username).join(", ");
136}

This file's history is long; its oldest lines are credited to the oldest commit read.