# PieCed Portal — Billing Phase 1 patch (suspend-via-admin fix) Single-file fix on top of the Phase 1 v2 drop. ## What it fixes The admin panel's suspend/resume button hits `/api/admin/tenants/[name]/suspend` (a different route from the customer-side `/api/tenants/[name]/suspend`). The v2 drop only hooked the customer route — admin suspends were going to K8s without producing a row in `tenant_suspension_events`. This patch adds the same `recordSuspensionEvent` hook to the admin route. No other code paths affected; no schema changes. ## Files ``` src/app/api/admin/tenants/[name]/suspend/route.ts MODIFIED ``` ## Deploy Extract over your `pieced-portal/` tree, rebuild, redeploy as usual. After the new image is running, verify: 1. Suspend any test tenant from the `/admin` panel. 2. Check the events table: ```bash kubectl -n pieced-system exec -it portal-db-1 -- psql -U postgres -d portal -c \ "SELECT * FROM tenant_suspension_events ORDER BY id DESC LIMIT 5;" ``` Expect a fresh `suspended` row for the tenant you just toggled. 3. Resume → expect a `resumed` row. ## Why I missed this Both routes share the same shape (PATCH/POST that sets `spec.suspend`), but they differ on: - URL path (`/api/admin/tenants/...` vs `/api/tenants/...`) - Method (POST vs PATCH) - Authorization (platform-only vs owner+platform) - Caller (admin panel vs customer cancel button) When I grepped for the suspend hook target I matched on the customer endpoint and didn't audit cross-cutting admin duplicates. I've since checked every site that calls `patchTenantSpec`, `createTenant`, or `deleteTenant` — this was the only missed billing-relevant one. Other `patchTenantSpec` sites are confirmed non-billing (openClawImage, channelUsers).