flagon-io/g1t

public

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/apps/web/app/routes/workspace/statement-csv.ts

61 lines2,524 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 });
17 const kinds = [...new Set(statement.value.groups.flatMap((group) => group.lines.map((line) => line.kind)))];
18 const rows: string[][] = [["date", "kind", "description", "project", "pull request", "model", "by", "amount (USD)"]];
19 for (const kind of kinds) {
20 let before: string | null = null;
21 for (let page = 0; page < MAX_PAGES; page++) {
22 const entries = await billing.statementEntries(params.owner, viewer, {
23 month: statement.value.month,
24 kind,
25 before,
26 });
27 if (!entries.ok || entries.value.length === 0) break;
28 for (const entry of entries.value) rows.push(row(kind, entry));
29 before = entries.value[entries.value.length - 1].id;
30 if (entries.value.length < 50) break;
31 }
32 }
33 const body = [rows[0], ...rows.slice(1).sort((a, b) => a[0].localeCompare(b[0]))].map((r) => r.map(cell).join(",")).join("\r\n");
34 return new Response(`${body}\r\n`, {
35 headers: {
36 "content-type": "text/csv; charset=utf-8",
37 "content-disposition": `attachment; filename="g1t-${params.owner.toLowerCase()}-${statement.value.month}.csv"`,
38 "cache-control": "no-store",
39 },
40 });
41}
42
43function row(kind: string, entry: LedgerEntry): string[] {
44 return [
45 entry.createdAt,
46 kind,
47 entry.description,
48 entry.repo ?? "",
49 entry.number ? String(entry.number) : "",
50 entry.model ?? "",
51 entry.createdBy ?? "",
52 // Charges positive, as on the statement.
53 (-entry.amountMicros / MICROS_PER_DOLLAR).toFixed(6),
54 ];
55}
56
57/** A CSV cell, quoted when it must be, and never read as a formula. */
58function cell(value: string): string {
59 const safe = /^[=+\-@\t\r]/.test(value) && !/^-?\d/.test(value) ? `'${value}` : value;
60 return /[",\r\n]/.test(safe) ? `"${safe.replaceAll('"', '""')}"` : safe;
61}