MC #106259 — Bilko Admin Portal Contract Fix

MC #106259 — fix for 4 contract defects found by Angie Jones' platform_admin UAT catalog (MC #106258). Backend DTOs, frontend types/guards, and one missing route were brought back in sync so the admin portal stops crashing and stops showing wrong data.

Defects fixed

#SeverityDefectFix
1 H GET /admin/dashboard returned 404 — no handler existed anywhere server-side despite being called on first admin page load and documented in the OpenAPI spec. Likely the first thing CEO saw fail on /admin. Implemented in AdminPortalRoutes.kt (get("/dashboard") inside the existing route("/admin") block, same requirePlatformAdmin() gate as every other admin route). AdminOrgService.getDashboardStats() added with real DB aggregates: totalOrgs (COUNT non-deleted orgs), activeTrials (BASIC plan + trial not expired), recentAudit (audit_log LEFT JOIN users, DESC limit 10), mrr (explicit 0.0, see limitation below).
2 H /admin/orgs/{id} detail page crashed on every org. Backend OrgDetail DTO never set mrr, so Jackson omitted the key entirely; frontend called Intl.NumberFormat().format(org.mrr)TypeError: Cannot convert undefined to a number, tripping the global Next.js error boundary. This is almost certainly the "admin stranice puca" CEO reported. OrgDetail DTO gained mrr: Double (always explicit, never omitted), plus vatMethod/trialStatus. Frontend defense-in-depth: all three MRR render sites (orgs/[id]/page.tsx, orgs/page.tsx, admin dashboard page.tsx) now guard == null before formatting, not just the one crash site.
3 M OrgListItem (the /admin/orgs list) was missing vatMethod, trialStatus, userCount, mrr even though the frontend TS interface declared them as required. Effect: VAT Method column always "—", Users column always blank, and every org — trial or paid — rendered with the green "paid" badge since trialBadge(undefined) silently fell through. OrgListItem gained all four fields. trialStatus derived via new resolveTrialStatus(planTier, trialEndsAt) (read-model projection only — does not touch TrialGatePlugin/TrialService enforcement). userCount is one batched query per page (grouped/counted in one round-trip), not N+1 per row.
4 L Pagination/total count silently broken. Backend has always returned nested {data, meta:{total,page,perPage,totalPages}}; the frontend AdminOrgsListResponse/AdminOrgUsersResponse TS types declared a flat shape ({data,total,...}). Result: header count never rendered, and pagination controls would silently never appear once org count exceeded the 50-per-page limit (untriggered today at 9 orgs, but a live landmine). Frontend types changed to nest under meta: AdminOrgsPaginationMeta (chosen over flattening the backend, to avoid touching other PaginatedResponse consumers). Call sites in orgs/page.tsx updated to read data.meta.total / data.meta.totalPages.

Branch / commit

Test results

Known limitation

mrr is returned as an explicit 0.0 everywhere (dashboard, org detail, org list) — not fabricated, but not real. There is no Stripe MRR aggregation anywhere in the Bilko codebase today: stripeSubscriptionId is stored per-org but only ever resolved for trial-gate checks and billing/webhook flows, never rolled up into an aggregate revenue figure. Wiring a real MRR source (either a live Stripe API call per org, or a synced mrr_amount column updated via webhook) is separate, larger follow-on work. Flagged in code comments on the 3 DTOs (OrgDetail, OrgListItem, dashboard stats) for whoever picks that up next.

Deliberately not touched

Follow-on: deploy is separate

This ticket covers code + test correctness only (worktree/local verification). PR → CI → merge to azdo/main → stage/demo promotion is a separate follow-on step, not yet done as of this page. Do not treat this fix as live on stage/demo until that pipeline runs and is verified (curl/browser check on the actual environment, per ZAKON PI2).

Evidence


Revision #2
Created 2026-07-24 09:41:09 UTC by John
Updated 2026-08-10 07:37:53 UTC by John