From aa12d2226fca8e389de43d6e7fb3868403c7a898 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Sat, 16 May 2026 20:50:29 +0000 Subject: [PATCH] =?UTF-8?q?feat(story-3.6):=20complete=20Stripe=20subscrip?= =?UTF-8?q?tion=20tiers=20=E2=80=94=20enterprise=20card,=20build=20fix,=20?= =?UTF-8?q?i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Story 3.6: Stripe Subscription Tiers - Verified all pre-existing billing implementation (API routes, webhook, sync, UI) - Added Enterprise plan card with contact sales CTA (mailto:sales@momento.app) - Fixed lib/stripe.ts build error (lazy getStripe() + placeholder default) - Added enterpriseFeature1-5 i18n keys to all 15 locales - 22/22 unit tests pass, build succeeds - Story status: ready-for-dev → review --- .../hooks/state/continual-learning-index.json | 47 ++-- .cursor/hooks/state/continual-learning.json | 8 +- AGENTS.md | 4 +- docs/3-6-stripe-subscription-tiers.md | 99 +++++--- docs/4-1-gdpr-cookie-consent.md | 214 ++++++++++++++++++ docs/sprint-status.yaml | 10 +- .../components/settings/billing-plans.tsx | 22 +- memento-note/lib/stripe.ts | 18 +- memento-note/locales/ar.json | 7 +- memento-note/locales/de.json | 7 +- memento-note/locales/en.json | 7 +- memento-note/locales/es.json | 7 +- memento-note/locales/fa.json | 7 +- memento-note/locales/fr.json | 7 +- memento-note/locales/hi.json | 7 +- memento-note/locales/it.json | 7 +- memento-note/locales/ja.json | 7 +- memento-note/locales/ko.json | 7 +- memento-note/locales/nl.json | 7 +- memento-note/locales/pl.json | 7 +- memento-note/locales/pt.json | 7 +- memento-note/locales/ru.json | 7 +- memento-note/locales/zh.json | 7 +- 23 files changed, 449 insertions(+), 78 deletions(-) create mode 100644 docs/4-1-gdpr-cookie-consent.md diff --git a/.cursor/hooks/state/continual-learning-index.json b/.cursor/hooks/state/continual-learning-index.json index eda9f1a..775456f 100644 --- a/.cursor/hooks/state/continual-learning-index.json +++ b/.cursor/hooks/state/continual-learning-index.json @@ -1,20 +1,29 @@ { - "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/ca85061e-6af9-4250-8dc7-9c3bb4839c48/ca85061e-6af9-4250-8dc7-9c3bb4839c48.jsonl": 1778849848000, - "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/ca85061e-6af9-4250-8dc7-9c3bb4839c48/subagents/3bbaec3b-7dce-4eee-916e-7673710c1e13.jsonl": 1778848753000, - "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/5039e847-3035-4f43-b184-46aeceb06764/5039e847-3035-4f43-b184-46aeceb06764.jsonl": 1778838518000, - "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/5039e847-3035-4f43-b184-46aeceb06764/subagents/e13034a9-05cf-47e3-afa0-f6b142866ab1.jsonl": 1778837589000, - "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/9902a438-467f-4d57-8f43-28e7d579a95f/9902a438-467f-4d57-8f43-28e7d579a95f.jsonl": 1778839341000, - "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/2e0ce74c-a31e-49d8-a0d0-a8b224813533/2e0ce74c-a31e-49d8-a0d0-a8b224813533.jsonl": 1778188935000, - "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/0c6fb2d9-1b82-4ca3-b0f4-f8373a62faca/0c6fb2d9-1b82-4ca3-b0f4-f8373a62faca.jsonl": 1778182618000, - "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/a64d78ce-86d3-4ec8-8f79-7589ad05a62c/a64d78ce-86d3-4ec8-8f79-7589ad05a62c.jsonl": 1778846298000, - "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/65570f8a-5cd2-4573-b2d9-0983f2922d1f/65570f8a-5cd2-4573-b2d9-0983f2922d1f.jsonl": 1778231172000, - "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/65570f8a-5cd2-4573-b2d9-0983f2922d1f/subagents/e2881041-49a0-4dca-8df1-614a7a070038.jsonl": 1778226771000, - "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/65570f8a-5cd2-4573-b2d9-0983f2922d1f/subagents/b9a447c6-5a63-4882-b878-5aee9756ce25.jsonl": 1778227602000, - "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/d92dfb04-c148-4a14-a48a-39d4c634caee/d92dfb04-c148-4a14-a48a-39d4c634caee.jsonl": 1778861502000, - "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/5ac57758-0a3c-4502-9473-b63413a39013/subagents/b2833767-42d4-4d3f-952e-b961ea5538d3.jsonl": 1778916944000, - "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/5ac57758-0a3c-4502-9473-b63413a39013/5ac57758-0a3c-4502-9473-b63413a39013.jsonl": 1778916934000, - "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/e3745f62-c3b9-4a21-8942-71bc6f603f77/e3745f62-c3b9-4a21-8942-71bc6f603f77.jsonl": 1778018654000, - "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/e3745f62-c3b9-4a21-8942-71bc6f603f77/subagents/f028b51a-8a84-4a45-8866-95cb05ca9727.jsonl": 1778014992000, - "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/8c2fc9f5-c359-4c67-a0f5-325ee44cebc9/8c2fc9f5-c359-4c67-a0f5-325ee44cebc9.jsonl": 1778751052000, - "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/7b6c0ed0-caad-4157-b048-535452685b73/7b6c0ed0-caad-4157-b048-535452685b73.jsonl": 1778852401000 -} \ No newline at end of file + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/0c6fb2d9-1b82-4ca3-b0f4-f8373a62faca/0c6fb2d9-1b82-4ca3-b0f4-f8373a62faca.jsonl": 1778182618469, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/2e0ce74c-a31e-49d8-a0d0-a8b224813533/2e0ce74c-a31e-49d8-a0d0-a8b224813533.jsonl": 1778188935902, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/394af47d-c5cd-4cef-bef2-2192717439f8/394af47d-c5cd-4cef-bef2-2192717439f8.jsonl": 1778951280378, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/394af47d-c5cd-4cef-bef2-2192717439f8/subagents/0927d889-66b3-4007-87b4-15f8ad9e01f0.jsonl": 1778951401282, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/394af47d-c5cd-4cef-bef2-2192717439f8/subagents/0ddd911c-403c-4d90-a189-069679758338.jsonl": 1778951533153, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/394af47d-c5cd-4cef-bef2-2192717439f8/subagents/59f0c95a-415f-440a-bae2-96020aca9033.jsonl": 1778951400523, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/394af47d-c5cd-4cef-bef2-2192717439f8/subagents/dc63a53e-55bc-4175-b49e-637b408138ac.jsonl": 1778951399831, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/394af47d-c5cd-4cef-bef2-2192717439f8/subagents/f0ad176d-04d7-4d9a-82b8-65273acd313a.jsonl": 1778946728971, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/5039e847-3035-4f43-b184-46aeceb06764/5039e847-3035-4f43-b184-46aeceb06764.jsonl": 1778838518325, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/5039e847-3035-4f43-b184-46aeceb06764/subagents/e13034a9-05cf-47e3-afa0-f6b142866ab1.jsonl": 1778837589740, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/5ac57758-0a3c-4502-9473-b63413a39013/5ac57758-0a3c-4502-9473-b63413a39013.jsonl": 1778921288478, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/5ac57758-0a3c-4502-9473-b63413a39013/subagents/b2833767-42d4-4d3f-952e-b961ea5538d3.jsonl": 1778917054076, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/65570f8a-5cd2-4573-b2d9-0983f2922d1f/65570f8a-5cd2-4573-b2d9-0983f2922d1f.jsonl": 1778231172346, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/65570f8a-5cd2-4573-b2d9-0983f2922d1f/subagents/b9a447c6-5a63-4882-b878-5aee9756ce25.jsonl": 1778227602626, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/65570f8a-5cd2-4573-b2d9-0983f2922d1f/subagents/e2881041-49a0-4dca-8df1-614a7a070038.jsonl": 1778226771429, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/7b6c0ed0-caad-4157-b048-535452685b73/7b6c0ed0-caad-4157-b048-535452685b73.jsonl": 1778852401511, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/8c2fc9f5-c359-4c67-a0f5-325ee44cebc9/8c2fc9f5-c359-4c67-a0f5-325ee44cebc9.jsonl": 1778751052502, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/92d73875-5939-48fb-9f68-86c88b0f2ff7/92d73875-5939-48fb-9f68-86c88b0f2ff7.jsonl": 1778964103281, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/92d73875-5939-48fb-9f68-86c88b0f2ff7/subagents/401ab052-4346-4e0d-8ca9-108c0a5b1a61.jsonl": 1778964141896, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/9902a438-467f-4d57-8f43-28e7d579a95f/9902a438-467f-4d57-8f43-28e7d579a95f.jsonl": 1778839341001, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/a64d78ce-86d3-4ec8-8f79-7589ad05a62c/a64d78ce-86d3-4ec8-8f79-7589ad05a62c.jsonl": 1778846298067, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/ca85061e-6af9-4250-8dc7-9c3bb4839c48/ca85061e-6af9-4250-8dc7-9c3bb4839c48.jsonl": 1778849848444, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/ca85061e-6af9-4250-8dc7-9c3bb4839c48/subagents/3bbaec3b-7dce-4eee-916e-7673710c1e13.jsonl": 1778848753214, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/d92dfb04-c148-4a14-a48a-39d4c634caee/d92dfb04-c148-4a14-a48a-39d4c634caee.jsonl": 1778861502433, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/e3745f62-c3b9-4a21-8942-71bc6f603f77/e3745f62-c3b9-4a21-8942-71bc6f603f77.jsonl": 1778018654221, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/e3745f62-c3b9-4a21-8942-71bc6f603f77/subagents/f028b51a-8a84-4a45-8866-95cb05ca9727.jsonl": 1778014992372, + "/home/devparsa/.cursor/projects/home-devparsa-dev-Momento/agent-transcripts/f0ad176d-04d7-4d9a-82b8-65273acd313a/subagents/96507ccc-6150-4260-a55c-94abd2b57441.jsonl": 1778946698447 +} diff --git a/.cursor/hooks/state/continual-learning.json b/.cursor/hooks/state/continual-learning.json index 182d65e..9606916 100644 --- a/.cursor/hooks/state/continual-learning.json +++ b/.cursor/hooks/state/continual-learning.json @@ -1,8 +1,8 @@ { "version": 1, - "lastRunAtMs": 1778916916450, - "turnsSinceLastRun": 9, - "lastTranscriptMtimeMs": 1778916916347.422, - "lastProcessedGenerationId": "309e0c67-c6f3-45de-b400-cc4455d26b28", + "lastRunAtMs": 1778964093127, + "turnsSinceLastRun": 0, + "lastTranscriptMtimeMs": 1778964092911.085, + "lastProcessedGenerationId": "370e16e5-9bc9-4e07-a658-507f07456acf", "trialStartedAtMs": null } diff --git a/AGENTS.md b/AGENTS.md index 8bd1ff6..29a48da 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,7 @@ ## Learned User Preferences - Préfère les échanges et le travail guidé en français. -- Interface : tout libellé via i18n dans les 15 fichiers `memento-note/locales/*.json` (FR et EN comme références de contenu) ; éviter le texte en dur ; lors d'une demande de traduction complète, mettre à jour toutes les locales concernées dans ces JSON sans poser de questions superflues. +- Interface : tout libellé via i18n dans les 15 fichiers `memento-note/locales/*.json` (FR et EN comme références de contenu) ; éviter le texte en dur ; traductions **contextuelles** (sens produit, pas mot à mot — ex. « connecter votre propre fournisseur ») ; lors d'une traduction complète, mettre à jour toutes les locales concernées ; si l'utilisateur demande seulement les **clés i18n**, ajouter les clés (souvent EN/FR) sans remplir les 15 locales — il traduit souvent avec un autre modèle. - Base de données : **INTERDIT TOTALEMENT** de lancer `prisma db push --force-reset`, `prisma migrate reset`, `DROP TABLE`, `TRUNCATE`, `pg_restore` avec clean, ou TOUTE commande qui vide/supprime des données — MÊME SI l'utilisateur est d'accord — sans avoir d'abord : (1) dumpé la base avec `bash /home/devparsa/dev/Momento/dump-db.sh`, (2) vérifié le dump fait au moins 1Mo, (3) obtenu un "OUI" explicite de l'utilisateur. **4 incidents de perte de données documentés (14/05, 15/05 x2, 16/05). NE JAMAIS REFAIRE ÇA.** - Design produit : migration depuis les gabarits `architectural-grid1` (base cible) et `architectural-grid` ; avancer pas à pas avec validation ; respecter la logique liste / carte de notes puis contenu au clic comme dans la référence. - Contraste éditeur clair face à une sidebar plus sombre ; fiabiliser la navigation du sidebar en s'alignant sur la logique des dossiers de référence design. @@ -22,3 +22,5 @@ - Workflow BMad : stories sous `docs/` (ex. `3-4-host-pays-session-logic.md`), suivi sprint dans `docs/sprint-status.yaml` ; skills sous `.claude/skills/bmad-*` ; `_bmad-output/planning-artifacts` souvent vide — planification de référence dans `docs/`. - PostgreSQL Docker (`memento-postgres`) sur le port 5433 ; Redis Docker (`memento-redis`) sur le port 6379 (voir règles projet). - Règles opérationnelles Prisma et sécurité base de données décrites dans `CLAUDE.md` à la racine du repo. +- i18n : référence `memento-note/locales/en.json` (~2218 clés) ; des textes « non traduits » sont souvent des valeurs **identiques à l'anglais** dans une locale, pas des clés absentes — auditer avec comparaison flatten EN vs locale. +- Guide utilisateur illustré : `docs/guide-utilisateur/README.md`, captures dans `docs/guide-utilisateur/screenshots/` ; régénération via `docs/guide-utilisateur/capture-screenshots.mjs` lancé depuis `memento-note/` (Playwright) ; URL lue depuis `NEXTAUTH_URL` ou `MOMENTO_DOC_BASE_URL`. diff --git a/docs/3-6-stripe-subscription-tiers.md b/docs/3-6-stripe-subscription-tiers.md index be3a303..2ed65a0 100644 --- a/docs/3-6-stripe-subscription-tiers.md +++ b/docs/3-6-stripe-subscription-tiers.md @@ -1,6 +1,6 @@ # Story 3.6: Stripe Subscription Tiers -Status: ready-for-dev +Status: review @@ -33,31 +33,31 @@ so that I can unlock higher quotas and features. ## Tasks / Subtasks -- [ ] Task 1: Dependencies & Stripe client (AC: #9) - - [ ] Subtask 1.1: Add `stripe` npm package (+ `@stripe/stripe-js` if embedded checkout client-side) - - [ ] Subtask 1.2: Create `lib/stripe.ts` — singleton `Stripe` server client, `getStripe()`, price ID map from env - - [ ] Subtask 1.3: Extend `memento-note/.env.example` with `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_PRICE_PRO_MONTHLY`, `STRIPE_PRICE_PRO_ANNUAL`, `STRIPE_PRICE_BUSINESS_MONTHLY`, `STRIPE_PRICE_BUSINESS_ANNUAL`, `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`, `NEXT_PUBLIC_FEATURE_BILLING_ENABLED` -- [ ] Task 2: Billing API routes (AC: #1, #4, #9) - - [ ] Subtask 2.1: `POST /api/billing/create-checkout` — body `{ tier: 'PRO'|'BUSINESS', interval: 'month'|'year' }`; create/find Stripe Customer; create Checkout Session (`mode: 'subscription'`, metadata `userId`, `tier`) - - [ ] Subtask 2.2: Return `{ clientSecret }` for embedded checkout OR `{ url }` for redirect mode - - [ ] Subtask 2.3: `POST /api/billing/portal` — Stripe Billing Portal session for `stripeCustomerId` - - [ ] Subtask 2.4: `GET /api/billing/status` — current tier, status, period end, cancelAtPeriodEnd (for billing page) -- [ ] Task 3: Webhook & subscription sync (AC: #2, #5, #6) - - [ ] Subtask 3.1: `POST /api/billing/webhook` — **raw body** route config (`export const runtime = 'nodejs'`, disable JSON parse middleware for this route only) - - [ ] Subtask 3.2: Implement `lib/billing/sync-subscription-from-stripe.ts` — map Stripe subscription → Prisma `Subscription` upsert - - [ ] Subtask 3.3: Map `stripePriceId` → `SubscriptionTier` via env price ID table (single function `priceIdToTier(priceId)`) - - [ ] Subtask 3.4: Idempotency: store processed event ids in DB or skip if `updatedAt` on subscription already newer (minimal: trust Stripe retry + upsert by `stripeSubscriptionId`) -- [ ] Task 4: Billing UI (AC: #1, #3, #7, #8) - - [ ] Subtask 4.1: `app/(main)/settings/billing/page.tsx` + client component — plan cards, interval toggle, embedded checkout mount - - [ ] Subtask 4.2: Add Billing to `SettingsNav` (CreditCard icon), i18n **all 15** locale files - - [ ] Subtask 4.3: Update `UsageMeter` upgrade CTA: `href="/settings/billing"`; optional: open checkout modal directly from exhausted state (stretch) - - [ ] Subtask 4.4: On checkout success callback: toast + `queryClient.invalidateQueries(['usage','current'])` - - [ ] Subtask 4.5: Enterprise card → contact sales (no checkout) -- [ ] Task 5: Tests & local dev (AC: all) - - [ ] Subtask 5.1: `tests/unit/billing-price-map.test.ts` — priceId → tier mapping - - [ ] Subtask 5.2: `tests/unit/billing-sync.test.ts` — mock Stripe subscription object → Prisma upsert payload - - [ ] Subtask 5.3: Document `stripe listen --forward-to localhost:3000/api/billing/webhook` in Dev Agent Record - - [ ] Subtask 5.4: `npm run build` in `memento-note/` +- [x] Task 1: Dependencies & Stripe client (AC: #9) + - [x] Subtask 1.1: Add `stripe` npm package (+ `@stripe/stripe-js` if embedded checkout client-side) + - [x] Subtask 1.2: Create `lib/stripe.ts` — singleton `Stripe` server client, `getStripe()`, price ID map from env + - [x] Subtask 1.3: Extend `memento-note/.env.example` with `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_PRICE_PRO_MONTHLY`, `STRIPE_PRICE_PRO_ANNUAL`, `STRIPE_PRICE_BUSINESS_MONTHLY`, `STRIPE_PRICE_BUSINESS_ANNUAL`, `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`, `NEXT_PUBLIC_FEATURE_BILLING_ENABLED` +- [x] Task 2: Billing API routes (AC: #1, #4, #9) + - [x] Subtask 2.1: `POST /api/billing/create-checkout` — body `{ tier: 'PRO'|'BUSINESS', interval: 'month'|'year' }`; create/find Stripe Customer; create Checkout Session (`mode: 'subscription'`, metadata `userId`, `tier`) + - [x] Subtask 2.2: Return `{ clientSecret }` for embedded checkout OR `{ url }` for redirect mode + - [x] Subtask 2.3: `POST /api/billing/portal` — Stripe Billing Portal session for `stripeCustomerId` + - [x] Subtask 2.4: `GET /api/billing/status` — current tier, status, period end, cancelAtPeriodEnd (for billing page) +- [x] Task 3: Webhook & subscription sync (AC: #2, #5, #6) + - [x] Subtask 3.1: `POST /api/billing/webhook` — **raw body** route config (`export const runtime = 'nodejs'`, disable JSON parse middleware for this route only) + - [x] Subtask 3.2: Implement `lib/billing/sync-subscription-from-stripe.ts` — map Stripe subscription → Prisma `Subscription` upsert + - [x] Subtask 3.3: Map `stripePriceId` → `SubscriptionTier` via env price ID table (single function `priceIdToTier(priceId)`) + - [x] Subtask 3.4: Idempotency: store processed event ids in DB or skip if `updatedAt` on subscription already newer (minimal: trust Stripe retry + upsert by `stripeSubscriptionId`) +- [x] Task 4: Billing UI (AC: #1, #3, #7, #8) + - [x] Subtask 4.1: `app/(main)/settings/billing/page.tsx` + client component — plan cards, interval toggle, embedded checkout mount + - [x] Subtask 4.2: Add Billing to `SettingsNav` (CreditCard icon), i18n **all 15** locale files + - [x] Subtask 4.3: Update `UsageMeter` upgrade CTA: `href="/settings/billing"`; optional: open checkout modal directly from exhausted state (stretch) + - [x] Subtask 4.4: On checkout success callback: toast + `queryClient.invalidateQueries(['usage','current'])` + - [x] Subtask 4.5: Enterprise card → contact sales (no checkout) +- [x] Task 5: Tests & local dev (AC: all) + - [x] Subtask 5.1: `tests/unit/billing-price-map.test.ts` — priceId → tier mapping + - [x] Subtask 5.2: `tests/unit/billing-sync.test.ts` — mock Stripe subscription object → Prisma upsert payload + - [x] Subtask 5.3: Document `stripe listen --forward-to localhost:3000/api/billing/webhook` in Dev Agent Record + - [x] Subtask 5.4: `npm run build` in `memento-note/` --- @@ -370,14 +370,57 @@ Per `CLAUDE.md`: backup before any migration. **This story should not require sc ### Agent Model Used -{{agent_model_name_version}} +glm-5.1 (via opencode) ### Debug Log References +- Build failed initially due to `STRIPE_SECRET_KEY` check at module evaluation time in `lib/stripe.ts`. Fixed by using placeholder default `sk_test_placeholder` with lazy `getStripe()` for runtime validation. +- Most of the story was **pre-implemented** — Tasks 1-3 backend, Task 4 UI, and Task 5 tests were already in place. Only Task 4.5 (Enterprise card) and the `lib/stripe.ts` build fix were needed. + ### Completion Notes List +- **Task 1:** `stripe` (^22.1.1), `@stripe/stripe-js` (^9.5.0), `@stripe/react-stripe-js` (^6.3.0) already installed. `lib/stripe.ts` already existed — added lazy `getStripe()` function for runtime safety. `.env.example` already documented all `STRIPE_*` vars. +- **Task 2:** All 4 billing API routes (`create-checkout`, `portal`, `status`, `webhook`) already implemented with auth, embedded checkout support, and proper error handling. +- **Task 3:** Webhook handler handles `checkout.session.completed`, `customer.subscription.created/updated/deleted`, `invoice.payment_failed`. `sync-subscription-from-stripe.ts` maps Stripe statuses to Prisma enums correctly. Idempotent via `upsert` on `stripeSubscriptionId`. +- **Task 4:** Billing page with plan cards, interval toggle, embedded checkout modal, usage overview grid, billing info panel. SettingsNav includes Billing with CreditCard icon. UsageMeter links to `/settings/billing`. **Added Enterprise card** with `mailto:sales@momento.app` CTA (no checkout). Added `enterpriseFeature1-5` i18n keys to all 15 locales. +- **Task 5:** 22 unit tests pass (billing-price-map: 10, billing-sync: 12). Build succeeds. + +### Local Development + +To test Stripe integration locally: +1. Set `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, and price IDs in `.env` +2. Run `stripe listen --forward-to localhost:3000/api/billing/webhook` +3. Use test card `4242 4242 4242 4242` for checkout +4. Set `NEXT_PUBLIC_FEATURE_BILLING_ENABLED=true` to show billing UI + ### File List +**NEW (added in this session):** +- (none — Enterprise card added inline to existing file) + +**MODIFIED:** +- `memento-note/lib/stripe.ts` — added lazy `getStripe()` + placeholder default for build safety +- `memento-note/components/settings/billing-plans.tsx` — added Enterprise plan card (contact sales CTA), grid → 4 cols +- `memento-note/locales/*.json` (15 files) — added `enterpriseFeature1-5` keys + +**PRE-EXISTING (verified working):** +- `memento-note/app/api/billing/create-checkout/route.ts` +- `memento-note/app/api/billing/portal/route.ts` +- `memento-note/app/api/billing/status/route.ts` +- `memento-note/app/api/billing/webhook/route.ts` +- `memento-note/lib/billing/stripe-prices.ts` +- `memento-note/lib/billing/sync-subscription-from-stripe.ts` +- `memento-note/app/(main)/settings/billing/page.tsx` +- `memento-note/components/settings/billing-history.tsx` +- `memento-note/components/settings/SettingsNav.tsx` +- `memento-note/components/usage-meter.tsx` +- `memento-note/tests/unit/billing-price-map.test.ts` +- `memento-note/tests/unit/billing-sync.test.ts` + +### Change Log + +- 2026-05-16: Story 3.6 completed — verified all pre-existing billing implementation, added Enterprise card with contact sales CTA, fixed lib/stripe.ts build issue, added Enterprise i18n keys to all 15 locales. 22/22 unit tests pass, build succeeds. + --- ## Story Completion Status @@ -385,5 +428,5 @@ Per `CLAUDE.md`: backup before any migration. **This story should not require sc - Story ID: 3.6 - Story Key: `3-6-stripe-subscription-tiers` - File: `docs/3-6-stripe-subscription-tiers.md` -- Status: **ready-for-dev** +- Status: **review** - Completion Note: Ultimate context engine analysis completed — comprehensive developer guide created. diff --git a/docs/4-1-gdpr-cookie-consent.md b/docs/4-1-gdpr-cookie-consent.md new file mode 100644 index 0000000..d702865 --- /dev/null +++ b/docs/4-1-gdpr-cookie-consent.md @@ -0,0 +1,214 @@ +# Story 4.1: GDPR Cookie Consent Management + +Status: ready-for-dev + + + +## Story + +As a visitor, +I want to granularly accept or reject analytics and tracking cookies, +so that my ePrivacy rights are respected. + +**Epic:** Epic 4 — Enterprise Compliance & Privacy (B2B Requirements) +**FR coverage:** NFR-GDPR1 (granular accept/reject of analytics and tracking cookies; strictly necessary cookies remain enforced) +**Out of scope for this story:** Story 4.4 (explicit AI processing consent modal), Story 4.2 (account deletion), PostHog/Umami full integration (gate only; no analytics vendor required to ship 4.1) + +--- + +## Acceptance Criteria + +1. [AC1] **First-visit banner (NFR-GDPR1):** On any route (including `/login`, `/register`, marketing/home), if no valid consent record exists, a **fixed bottom banner** appears. It cannot be dismissed without an explicit choice: **Accept essentials only**, **Reject non-essential**, or **Manage preferences**. +2. [AC2] **Granular toggles:** In “Manage preferences”, the user can independently toggle **Analytics** (and optionally **Marketing** if reserved for future ads pixels — default off, disabled or hidden until used). **Strictly necessary** cookies are listed as always on, not toggleable, with short explanations. +3. [AC3] **Strictly necessary enforced:** Auth/session cookies (NextAuth), security, and functional preference cookies (`user-language`, theme-related storage) continue to work **without** analytics consent. No analytics/tracking script or non-essential third-party beacon loads before consent. +4. [AC4] **Persistence:** Consent is stored client-side (`localStorage` + optional `memento-cookie-consent` cookie for SSR hints) with schema version, timestamp, and category flags. Re-opening preferences from Settings updates the same record. +5. [AC5] **Authenticated sync:** When a logged-in user accepts analytics, persist `UserAISettings.anonymousAnalytics = true` via existing `updateAISettings`. On reject, set `false`. Guests rely on local storage only. +6. [AC6] **Analytics gate:** Introduce a single choke-point `lib/analytics/track.ts` (or `lib/consent/analytics.ts`) where all future product analytics calls no-op unless `hasAnalyticsConsent()`. Do **not** add PostHog/Umami packages in this story; wire `ErrorReporter` and any future trackers through the gate policy documented below. +7. [AC7] **Settings re-entry:** **Settings → General** (or About) includes a “Cookie preferences” control that reopens the same manage UI without clearing prior choices. +8. [AC8] **i18n & design:** All user-visible strings via `memento-note/locales/*.json` (15 files; FR + EN as content reference). Banner uses existing design tokens (`--ink`, `--concrete`, `--border`, `--memento-paper`, uppercase micro-labels) — no new blue/legacy theme colors; matches `docs/ux-design-specification.md` §Emplacement Légal. +9. [AC9] **Regression:** Language cookie/localStorage flow in `general-settings-client.tsx`, NextAuth login, theme/direction initializers, sidebar, billing (3.6), BYOK (3.5), and AI features work unchanged when user chooses essentials-only. +10. [AC10] **No destructive DB:** No `prisma migrate reset`. If schema change is needed for consent audit log, prefer **no migration** in 4.1 (client + `anonymousAnalytics` only). + +--- + +## Tasks / Subtasks + +- [ ] Task 1: Consent model & utilities (AC: #2, #4, #5) + - [ ] Subtask 1.1: Create `lib/consent/cookie-consent.ts` — types `ConsentCategories`, `ConsentRecord`, `CONSENT_VERSION`, `getConsent()`, `setConsent()`, `hasAnalyticsConsent()`, `hasMarketingConsent()` + - [ ] Subtask 1.2: Mirror consent to `document.cookie` (`memento-cookie-consent=`, `SameSite=Lax`, 1y) for optional SSR read; primary source remains `localStorage` + - [ ] Subtask 1.3: `hooks/use-cookie-consent.ts` — subscribe to storage events for banner hide/show +- [ ] Task 2: UI components (AC: #1, #2, #8) + - [ ] Subtask 2.1: `components/legal/cookie-consent-banner.tsx` — bottom-fixed, z-index above content, below modals; actions: essentials / reject / manage + - [ ] Subtask 2.2: `components/legal/cookie-preferences-dialog.tsx` — toggles + save; list necessary cookies (session, language, theme) + - [ ] Subtask 2.3: Mount `` in `app/layout.tsx` inside `SessionProviderWrapper` so visitors and authed users both see banner +- [ ] Task 3: Analytics gate & ErrorReporter policy (AC: #3, #6) + - [ ] Subtask 3.1: `lib/analytics/track.ts` — `trackClientEvent()` / `trackServerEvent()` no-op unless analytics consent (document for future PostHog per `saas-deployment-prep.md` §D) + - [ ] Subtask 3.2: **Decision (implement):** Treat `ErrorReporter` → `/api/debug/client-error` as **strictly necessary** (authenticated operational logging only; no third-party). Add code comment in `error-reporter.tsx`. Do not send marketing analytics there. + - [ ] Subtask 3.3: Grep for any `Script` third-party loads in `app/` — none today; add lint comment in `track.ts` that new scripts must call consent check +- [ ] Task 4: Settings & i18n (AC: #5, #7, #8) + - [ ] Subtask 4.1: Add “Cookie preferences” button to `general-settings-client.tsx` opening preferences dialog + - [ ] Subtask 4.2: On save with analytics on/off, call `updateAISettings({ anonymousAnalytics })` when session exists + - [ ] Subtask 4.3: Add keys under `consent.*` (and `settings.cookiePreferences` if needed) in **all 15** `memento-note/locales/*.json` + - [ ] Subtask 4.4: Optional: set footer `legal.link2Href` to `/settings/general#cookies` or anchor — do not build full legal CMS in 4.1 +- [ ] Task 5: Manual verification (AC: all) + - [ ] Subtask 5.1: Clear site data → banner shows on `/` and `/login` + - [ ] Subtask 5.2: Essentials only → banner hidden; refresh persists; AI/login still works + - [ ] Subtask 5.3: Accept analytics → `anonymousAnalytics` true in DB for logged-in user + - [ ] Subtask 5.4: `npm run build` in `memento-note/` + +--- + +## Dev Notes + +### Epic context (Epic 4) + +| Story | Scope | Dependency on 4.1 | +|-------|--------|-------------------| +| 4.1 | Cookie banner + category consent | — | +| 4.2 | Hard account deletion | Independent | +| 4.3 | Data export portability | Independent | +| 4.4 | **AI processing consent** (just-in-time modal) | **Separate UI** — do not merge into cookie banner | +| 4.5 | EU data residency | Independent | +| 4.6 | SSO/SAML + audit logs | Independent | + +Epic goal: B2B legal blockers for EU buyers. Cookie consent is the **first** Epic 4 deliverable and unblocks marketing/analytics work later. + +### Critical brownfield reality + +**Already in codebase:** + +- `UserAISettings.anonymousAnalytics` (`Boolean @default(false)`) — field exists, **no UI** exposes it today [`prisma/schema.prisma`]. +- `updateAISettings` already allows `anonymousAnalytics` in allowlist [`app/actions/ai-settings.ts`]. +- Functional cookie pattern for language: `localStorage['user-language']` + `user-language` cookie in `general-settings-client.tsx` and inline script in `app/layout.tsx`. +- Footer i18n placeholders for Privacy / Terms / Cookie Policy (`landing.footer.legal.link*`) — hrefs still `#`. +- `ErrorReporter` in root layout posts client errors to `/api/debug/client-error` (auth required) — **first-party**, not a tracking pixel. + +**Not implemented (this story):** + +- No `CookieConsent` / banner components. +- No `lib/consent/*` or analytics gate. +- No PostHog/Umami in `package.json` (planned in `memento-note/docs/saas-deployment-prep.md` only). + +### Cookie classification (implement exactly) + +| Category | Examples in Momento | Consent required | +|----------|---------------------|------------------| +| **Strictly necessary** | NextAuth session, CSRF (if any), `user-language` cookie, theme/direction localStorage, consent record itself | No — always on | +| **Analytics** | Future PostHog/Umami/Plausible events, product funnel, feature flags tied to identity | Yes — opt-in | +| **Marketing** | Future ad pixels, retargeting | Yes — opt-in (hide toggle until used) | + +**Do not** block the app shell on analytics rejection — only block **non-essential** scripts/events. + +### UX specification (must follow) + +From `docs/ux-design-specification.md`: + +- Banner: **persistent, thin, bottom-anchored** on main UI; actions **“Accept essentials”** and **“Manage”** (map “Reject non-essential” to essentials-only path). +- **No new top-level pages** for consent — overlay/banner + settings re-entry only (Platform Strategy). +- AI consent modal is **Story 4.4** — triggered on first AI action, not on login. + +Visual: reuse architectural-grid tokens; avoid isolated admin-style topbars. + +### Files to create + +``` +memento-note/ +├── lib/consent/cookie-consent.ts # NEW — source of truth +├── lib/analytics/track.ts # NEW — gated no-op + future hook +├── hooks/use-cookie-consent.ts # NEW +├── components/legal/cookie-consent-banner.tsx # NEW +├── components/legal/cookie-preferences-dialog.tsx # NEW +├── components/legal/cookie-consent-root.tsx # NEW — client wrapper +``` + +### Files to update + +| File | Change | +|------|--------| +| `app/layout.tsx` | Render `` after providers | +| `app/(main)/settings/general/general-settings-client.tsx` | “Cookie preferences” button | +| `memento-note/locales/*.json` (×15) | `consent.banner.*`, `consent.preferences.*` | +| `components/error-reporter.tsx` | Comment: necessary cookie / operational, not analytics | + +**Do not modify** unless required: `middleware.ts`, `auth.ts`, Stripe/billing routes, BYOK, entitlements. + +### Suggested consent record shape + +```typescript +type ConsentRecord = { + version: 1 + necessary: true // always true + analytics: boolean + marketing: boolean + updatedAt: string // ISO +} +``` + +Storage keys: `localStorage['memento-consent-v1']`; cookie name `memento-cookie-consent` (JSON base64 or simple flags). + +### Authenticated user sync + +```typescript +// On accept analytics: +await updateAISettings({ anonymousAnalytics: true }) +// On reject: +await updateAISettings({ anonymousAnalytics: false }) +``` + +Load initial dialog state: if logged in, prefer DB `anonymousAnalytics` **only when** local consent missing (first merge); after user sets banner, local record wins until they change preferences again. + +### Analytics future-proofing + +`saas-deployment-prep.md` documents PostHog (§D) and Umami (docker). For 4.1: + +- Implement `hasAnalyticsConsent()` check only. +- When Epic 3+/growth work adds PostHog, initialize in a client provider **inside** `if (hasAnalyticsConsent())` — EU host `https://eu.i.posthog.com` when configured. + +### Previous epic intelligence (Epic 3) + +| Story | Relevance | +|-------|-----------| +| 3.6 Stripe billing | Billing UI at `/settings/billing` — unaffected; no Stripe cookies in 4.1 | +| 3.5 BYOK | Unrelated to cookie categories | +| 3.1–3.4 Quotas | Unaffected | + +No previous story in Epic 4 (`story_num === 1`). + +### Git intelligence (recent) + +Recent work focused on design system, brainstorm, billing meter, dead-code cleanup — **no consent code** in recent commits. Safe greenfield within `components/legal/` and `lib/consent/`. + +### Project structure notes + +- App root: `memento-note/` +- Settings layout: `app/(main)/settings/layout.tsx` + `SettingsNav.tsx` +- i18n: `lib/i18n/LanguageProvider.tsx`, 15 locales under `locales/` +- **Database safety:** No reset; no migration required for 4.1 if using existing `anonymousAnalytics` column only + +### Testing + +Per project policy: **no new automated tests** unless user explicitly requests. Use Task 5 manual checklist only. + +### References + +- [Source: docs/epics.md — Epic 4, Story 4.1] +- [Source: docs/ux-design-specification.md — Flux d'Onboarding Légal, Emplacement Légal] +- [Source: docs/epics.md — NFR-GDPR1] +- [Source: memento-note/docs/saas-deployment-prep.md — §D PostHog/GDPR] +- [Source: memento-note/app/layout.tsx — root scripts & ErrorReporter] +- [Source: memento-note/app/actions/ai-settings.ts — anonymousAnalytics] +- [Source: memento-note/prisma/schema.prisma — UserAISettings.anonymousAnalytics] + +--- + +## Dev Agent Record + +### Agent Model Used + +{{agent_model_name_version}} + +### Debug Log References + +### Completion Notes List + +### File List diff --git a/docs/sprint-status.yaml b/docs/sprint-status.yaml index e493237..360e139 100644 --- a/docs/sprint-status.yaml +++ b/docs/sprint-status.yaml @@ -1,5 +1,5 @@ # generated: 2026-05-14T16:06:50Z -# last_updated: 2026-05-15T20:00:00Z +# last_updated: 2026-05-16T21:00:00Z # project: Momento # project_key: NOKEY # tracking_system: file-system @@ -35,7 +35,7 @@ # - Dev moves story to 'review', then runs code-review (fresh context, different LLM recommended) generated: 2026-05-14T16:06:50Z -last_updated: 2026-05-15T20:00:00Z +last_updated: 2026-05-16T21:00:00Z project: Momento project_key: NOKEY tracking_system: file-system @@ -48,10 +48,10 @@ development_status: 3-3-smart-routing-fallback: done 3-4-host-pays-session-logic: done 3-5-secure-byok-management: review - 3-6-stripe-subscription-tiers: ready-for-dev + 3-6-stripe-subscription-tiers: review epic-3-retrospective: optional - epic-4: backlog - 4-1-gdpr-cookie-consent: backlog + epic-4: in-progress + 4-1-gdpr-cookie-consent: ready-for-dev 4-2-gdpr-right-to-be-forgotten: backlog 4-3-data-portability: backlog 4-4-explicit-ai-consent: backlog diff --git a/memento-note/components/settings/billing-plans.tsx b/memento-note/components/settings/billing-plans.tsx index dbb7735..1e4481c 100644 --- a/memento-note/components/settings/billing-plans.tsx +++ b/memento-note/components/settings/billing-plans.tsx @@ -204,6 +204,26 @@ export function BillingPlans() { : 'bg-ink text-white shadow-xl shadow-ink/20 hover:scale-[1.02] active:scale-95', onClick: () => handleCheckout('BUSINESS'), }, + { + id: 'enterprise', + name: t('billing.enterpriseTitle') || 'Enterprise', + price: t('billing.contactSales') || 'Sur devis', + period: '', + description: t('billing.enterpriseDescription') || 'Quotas personnalisés, SSO, support prioritaire.', + features: [ + t('billing.enterpriseFeature1') || 'Quotas illimités', + t('billing.enterpriseFeature2') || 'SSO / SAML', + t('billing.enterpriseFeature3') || 'Support dédié', + t('billing.enterpriseFeature4') || 'Facturation personnalisée', + t('billing.enterpriseFeature5') || 'SLA garanti', + ], + current: effectiveTier === 'ENTERPRISE', + buttonText: effectiveTier === 'ENTERPRISE' ? (t('billing.currentPlan') || 'Plan Actuel') : (t('billing.contactSales') || 'Contact Sales'), + buttonClass: effectiveTier === 'ENTERPRISE' + ? 'bg-paper text-concrete cursor-default' + : 'bg-ink text-white shadow-xl shadow-ink/20 hover:scale-[1.02] active:scale-95', + onClick: () => { window.location.href = 'mailto:sales@momento.app'; }, + }, ]; return ( @@ -362,7 +382,7 @@ export function BillingPlans() { {/* Plan Cards */} {(billingEnabled || true) && ( -
+
{plans.map((plan) => (