Skip to content
97 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 statement is a month at a time, a line per kind of charge1import { MICROS_PER_DOLLAR, type LedgerEntry } from "@g1t/contracts";
2import { data } from "react-router";
3
4import type { Route } from "./+types/statement-csv";
5import { billing } from "../../lib/services.server";
6import { getViewer } from "../../lib/session.server";
7
8/** Pages read per line at most: 50 entries each. */
9const MAX_PAGES = 200;
10
11/** A month's statement as CSV: every entry, with the line it falls under. */
12export async function loader({ params, context, request }: Route.LoaderArgs) {
13 const viewer = getViewer(context);
14 const month = new URL(request.url).searchParams.get("month");
15 const statement = await billing.statement(params.owner, viewer, month, "day");
16 if (!statement.ok) throw data(null, { status: 404 });
17 const kinds = [...new Set(statement.value.groups.flatMap((group) => group.lines.map((line) => line.kind)))];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put18 const rows: string[][] = [
19 [
20 "date",
21 "kind",
22 "description",
23 "project",
24 "pull request",
25 "model",
26 "by",
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging27 "price (USD)",
28 "discount (USD)",
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put29 "amount (USD)",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look30 "paid by included usage (USD)",
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put31 "paid by trial credit (USD)",
32 "paid by open-source pool (USD)",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look33 "covered by g1t (USD)",
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put34 ],
35 ];
The statement is a month at a time, a line per kind of charge36 for (const kind of kinds) {
37 let before: string | null = null;
38 for (let page = 0; page < MAX_PAGES; page++) {
39 const entries = await billing.statementEntries(params.owner, viewer, {
40 month: statement.value.month,
41 kind,
42 before,
43 });
44 if (!entries.ok || entries.value.length === 0) break;
45 for (const entry of entries.value) rows.push(row(kind, entry));
46 before = entries.value[entries.value.length - 1].id;
47 if (entries.value.length < 50) break;
48 }
49 }
50 const body = [rows[0], ...rows.slice(1).sort((a, b) => a[0].localeCompare(b[0]))].map((r) => r.map(cell).join(",")).join("\r\n");
51 return new Response(`${body}\r\n`, {
52 headers: {
53 "content-type": "text/csv; charset=utf-8",
54 "content-disposition": `attachment; filename="g1t-${params.owner.toLowerCase()}-${statement.value.month}.csv"`,
55 "cache-control": "no-store",
56 },
57 });
58}
59
60function row(kind: string, entry: LedgerEntry): string[] {
61 return [
62 entry.createdAt,
63 kind,
64 entry.description,
65 entry.repo ?? "",
66 entry.number ? String(entry.number) : "",
67 entry.model ?? "",
68 entry.createdBy ?? "",
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging69 // Usage at its price (empty for money in), then the discount off it.
70 entry.kind === "usage" ? (usagePrice(entry) / MICROS_PER_DOLLAR).toFixed(6) : "",
71 ((entry.discountMicros ?? 0) / MICROS_PER_DOLLAR).toFixed(6),
The statement is a month at a time, a line per kind of charge72 // Charges positive, as on the statement.
73 (-entry.amountMicros / MICROS_PER_DOLLAR).toFixed(6),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put74 ((entry.creditMicros ?? 0) / MICROS_PER_DOLLAR).toFixed(6),
75 ((entry.trialMicros ?? 0) / MICROS_PER_DOLLAR).toFixed(6),
76 ((entry.ossMicros ?? 0) / MICROS_PER_DOLLAR).toFixed(6),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look77 ((entry.givenMicros ?? 0) / MICROS_PER_DOLLAR).toFixed(6),
The statement is a month at a time, a line per kind of charge78 ];
79}
80
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging81/** A usage entry at its price: charged, what paid for it first, and the discount. */
82function usagePrice(entry: LedgerEntry): number {
83 return (
84 -entry.amountMicros +
85 (entry.creditMicros ?? 0) +
86 (entry.trialMicros ?? 0) +
87 (entry.ossMicros ?? 0) +
88 (entry.givenMicros ?? 0) +
89 (entry.discountMicros ?? 0)
90 );
91}
92
The statement is a month at a time, a line per kind of charge93/** A CSV cell, quoted when it must be, and never read as a formula. */
94function cell(value: string): string {
95 const safe = /^[=+\-@\t\r]/.test(value) && !/^-?\d/.test(value) ? `'${value}` : value;
96 return /[",\r\n]/.test(safe) ? `"${safe.replaceAll('"', '""')}"` : safe;
97}