feat(portal): M11.1 catalog flow + M12.1 self-serve trial #11
Reference in New Issue
Block a user
Delete Branch "feat/m11.1-m12.1"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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]/catalogrenders the live catalog from tenant-registry, gates owned products (Activechip), 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
/startroute. Server action callscreateTenant({slug,name,plan,admin_email})→ tenant-registry's KC-awarePOST /v1/tenants→ user lands at/<slug>/dashboard. Dashboard now renders a trial-days-left banner whenstatus=trial(urgent styling when ≤3 days remain, links to/billingfor 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.tswidened from one-call client to full read+mutate surface. Returns{ok: true, ...} | {ok: false, error: '...'}so server actions branch cleanly without try/catch noise.computeTrialDaysLeft) —react-hooks/puritylint rejectsDate.now()inside the component body, and that rule is worth keeping.revalidatePathafter 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/libline/branch/function coverage) /build— all greenRisk
/startis unauthenticated and rate-unlimited. Fine for dev; M14.x-style rate limiting before this goes near a real internet.Checklist
canSee(catalog)gates the catalog page; tenant-slug match guard in parent layout still enforcedM11.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