diff --git a/README.md b/README.md index 9d4fb84..41c47e2 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ Schema + event tracking. No UI yet (that lands in Phase 2). This zip mirrors the `pieced-portal/` repo root — extract over your existing source tree to apply. +**v2 fix:** stripped stray backticks from SQL comments that were +closing the `MIGRATION_SQL` template literal early. If you got +"Expected a semicolon" at db.ts:335 with v1, this build is the fix. + --- ## Files in this drop @@ -34,17 +38,17 @@ No `package.json` changes — Phase 1 uses only deps already present. `src/app/api/admin/requests/[id]/approve/route.ts` - Imports `recordTenantCreated`, `recordSkillEvents`, `recordSuspensionEvent` from `@/lib/db` - - Resume path: records a `resumed` suspension event after the - `patchTenantSpec({suspend: false})` call - - Provision path: records `recordTenantCreated` + initial `enabled` - events after `createTenant` + - Resume path: records a `resumed` suspension event after + `patchTenantSpec({suspend: false})` + - Provision path: records `recordTenantCreated` + initial + `enabled` events after `createTenant` `src/app/api/tenants/[name]/route.ts` - Imports `recordSkillEvents` - After `patchTenantSpec` succeeds and the patch touched `packages`, computes the diff (added/removed) and writes events. - Diff is computed against the patched CR (the returned state) so - events match what K8s committed. + Diff is computed against the patched CR (the returned state) + so events match what K8s committed. `src/app/api/tenants/[name]/suspend/route.ts` - Imports `recordSuspensionEvent` @@ -134,19 +138,10 @@ Expected: one row, all zeros, vat_rate_chli=8.10. ### Step 2 — Backfill existing tenants -Run the backfill once. From your laptop (with a logged-in admin -session cookie) or from another pod with cluster access: +Run the backfill once. From a logged-in admin browser tab DevTools +console: -``` -curl -X POST \ - -H "Cookie: " \ - https://app.pieced.ch/api/admin/billing/backfill -``` - -Or, easier, hit the URL from a logged-in browser tab and use the -DevTools console: - -``` +```js await fetch('/api/admin/billing/backfill', { method: 'POST' }) .then(r => r.json()) ``` @@ -163,17 +158,7 @@ Expected response (numbers will vary): ``` Run it a SECOND time — all three "Inserted" counts should be 0 -(idempotency check): - -``` -{ - "message": "Backfill complete.", - "tenantsExamined": 4, - "lifecycleInserted": 0, - "eventsInserted": 0, - "suspensionEventsInserted": 0 -} -``` +(idempotency check). ### Step 3 — Verify backfill data @@ -182,11 +167,9 @@ kubectl -n portal exec -it portal-db-1 -- psql -U portal -d portal ``` ```sql --- All tenants have lifecycle rows SELECT tenant_name, zitadel_org_id, created_at, deleted_at FROM tenant_billing_lifecycle ORDER BY created_at; --- Initial skill events match each tenant's current spec.packages SELECT tenant_name, skill_id, event_kind, occurred_at FROM tenant_skill_events ORDER BY tenant_name, occurred_at; ``` @@ -201,9 +184,8 @@ Every package currently in `spec.packages` should have a matching ### Step 4 — Live skill toggle -Open the customer-facing tenant detail page for a test tenant. -Enable a package (e.g. `searxng-local-search`) that wasn't on -before. +From the customer-facing tenant detail page, enable a package not +previously present (e.g. `searxng-local-search`): ```sql SELECT * FROM tenant_skill_events @@ -211,15 +193,12 @@ SELECT * FROM tenant_skill_events ORDER BY id DESC LIMIT 3; ``` -You should see a fresh `enabled` row with the package id and a -timestamp matching the toggle. - -Disable the same package, re-check — you should now see a -`disabled` row added on top. +Expect a fresh `enabled` row. Disable the package → expect a +`disabled` row on top. ### Step 5 — Live suspend toggle -From the customer-side cancel button on a test tenant, suspend it: +Cancel a test tenant from the customer-side button: ```sql SELECT * FROM tenant_suspension_events @@ -227,14 +206,12 @@ SELECT * FROM tenant_suspension_events ORDER BY id DESC LIMIT 3; ``` -Expect a `suspended` row. - -As platform admin, resume it via the resume-request flow. Expect a -`resumed` row to land. +Expect a `suspended` row. Resume via the admin approval flow → +expect a `resumed` row. ### Step 6 — Live delete -Delete a test tenant from the admin panel. Check: +Delete a test tenant from the admin panel: ```sql SELECT tenant_name, created_at, deleted_at @@ -244,10 +221,10 @@ SELECT tenant_name, created_at, deleted_at `deleted_at` should be stamped with roughly "now". -### Step 7 — Pricing helpers (smoke test) +### Step 7 — Pricing rows survive (optional) -Optional sanity check — set a price for a skill and a monthly fee -via direct SQL (Phase 2 will add a UI for this): +Direct-INSERT a price into `platform_pricing` and `skill_pricing`, +restart the portal pod, confirm rows survive: ```sql UPDATE platform_pricing @@ -263,36 +240,18 @@ ON CONFLICT (skill_id) DO UPDATE ``` No application behaviour changes from these — they're inert until -Phase 2 starts computing invoices. The check is just "the rows -land and survive a restart". - -### Step 8 — Org billing config auto-create - -Verify the org_billing_config auto-creates on first read. Call the -helper indirectly: open the admin panel (which will eventually -read this); for now, force it via SQL: - -```sql --- Pick any org id from your tenant labels -SELECT zitadel_org_id, pay_by_invoice, auto_invoice_enabled, - auto_reminders_enabled - FROM org_billing_config; -``` - -At this point this table is empty (Phase 2's UI will populate it). -That's expected and fine. +Phase 2 starts computing invoices. --- ## Rollback -If anything misbehaves, the migration is additive — no existing -columns/tables touched. To roll back: +The migration is additive — no existing columns/tables touched. +To roll back: 1. Re-deploy the previous portal image (revert the tag in gitops) -2. The new tables remain in the DB but are unreferenced. Leave them - in place — Phase 2 will use them again. Or drop them if you - want a clean slate: +2. New tables remain in the DB but are unreferenced. Leave them + in place — Phase 2 will use them again. Or drop them: ```sql DROP TABLE IF EXISTS invoice_reminders, invoice_lines, invoices, invoice_number_counters, org_payment_methods, org_billing_config, @@ -312,15 +271,3 @@ columns/tables touched. To roll back: * No reminders or cron These are Phases 2-6. - -## What to expect in Phase 2 - -* `/admin/billing` page: platform pricing editor, per-skill pricing, - per-org config -* "Generate invoice for org X / month M" admin button (testing tool - + foundation for the Phase 6 monthly cron) -* `lib/billing.ts` introducing the invoice computation pipeline - (LiteLLM spend pull, Threema usage pull, skill day collapse, - monthly-fee proration, suspended-day exclusion, VAT calc) -* PDF rendering via `@react-pdf/renderer` (adds one dep) -* Admin "mark paid" action for the bill-pay flow diff --git a/src/lib/db.ts b/src/lib/db.ts index 1da3c5a..6d15a2d 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -332,8 +332,8 @@ const MIGRATION_SQL = ` updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); - -- One row per tenant. `created_at` anchors first-month proration; - -- `deleted_at` (nullable, stamped on delete) anchors last-month + -- One row per tenant. created_at anchors first-month proration; + -- deleted_at (nullable, stamped on delete) anchors last-month -- proration. The PiecedTenant CR is the source of truth for -- existence, but once the CR is deleted we lose its -- creationTimestamp — so we mirror those two bookends here. @@ -352,7 +352,7 @@ const MIGRATION_SQL = ` -- log preserves history for audit and lets us re-bill historical -- months reproducibly. -- - -- `skill_id` is the package id from PACKAGE_CATALOG. We store + -- skill_id is the package id from PACKAGE_CATALOG. We store -- events for ALL package toggles, not just skill-category — the -- channel/core toggles are cheap to record and may become billable -- in the future without a schema change. @@ -479,9 +479,9 @@ const MIGRATION_SQL = ` CREATE UNIQUE INDEX IF NOT EXISTS uniq_invoices_org_period ON invoices(zitadel_org_id, period_start); - -- Invoice line items. `kind` lets the PDF renderer group lines - -- (all monthly fees together, all AI usage together, etc.) and - -- the admin UI filter by category. + -- Invoice line items. The kind column lets the PDF renderer + -- group lines (all monthly fees together, all AI usage together, + -- etc.) and the admin UI filter by category. CREATE TABLE IF NOT EXISTS invoice_lines ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,