Files
Momento/_bmad-output/implementation-artifacts/spec-subscription-billing-admin-management.md
Antigravity ee70e74bf5
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 5m39s
CI / Deploy production (on server) (push) Successful in 22s
fix: 5 bugs critiques de l'éditeur (Phase 1 audit)
1. replaceAll (Find & Replace) — une seule transaction ProseMirror
   au lieu d'un forEach cassé. Tous les matchs sont maintenant remplacés.

2. Link Preview unwrap — deleteNode() au lieu de clearer les attrs
   qui laissaient un nœud fantôme invisible dans le document.

3. Conversion Markdown → richtext — breaks: true dans marked.parse()
   Les simple newlines sont maintenant convertis en <br>.
   + préserve les blocs custom (toggle, callout, math, columns,
   outline, link-preview) en commentaires HTML lors de l'export MD.

4. emitNoteChange exercices — shape corrigée (type:'created' attend
   un objet Note, pas noteId/notebookId séparés).

5. Raccourcis clavier sans conflit :
   Cmd+Shift+C → Cmd+Alt+C (callout, avant: copier)
   Cmd+Shift+O → Cmd+Alt+O (outline, avant: historique/signets)
   Cmd+Shift+L → Cmd+Alt+L (colonnes, avant: lock screen macOS)
2026-06-20 15:48:18 +00:00

