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)
164 lines
11 KiB
Markdown
164 lines
11 KiB
Markdown
---
|
||
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 limit−1 | 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 quota−1: 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)
|