Skip to content

g1t/apps/web/app/routes/workspace/statement-csv.ts

105 lines4,123 bytesCodeBlame

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 });
Merge Stripe Tax, the card fee on card payments, and one free workspace per person17 // Tax and card fees are paid with payments, not ledger entries: a row a day each.
18 const passed = new Set(["Tax", "Card processing fees"]);
19 const kinds = [...new Set(statement.value.groups.flatMap((group) => group.lines.map((line) => line.kind)))].filter((kind) => !passed.has(kind));
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put20 const rows: string[][] = [
21 [
22 "date",
23 "kind",
24 "description",
25 "project",
26 "pull request",
27 "model",
28 "by",
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging29 "price (USD)",
30 "discount (USD)",
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put31 "amount (USD)",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look32 "paid by included usage (USD)",
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put33 "paid by trial credit (USD)",
34 "paid by open-source pool (USD)",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look35 "covered by g1t (USD)",
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put36 ],
37 ];
The statement is a month at a time, a line per kind of charge38 for (const kind of kinds) {
39 let before: string | null = null;
40 for (let page = 0; page < MAX_PAGES; page++) {
41 const entries = await billing.statementEntries(params.owner, viewer, {
42 month: statement.value.month,
43 kind,
44 before,
45 });
46 if (!entries.ok || entries.value.length === 0) break;
47 for (const entry of entries.value) rows.push(row(kind, entry));
48 before = entries.value[entries.value.length - 1].id;
49 if (entries.value.length < 50) break;
50 }
51 }
Merge Stripe Tax, the card fee on card payments, and one free workspace per person52 for (const group of statement.value.groups) {
53 for (const line of group.lines.filter((l) => passed.has(l.kind))) {
54 const amount = ((line.passedMicros ?? 0) / MICROS_PER_DOLLAR).toFixed(6);
55 rows.push([group.key, line.kind, "Paid with the day's payments, not from the balance", "", "", "", "", "", "", amount, "", "", "", ""]);
56 }
57 }
The statement is a month at a time, a line per kind of charge58 const body = [rows[0], ...rows.slice(1).sort((a, b) => a[0].localeCompare(b[0]))].map((r) => r.map(cell).join(",")).join("\r\n");
59 return new Response(`${body}\r\n`, {
60 headers: {
61 "content-type": "text/csv; charset=utf-8",
62 "content-disposition": `attachment; filename="g1t-${params.owner.toLowerCase()}-${statement.value.month}.csv"`,
63 "cache-control": "no-store",
64 },
65 });
66}
67
68function row(kind: string, entry: LedgerEntry): string[] {
69 return [
70 entry.createdAt,
71 kind,
72 entry.description,
73 entry.repo ?? "",
74 entry.number ? String(entry.number) : "",
75 entry.model ?? "",
76 entry.createdBy ?? "",
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging77 // Usage at its price (empty for money in), then the discount off it.
78 entry.kind === "usage" ? (usagePrice(entry) / MICROS_PER_DOLLAR).toFixed(6) : "",
79 ((entry.discountMicros ?? 0) / MICROS_PER_DOLLAR).toFixed(6),
The statement is a month at a time, a line per kind of charge80 // Charges positive, as on the statement.
81 (-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 put82 ((entry.creditMicros ?? 0) / MICROS_PER_DOLLAR).toFixed(6),
83 ((entry.trialMicros ?? 0) / MICROS_PER_DOLLAR).toFixed(6),
84 ((entry.ossMicros ?? 0) / MICROS_PER_DOLLAR).toFixed(6),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look85 ((entry.givenMicros ?? 0) / MICROS_PER_DOLLAR).toFixed(6),
The statement is a month at a time, a line per kind of charge86 ];
87}
88
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging89/** A usage entry at its price: charged, what paid for it first, and the discount. */
90function usagePrice(entry: LedgerEntry): number {
91 return (
92 -entry.amountMicros +
93 (entry.creditMicros ?? 0) +
94 (entry.trialMicros ?? 0) +
95 (entry.ossMicros ?? 0) +
96 (entry.givenMicros ?? 0) +
97 (entry.discountMicros ?? 0)
98 );
99}
100
The statement is a month at a time, a line per kind of charge101/** A CSV cell, quoted when it must be, and never read as a formula. */
102function cell(value: string): string {
103 const safe = /^[=+\-@\t\r]/.test(value) && !/^-?\d/.test(value) ? `'${value}` : value;
104 return /[",\r\n]/.test(safe) ? `"${safe.replaceAll('"', '""')}"` : safe;
105}

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