From 396f2c8a858f6480eb8b5d0657f5fdc3998b7291 Mon Sep 17 00:00:00 2001 From: syn Date: Mon, 20 Jul 2026 19:44:30 -0500 Subject: [PATCH 01/12] feat(billing): add cloud usage billing foundation --- .../CloudBillingSkuForm.tsx | 204 ++++++++ .../src/app/admin/cloud-billing-skus/page.tsx | 258 ++++++++++ .../src/app/admin/components/AppSidebar.tsx | 6 + apps/web/src/lib/cloud-billing-sku.test.ts | 66 +++ apps/web/src/lib/cloud-billing-sku.ts | 53 ++ apps/web/src/lib/trpc/init.ts | 3 +- apps/web/src/routers/admin-router.ts | 2 + .../admin/cloud-billing-skus-router.test.ts | 181 +++++++ .../admin/cloud-billing-skus-router.ts | 73 +++ dev/local/services.ts | 5 + packages/container-usage/package.json | 29 ++ packages/container-usage/src/client.test.ts | 134 +++++ packages/container-usage/src/client.ts | 156 ++++++ packages/container-usage/src/context.test.ts | 57 ++ packages/container-usage/src/context.ts | 111 ++++ .../container-usage/src/contracts.test.ts | 46 ++ packages/container-usage/src/contracts.ts | 232 +++++++++ .../container-usage/src/heartbeat.test.ts | 485 ++++++++++++++++++ packages/container-usage/src/heartbeat.ts | 228 ++++++++ packages/container-usage/src/index.ts | 4 + packages/container-usage/tsconfig.json | 18 + packages/db/src/schema.ts | 123 +++++ pnpm-lock.yaml | 67 ++- .../container-usage-meter/drizzle.config.ts | 8 + .../drizzle/0000_big_nightshade.sql | 24 + .../drizzle/meta/0000_snapshot.json | 176 +++++++ .../drizzle/meta/_journal.json | 13 + .../drizzle/migrations.js | 9 + services/container-usage-meter/package.json | 32 ++ .../container-usage-meter/src/config.test.ts | 32 ++ .../src/db/sqlite-schema.ts | 35 ++ .../src/failover-buffer.ts | 192 +++++++ .../src/failover-contract.ts | 66 +++ services/container-usage-meter/src/index.ts | 24 + .../container-usage-meter/src/meter.test.ts | 134 +++++ services/container-usage-meter/src/meter.ts | 153 ++++++ .../container-usage-meter/src/postgres.ts | 48 ++ .../src/reconciliation.ts | 27 + .../container-usage-meter/src/sharding.ts | 18 + .../test/integration/failover-buffer.test.ts | 143 ++++++ .../container-usage-meter/test/test-worker.ts | 7 + services/container-usage-meter/tsconfig.json | 19 + .../container-usage-meter/vitest.config.ts | 9 + .../vitest.workers.config.ts | 15 + .../worker-configuration.d.ts | 18 + services/container-usage-meter/wrangler.jsonc | 47 ++ .../container-usage-meter/wrangler.test.jsonc | 22 + 47 files changed, 3810 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/admin/cloud-billing-skus/CloudBillingSkuForm.tsx create mode 100644 apps/web/src/app/admin/cloud-billing-skus/page.tsx create mode 100644 apps/web/src/lib/cloud-billing-sku.test.ts create mode 100644 apps/web/src/lib/cloud-billing-sku.ts create mode 100644 apps/web/src/routers/admin/cloud-billing-skus-router.test.ts create mode 100644 apps/web/src/routers/admin/cloud-billing-skus-router.ts create mode 100644 packages/container-usage/package.json create mode 100644 packages/container-usage/src/client.test.ts create mode 100644 packages/container-usage/src/client.ts create mode 100644 packages/container-usage/src/context.test.ts create mode 100644 packages/container-usage/src/context.ts create mode 100644 packages/container-usage/src/contracts.test.ts create mode 100644 packages/container-usage/src/contracts.ts create mode 100644 packages/container-usage/src/heartbeat.test.ts create mode 100644 packages/container-usage/src/heartbeat.ts create mode 100644 packages/container-usage/src/index.ts create mode 100644 packages/container-usage/tsconfig.json create mode 100644 services/container-usage-meter/drizzle.config.ts create mode 100644 services/container-usage-meter/drizzle/0000_big_nightshade.sql create mode 100644 services/container-usage-meter/drizzle/meta/0000_snapshot.json create mode 100644 services/container-usage-meter/drizzle/meta/_journal.json create mode 100644 services/container-usage-meter/drizzle/migrations.js create mode 100644 services/container-usage-meter/package.json create mode 100644 services/container-usage-meter/src/config.test.ts create mode 100644 services/container-usage-meter/src/db/sqlite-schema.ts create mode 100644 services/container-usage-meter/src/failover-buffer.ts create mode 100644 services/container-usage-meter/src/failover-contract.ts create mode 100644 services/container-usage-meter/src/index.ts create mode 100644 services/container-usage-meter/src/meter.test.ts create mode 100644 services/container-usage-meter/src/meter.ts create mode 100644 services/container-usage-meter/src/postgres.ts create mode 100644 services/container-usage-meter/src/reconciliation.ts create mode 100644 services/container-usage-meter/src/sharding.ts create mode 100644 services/container-usage-meter/test/integration/failover-buffer.test.ts create mode 100644 services/container-usage-meter/test/test-worker.ts create mode 100644 services/container-usage-meter/tsconfig.json create mode 100644 services/container-usage-meter/vitest.config.ts create mode 100644 services/container-usage-meter/vitest.workers.config.ts create mode 100644 services/container-usage-meter/worker-configuration.d.ts create mode 100644 services/container-usage-meter/wrangler.jsonc create mode 100644 services/container-usage-meter/wrangler.test.jsonc diff --git a/apps/web/src/app/admin/cloud-billing-skus/CloudBillingSkuForm.tsx b/apps/web/src/app/admin/cloud-billing-skus/CloudBillingSkuForm.tsx new file mode 100644 index 0000000000..acf60e6bb0 --- /dev/null +++ b/apps/web/src/app/admin/cloud-billing-skus/CloudBillingSkuForm.tsx @@ -0,0 +1,204 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { + createCloudBillingSkuInputSchema, + multiplyCloudBillingRate, + type CreateCloudBillingSkuInput, +} from '@/lib/cloud-billing-sku'; + +type Props = { + pending: boolean; + onSubmit: (values: CreateCloudBillingSkuInput) => void; +}; + +type RawForm = { + id: string; + name: string; + description: string; + rate: string; + exampleMinutes: string; +}; + +function preview(rate: string, seconds: number): string { + const parsed = createCloudBillingSkuInputSchema.shape.rate_cents_per_unit.safeParse(rate); + return parsed.success ? `${multiplyCloudBillingRate(parsed.data, seconds)} cents` : '—'; +} + +export default function CloudBillingSkuForm({ pending, onSubmit }: Props) { + const [raw, setRaw] = useState({ + id: '', + name: '', + description: '', + rate: '', + exampleMinutes: '15', + }); + const [errors, setErrors] = useState>>( + {} + ); + + const exampleMinutes = /^\d+$/.test(raw.exampleMinutes) ? Number(raw.exampleMinutes) : 0; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + const parsed = createCloudBillingSkuInputSchema.safeParse({ + id: raw.id.trim(), + name: raw.name.trim(), + description: raw.description.trim() || null, + unit: 'second', + rate_cents_per_unit: raw.rate.trim(), + }); + if (!parsed.success) { + const next: Partial> = {}; + for (const issue of parsed.error.issues) { + const key = issue.path[0] as keyof CreateCloudBillingSkuInput | undefined; + if (key && !next[key]) next[key] = issue.message; + } + setErrors(next); + const firstKey = parsed.error.issues[0]?.path[0]; + const fieldId = + firstKey === 'id' + ? 'sku-id' + : firstKey === 'name' + ? 'sku-name' + : firstKey === 'description' + ? 'sku-description' + : firstKey === 'rate_cents_per_unit' + ? 'sku-rate' + : undefined; + if (fieldId) document.getElementById(fieldId)?.focus(); + return; + } + setErrors({}); + onSubmit(parsed.data); + }; + + return ( +
+
+
+ + setRaw(current => ({ ...current, id: event.target.value }))} + /> +

+ Producer-facing constant. Use lowercase letters, numbers, and hyphens. +

+ {errors.id && ( + + )} +
+ +
+ + setRaw(current => ({ ...current, name: event.target.value }))} + /> + {errors.name && ( + + )} +
+ +
+ +