164 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 'Subscription & Quota Admin Management'
type: 'feature'
created: '2026-06-20'
status: 'done'
baseline_commit: '5b13a88b726c03ca6ff5603d901147448a72adbc'
context:
- 'memento-note/lib/entitlements.ts'
- 'memento-note/lib/config.ts'
- 'memento-note/lib/billing/stripe-prices.ts'
---
<frozen-after-approval reason="human-owned intent — do not modify unless human renegotiates">
## Intent
**Problem:** Subscription tiers, AI quotas, and Stripe business config are split between hardcoded code (`TIER_LIMITS`), server `.env` (price IDs, billing flag), and a minimal admin override (tier dropdown only). Operators cannot adjust limits or billing settings without redeploy; quota enforcement has a TOCTOU race (`check` then `increment`); token analytics are partially broken.
**Approach:** Add a secure admin **Billing & Quotas** console backed by DB (`PlanEntitlement` + `SystemConfig`), keep Stripe secrets in infra env only, unify quota enforcement on atomic `reserveUsageOrThrow`, and expose usage dashboards with audit logging for manual tier overrides.
## Boundaries & Constraints
**Always:**
- `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` stay in server env / secrets vault — never in `SystemConfig`, never editable from browser.
- Stripe remains authoritative for paid subscriptions; webhooks drive state; manual admin tier changes are support overrides only.
- Quota gating unit stays **requests per feature per calendar month** (not token-based blocking).
- Redis quota checks fail-open with structured error logging (industry standard for AI rate limits).
- All admin mutations require `role === ADMIN`; i18n for new UI strings in `locales/en.json` and `locales/fr.json` minimum.
- Non-destructive DB migration only; seed `PlanEntitlement` from current `TIER_LIMITS` defaults.
**Ask First:**
- Adding new Prisma models beyond `PlanEntitlement` (e.g. full `FeatureFlag` wiring).
- Changing Stripe products/prices in live Stripe Dashboard (ops action, not code).
- Switching to usage-based Stripe Meter / Metronome billing.
**Never:**
- Store `sk_*` or `whsec_*` in plaintext DB or admin forms.
- Run `prisma migrate reset`, `db push --accept-data-loss`, or any destructive DB command.
- Replace Redis real-time counters with synchronous PostgreSQL writes on every AI request.
- Add automated tests unless explicitly requested later.
## I/O & Edge-Case Matrix
| Scenario | Input / State | Expected Output / Behavior | Error Handling |
|----------|--------------|---------------------------|----------------|
| Admin saves tier limit | ADMIN edits PRO `chat` limit 50→75 | `PlanEntitlement` upserted; next `getLimit()` returns 75; existing Redis counters unchanged until period rollover | 403 if non-admin; 400 if invalid feature or negative limit |
| Admin saves Stripe price ID | ADMIN sets `STRIPE_PRICE_PRO_MONTHLY` in SystemConfig | `resolvePriceId('PRO','month')` reads DB value; env used only as fallback | 400 if empty when billing enabled |
| User at quota | User on BASIC, 30/30 `semantic_search` used | `reserveUsageOrThrow` returns 402 `QUOTA_EXCEEDED` | Fail-open if Redis down (allow + log alert) |
| Concurrent AI requests | Two requests at limit1 | Only one succeeds; second gets 402 (atomic Lua) | N/A |
| Admin manual tier override | ADMIN sets user to BUSINESS | `Subscription` upserted; `AuditLog` entry with actor, target, old/new tier | Cannot override own tier (existing rule) |
| Billing disabled | `BILLING_ENABLED=false` in SystemConfig | `/settings/billing` shows disabled state; checkout buttons hidden | N/A |
| suggest-charts usage | Successful chart suggestion | `incrementUsageAsync` called once (not broken 4-arg call) | N/A |
</frozen-after-approval>
## Code Map
- `memento-note/prisma/schema.prisma` — add `PlanEntitlement` model (`tier`, `feature`, `limitValue`; unique on `[tier, feature]`; `null` limitValue = unlimited; omit row or sentinel = feature unavailable per tier)
- `memento-note/prisma/migrations/*` — additive migration + seed from current `TIER_LIMITS`
- `memento-note/lib/entitlements.ts` — load limits from DB with in-memory cache (TTL ~60s) and hardcoded fallback; export `getLimit()` async; keep fail-open on Redis errors
- `memento-note/lib/billing/stripe-prices.ts` — read price IDs via `getConfigValue()` with env fallback; remove sole dependence on `process.env`
- `memento-note/lib/config.ts` — add billing keys to `ENV_FALLBACKS` (`BILLING_ENABLED`, four `STRIPE_PRICE_*`)
- `memento-note/app/actions/admin-billing.ts` — server actions: `getPlanEntitlements`, `updatePlanEntitlement`, `updateBillingConfig`, `getUsageOverview`
- `memento-note/app/(admin)/admin/billing/page.tsx` — admin UI: tier limits matrix, billing config, usage summary table
- `memento-note/app/(admin)/admin/billing/billing-admin-form.tsx` — client form component
- `memento-note/components/admin-sidebar.tsx` — nav link `/admin/billing`
- `memento-note/app/actions/admin.ts` — log `SUBSCRIPTION_OVERRIDE` audit on `updateUserSubscription`
- `memento-note/lib/audit-log.ts` — extend `AuditAction` with `SUBSCRIPTION_OVERRIDE`, `BILLING_CONFIG_UPDATED`, `PLAN_ENTITLEMENT_UPDATED`
- `memento-note/components/settings/billing-plans.tsx` — read `billingEnabled` from `/api/billing/status` instead of build-time env only
- `memento-note/app/api/billing/status/route.ts` — include `billingEnabled` from SystemConfig
- `memento-note/app/api/ai/suggest-charts/route.ts` — fix usage tracking (use `incrementUsageAsync`, not broken `trackFeatureUsage` call)
- AI routes using `checkEntitlementOrThrow` + `incrementUsageAsync` — migrate to `reserveUsageOrThrow` only (remove duplicate increment on success); include chat route
## Tasks & Acceptance
**Execution:**
- [x] `prisma/schema.prisma` + migration — add `PlanEntitlement`, seed defaults from `TIER_LIMITS` — DB-backed limits without breaking existing users
- [x] `lib/entitlements.ts` — async limit loader with cache + fallback; update `canUseFeature`, `reserveUsageOrThrow`, `getUserQuotas` — single source of truth
- [x] `lib/billing/stripe-prices.ts` + `lib/config.ts` — SystemConfig-backed price IDs and `BILLING_ENABLED` — admin-editable business config
- [x] `app/actions/admin-billing.ts` — CRUD entitlements + billing config + usage overview query (join `UsageLog` + Redis snapshot) — secure admin API surface
- [x] `app/(admin)/admin/billing/*` + `components/admin-sidebar.tsx` — admin UI section — operator-facing management
- [x] `app/actions/admin.ts` + `lib/audit-log.ts` — audit trail for tier overrides and config changes — support accountability
- [x] AI routes (grep `checkEntitlementOrThrow`) + `app/api/chat/route.ts` — switch to `reserveUsageOrThrow`; drop redundant `incrementUsageAsync` on success — fix TOCTOU race
- [x] `app/api/ai/suggest-charts/route.ts` — fix broken usage call — restore quota accounting
- [x] `app/api/billing/status/route.ts` + `components/settings/billing-plans.tsx` — runtime billing flag — no redeploy to toggle billing UI
- [x] `locales/en.json` + `locales/fr.json` — i18n keys under `admin.billing.*` — FR/EN reference labels
**Acceptance Criteria:**
- Given an ADMIN on `/admin/billing`, when they change PRO `chat` limit and save, then a new entitlement check within 60s reflects the new limit without app restart.
- Given a BASIC user at quota, when two parallel AI requests arrive, then at most one succeeds and the other returns HTTP 402.
- Given `BILLING_ENABLED` is false in SystemConfig, when a user opens `/settings/billing`, then checkout/upgrade actions are hidden and a clear disabled message is shown.
- Given an ADMIN changes a user's tier from `/admin/users`, when the change succeeds, then an `AuditLog` row records actor, target user, old tier, and new tier.
- Given Stripe price IDs are set in SystemConfig (env unset), when checkout is initiated, then the correct Stripe price is used.
- Given Redis is unreachable, when an AI request is made, then the request is allowed (fail-open) and an error is logged server-side.
- Given `STRIPE_SECRET_KEY` remains env-only, when inspecting `SystemConfig` table, then no `sk_` or `whsec_` values exist.
## Design Notes
**PlanEntitlement limit encoding:** `limitValue: null` → unlimited; positive integer → monthly request cap; omit row (or explicit `-1` sentinel if needed) → feature not available on tier (maps to current `undefined` in `TIER_LIMITS`).
**Cache invalidation:** After admin saves entitlements, call a small `invalidateEntitlementCache()` so changes apply immediately without waiting for TTL.
**Quota migration pattern:** Replace `await checkEntitlementOrThrow(userId, feature)` + `incrementUsageAsync(...)` with `await reserveUsageOrThrow(userId, feature)` at request start. Do not increment again on success. For streaming chat, reserve before `streamText`; no post-finish increment.
**Usage dashboard:** Show per-tier aggregate from `UsageLog` (last synced month) plus optional live Redis read for current month top features — degraded state if sync cron stale is acceptable (show `syncedAt`).
## Verification
**Commands:**
- `cd memento-note && npm run test:unit -- tests/unit/entitlements.test.ts` — expected: pass after async limit loader changes
- `cd memento-note && npx tsc --noEmit` — expected: no type errors
**Manual checks:**
- ADMIN `/admin/billing`: edit a limit, save, verify `/api/usage/current` reflects new cap for a test user on that tier.
- Non-admin GET to admin-billing server actions returns unauthorized.
- Two rapid AI calls at quota1: only one succeeds.
## Spec Change Log
## Suggested Review Order
**Entitlements & DB limits**
- Central loader: DB `PlanEntitlement` with 60s cache and hardcoded fallback
[`plan-entitlements.ts:1`](../../memento-note/lib/plan-entitlements.ts#L1)
- Prisma model + seeded migration from former `TIER_LIMITS`
[`schema.prisma:814`](../../memento-note/prisma/schema.prisma#L814)
- Entitlements API now reads limits asynchronously from cache
[`entitlements.ts:1`](../../memento-note/lib/entitlements.ts#L1)
**Admin console**
- Server actions: entitlements CRUD, billing config, usage overview
[`admin-billing.ts:1`](../../memento-note/app/actions/admin-billing.ts#L1)
- Admin UI at `/admin/billing` with tier matrix + Stripe price IDs
[`billing-admin-client.tsx:1`](../../memento-note/app/(admin)/admin/billing/billing-admin-client.tsx#L1)
- Audit trail on manual tier override
[`admin.ts:142`](../../memento-note/app/actions/admin.ts#L142)
**Stripe & billing UX**
- Price IDs and `BILLING_ENABLED` via SystemConfig (env fallback)
[`stripe-prices.ts:79`](../../memento-note/lib/billing/stripe-prices.ts#L79)
- Runtime billing flag exposed to settings page
[`billing-plans.tsx:80`](../../memento-note/components/settings/billing-plans.tsx#L80)
**Quota hardening**
- Atomic `reserveUsageOrThrow` on billable AI routes (replaces check+increment)
[`chat/route.ts:69`](../../memento-note/app/api/chat/route.ts#L69)
- Fixed broken suggest-charts usage tracking
[`suggest-charts/route.ts:58`](../../memento-note/app/api/ai/suggest-charts/route.ts#L58)
**Tests**
- Entitlements + billing price map unit tests updated
[`entitlements.test.ts:1`](../../memento-note/tests/unit/entitlements.test.ts#L1)