feat(portal): M11.1 catalog flow + M12.1 self-serve trial #11

Merged
sharang merged 1 commits from feat/m11.1-m12.1 into main 2026-05-19 16:27:11 +00:00
Owner

What

End-to-end functional flow: a stranger lands at /start, fills the form, gets a tenant + an IT_ADMIN invite, lands in their portal, sees the trial banner counting down, opens the catalog, picks a product, gets an entitlement.

M11.1/[slug]/catalog renders the live catalog from tenant-registry, gates owned products (Active chip), exposes two server actions per remaining card: RequestPOST /v1/catalog/request (audit event today; ERPNext lead when M8.x lands); Start 14-day trialPOST /v1/catalog/trial-request (immediate entitlement, 14-day expiry per M4.2). Flash banner on success/error via ?ok= / ?err= query params.

M12.1 — Public /start route. Server action calls createTenant({slug,name,plan,admin_email}) → tenant-registry's KC-aware POST /v1/tenants → user lands at /<slug>/dashboard. Dashboard now renders a trial-days-left banner when status=trial (urgent styling when ≤3 days remain, links to /billing for the upgrade flow that M8.3 will provide).

Why

Closes the loop: portal can now create a tenant, sign in, browse the catalog, and request/trial a product without anyone touching curl. Every interaction emits the right audit event so M10.2's compliance view will have content.

Linked milestones: M11.1, M12.1

How

  • src/lib/tenant-registry.ts widened from one-call client to full read+mutate surface. Returns {ok: true, ...} | {ok: false, error: '...'} so server actions branch cleanly without try/catch noise.
  • Trial banner days computation moved to a top-level pure helper (computeTrialDaysLeft) — react-hooks/purity lint rejects Date.now() inside the component body, and that rule is worth keeping.
  • Catalog page uses revalidatePath after a successful trial start so the next render sees the new entitlement without a stale-cache window.

Test plan

  • pnpm lint / typecheck / test (22 vitest cases, 100% src/lib line/branch/function coverage) / build — all green
  • Manual: signed-in IT_ADMIN session against the live dev stack — Request + Trial buttons hit tenant-registry, audit log records the events, dashboard banner updates after a trial flip

Risk

  • Server actions don't redirect the user to a sign-in page if their session is missing — they'll hit the 403 NotAuthorized component or get a 404 if the tenant is missing. M10.1 adds a shared sign-in redirect.
  • /start is unauthenticated and rate-unlimited. Fine for dev; M14.x-style rate limiting before this goes near a real internet.

Checklist

  • Unit tests (22, all green)
  • Docs updated (CHANGELOG)
  • Secrets via Infisical — none new
  • Migration — n/a
  • Tenant scoping — canSee(catalog) gates the catalog page; tenant-slug match guard in parent layout still enforced
  • CHANGELOG entry
## What End-to-end functional flow: a stranger lands at `/start`, fills the form, gets a tenant + an IT_ADMIN invite, lands in their portal, sees the trial banner counting down, opens the catalog, picks a product, gets an entitlement. **M11.1** — `/[slug]/catalog` renders the live catalog from tenant-registry, gates owned products (`Active` chip), exposes two server actions per remaining card: **Request** → `POST /v1/catalog/request` (audit event today; ERPNext lead when M8.x lands); **Start 14-day trial** → `POST /v1/catalog/trial-request` (immediate entitlement, 14-day expiry per M4.2). Flash banner on success/error via `?ok=` / `?err=` query params. **M12.1** — Public `/start` route. Server action calls `createTenant({slug,name,plan,admin_email})` → tenant-registry's KC-aware `POST /v1/tenants` → user lands at `/<slug>/dashboard`. Dashboard now renders a trial-days-left banner when `status=trial` (urgent styling when ≤3 days remain, links to `/billing` for the upgrade flow that M8.3 will provide). ## Why Closes the loop: portal can now create a tenant, sign in, browse the catalog, and request/trial a product without anyone touching curl. Every interaction emits the right audit event so M10.2's compliance view will have content. Linked milestones: **M11.1**, **M12.1** ## How - `src/lib/tenant-registry.ts` widened from one-call client to full read+mutate surface. Returns `{ok: true, ...} | {ok: false, error: '...'}` so server actions branch cleanly without try/catch noise. - Trial banner days computation moved to a top-level pure helper (`computeTrialDaysLeft`) — `react-hooks/purity` lint rejects `Date.now()` inside the component body, and that rule is worth keeping. - Catalog page uses `revalidatePath` after a successful trial start so the next render sees the new entitlement without a stale-cache window. ## Test plan - [x] `pnpm lint` / `typecheck` / `test` (22 vitest cases, 100% `src/lib` line/branch/function coverage) / `build` — all green - [x] Manual: signed-in IT_ADMIN session against the live dev stack — Request + Trial buttons hit tenant-registry, audit log records the events, dashboard banner updates after a trial flip ## Risk - Server actions don't redirect the user to a sign-in page if their session is missing — they'll hit the 403 NotAuthorized component or get a 404 if the tenant is missing. M10.1 adds a shared sign-in redirect. - `/start` is unauthenticated and rate-unlimited. Fine for dev; M14.x-style rate limiting before this goes near a real internet. ## Checklist - [x] Unit tests (22, all green) - [x] Docs updated (CHANGELOG) - [x] Secrets via Infisical — none new - [ ] Migration — n/a - [x] Tenant scoping — `canSee(catalog)` gates the catalog page; tenant-slug match guard in parent layout still enforced - [x] CHANGELOG entry
sharang added 1 commit 2026-05-19 16:26:14 +00:00
feat(portal): M11.1 catalog flow + M12.1 self-serve trial
ci / shared (pull_request) Successful in 4s
ci / test (pull_request) Successful in 29s
ci / e2e (pull_request) Has been skipped
ci / image (pull_request) Has been skipped
c1d7f41b59
M11.1 — /[slug]/catalog page renders the live catalog from tenant-registry,
gates already-owned products with an 'Active' chip, and exposes two
server actions per remaining card:
  - Request → POST /v1/catalog/request (emits an audit event; sales
    follow-up flow will pick this up when M8.x lands ERPNext + the
    Lead webhook)
  - Start 14-day trial → POST /v1/catalog/trial-request (provisions
    the entitlement immediately; 14-day expiry per M4.2)
Flash banner on success/error (?ok= / ?err= query params).

M12.1 — Public /start route. Server action calls
createTenant({slug, name, plan, admin_email}) → tenant-registry's
KC-aware POST /v1/tenants → user lands at /<slug>/dashboard. The
dashboard now renders a trial-days-left banner when status=trial
and trial_ends_at is set (urgent styling when ≤3 days remain).

Library:
  src/lib/tenant-registry.ts widened from one-call client to the
  full read+mutate surface (fetchCatalog, fetchEntitlements,
  requestProduct, startTrial, createTenant). Returns typed
  {ok: true, ...} | {ok: false, error: '...'} so server actions
  branch cleanly. 22 vitest cases, 100% line+branch+function
  coverage of src/lib/.

Catalog tests rely on the mock-fetch pattern; the user-visible
flow is exercised by Playwright when the dev stack is up.

Refs: M11.1 + M12.1
CODEOWNERS rules requested review from Benjamin_Boenisch 2026-05-19 16:26:14 +00:00
sharang merged commit ecbe6ae74b into main 2026-05-19 16:27:11 +00:00
sharang deleted branch feat/m11.1-m12.1 2026-05-19 16:27:11 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: platform/portal#11