1import * as GCP from "alchemy/GCP";
2import * as Effect from "effect/Effect";
3
4/** The warehouse the nightly job reads from. */
5export const Warehouse = GCP.BigQuery.Dataset("Warehouse", {
6 forceDestroy: true,
7});
8
9/**
10 * One row per order. Amounts are integer cents so the sums the job
11 * computes are exact.
12 */
13export const Orders = Effect.gen(function* () {
14 const dataset = yield* Warehouse;
15 return yield* GCP.BigQuery.Table("Orders", {
16 datasetId: dataset.datasetId,
17 tableId: "orders",
18 schema: [
19 { name: "id", type: "STRING", mode: "REQUIRED" },
20 { name: "region", type: "STRING", mode: "REQUIRED" },
21 { name: "amountCents", type: "INTEGER", mode: "REQUIRED" },
22 { name: "occurredAt", type: "TIMESTAMP", mode: "REQUIRED" },
23 ],
24 });
25});
26
27/**
28 * Where the daily summaries land, one JSON object per day under
29 * `daily/`. `forceDestroy` empties the bucket on `alchemy destroy`; drop
30 * it in production so a destroy cannot take the reports with it.
31 */
32export const Reports = GCP.Storage.Bucket("Reports", {
33 forceDestroy: true,
34});
35
36/** Summaries are written under this prefix. */
37export const SUMMARY_PREFIX = "daily/";
38
39/** `daily/2026-09-27.json` for a window ending on that UTC day. */
40export const summaryKey = (windowEnd: Date) =>
41 `${SUMMARY_PREFIX}${windowEnd.toISOString().slice(0, 10)}.json`;
42
43/** Per-region totals inside a {@link DailySummary}. */
44export interface RegionTotal {
45 region: string;
46 orders: number;
47 revenueCents: number;
48}
49
50/** The JSON document the job writes for each run. */
51export interface DailySummary {
52 /** Start of the 24-hour window (inclusive, RFC3339). */
53 from: string;
54 /** End of the window (exclusive, RFC3339) — the moment the run started. */
55 to: string;
56 /** The Cloud Run execution that produced this summary, if known. */
57 execution: string | null;
58 orders: number;
59 revenueCents: number;
60 regions: RegionTotal[];
61}