# 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

<table id="bkmrk-%23severitydefectfix-1"><thead><tr><th>\#</th><th>Severity</th><th>Defect</th><th>Fix</th></tr></thead><tbody><tr><td>1</td><td>H</td><td>`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`.</td><td>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).</td></tr><tr><td>2</td><td>H</td><td>`/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.</td><td>`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.</td></tr><tr><td>3</td><td>M</td><td>`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.</td><td>`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.</td></tr><tr><td>4</td><td>L</td><td>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).</td><td>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`.</td></tr></tbody></table>

## Branch / commit

- Repo: `Bilko` (canonical remote `azdo`)
- Branch: `feat/106259-admin-contract-fix`, created off `azdo/main` at `cb3746d6`
- Commit: `5513d0e6339864b31c2d3507ca0219ede5b052ba` — pushed to `azdo`, verified on remote via `git ls-remote`
- 8 files changed: `AdminPortalRoutes.kt`, `AdminOrgService.kt`, `AdminPortalRoutesHttpIntegrationTest.kt` (new tests T16–T19b), `orgs/[id]/page.tsx`, `orgs/page.tsx`, admin dashboard `page.tsx`, `lib/api.ts`, and a new frontend test file `admin-contract-106259.test.tsx`

## Test results

- **Backend**: `AdminPortalRoutesHttpIntegrationTest` — **23/23 passed** (9 new tests T16–T19b, one per fix + auth-gate coverage, plus 14 pre-existing T1–T15 unmodified, zero regressions). Sibling admin suites unmodified and green: `AdminPortalRoutesP2Test` 10/10, `AdminRoutesHttpIntegrationTest` 6/6.
- **Frontend**: new `admin-contract-106259.test.tsx` — **9/9 passed** (React Testing Library, real rendered component tree — MRR undefined/null guards, real-number formatting, vatMethod/trialStatus/userCount render, nested `meta.total`/`meta.totalPages` reads, pagination-controls visibility). Full production `next build` succeeds (65/65 routes), `tsc --noEmit` clean, pre-push `turbo run type-check` across all 12 workspace packages green.
- **Independent verification (Angie Jones / Proveo)** — read code directly in the isolated worktree at commit `5513d0e6` (did not trust the builder's self-report), re-ran backend integration tests and frontend Vitest herself, and diffed the commit against base to confirm scope boundaries. **Verdict: PASS** on all 4 items. Confirmed independently via her own grep that no aggregate MRR computation exists anywhere in the codebase, and via direct diff that zero auth files were touched.

## 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

- `AdminAuthPlugin.kt`, `JwtService.kt`, `Authentication.kt`, and the `bilko-jwt` verifier — confirmed untouched by direct diff (`git diff cb3746d6 5513d0e6 --name-only`, zero overlap). Angie's original catalog established that the `/admin/orgs` 401 CEO/UAT saw was a stale/expired access-token test artefact, not a server-side auth defect — nothing in the auth stack needed fixing.
- `/admin/users` 405 — by design, only a `POST` handler exists; no frontend page calls `GET /admin/users`. Not a bug.
- "Report error" support-ticket button — confirmed working live during the original catalog investigation (real `POST /support/tickets` → 201).
- Flyway V146 warning — not reproducible; DB and deployed image agreed on schema version at investigation time.

## 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

- Defect catalog (spec): `~/system/evidence/106258/admin-defect-catalog.md`
- Build evidence: `~/system/evidence/106259/build-evidence.md`
- Independent verify (PASS): `~/system/evidence/106259/angie-verify-2026-07-24.md`