Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
105 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.

Money in billing's messages reads to the cent, and the deploy reads migrations again after a failure1import { 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));
Money in billing's messages reads to the cent, and the deploy reads migrations again after a failure20 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)",
Money in billing's messages reads to the cent, and the deploy reads migrations again after a failure31 "amount (USD)",
32 "paid by included usage (USD)",
33 "paid by trial credit (USD)",
34 "paid by open-source pool (USD)",
35 "covered by g1t (USD)",
36 ],
37 ];
38 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 }
Money in billing's messages reads to the cent, and the deploy reads migrations again after a failure58 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),
Money in billing's messages reads to the cent, and the deploy reads migrations again after a failure80 // Charges positive, as on the statement.
81 (-entry.amountMicros / MICROS_PER_DOLLAR).toFixed(6),
82 ((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),
85 ((entry.givenMicros ?? 0) / MICROS_PER_DOLLAR).toFixed(6),
86 ];
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
Money in billing's messages reads to the cent, and the deploy reads migrations again after a failure101/** 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.