Rend les liens entre notes visibles et persistants (sync NoteLink au save, auto-save, graphe réseau rafraîchi), ajoute living blocks, Memory Echo, recherche globale, consentement IA explicite et consolide les prototypes design en architectural-grid. Co-authored-by: Cursor <cursoragent@cursor.com>
433 lines
22 KiB
Markdown
433 lines
22 KiB
Markdown
# Story 3.6: Stripe Subscription Tiers
|
||
|
||
Status: done
|
||
|
||
<!-- Ultimate context engine analysis completed - comprehensive developer guide created -->
|
||
|
||
## Story
|
||
|
||
As a user,
|
||
I want to upgrade to a paid tier (Pro, Business, Enterprise) via Stripe,
|
||
so that I can unlock higher quotas and features.
|
||
|
||
**Epic:** Epic 3 — The SaaS Commercial Engine (Monetization & API Cost Protection)
|
||
**FR coverage:** FR16 (paid subscription tiers via payment gateway), FR13 (usage meter reflects new limits after upgrade)
|
||
**NFR coverage:** NFR-SC2 (post-checkout entitlement reads remain Redis-fast; tier comes from PG, not Stripe per request)
|
||
|
||
---
|
||
|
||
## Acceptance Criteria
|
||
|
||
1. [AC1] **Checkout Pro & Business:** Authenticated users on BASIC (or expired paid) can start checkout for **Pro** or **Business**, **monthly or annual**, via Stripe. Checkout uses Stripe **Embedded Checkout** (iframe/modal overlay) when possible so the user does not leave the editor context; hosted redirect with `success_url` / `cancel_url` is an acceptable fallback if embedded is blocked.
|
||
2. [AC2] **Instant unlock:** On successful payment (`checkout.session.completed` and/or `customer.subscription.created/updated` webhook), the user's `Subscription` row is upserted with correct `tier`, `status: ACTIVE`, `stripeCustomerId`, `stripeSubscriptionId`, `stripePriceId`, `currentPeriodStart`, `currentPeriodEnd`. `getEffectiveTier(userId)` immediately returns the paid tier (no app restart).
|
||
3. [AC3] **Entitlements wired:** After tier change, `getUserQuotas()` reflects new `TIER_LIMITS` (e.g. PRO chat/reformulate unlocked). Sidebar `UsageMeter` refreshes within one client poll cycle (invalidate React Query `['usage','current']` on success).
|
||
4. [AC4] **Customer portal:** Paid users can open Stripe Customer Portal (manage card, cancel, switch plan) from **Settings → Billing** without custom cancel UI in v1.
|
||
5. [AC5] **Lifecycle webhooks:** Webhook handler verifies `Stripe-Signature`, is idempotent, and handles at minimum: `checkout.session.completed`, `customer.subscription.updated`, `customer.subscription.deleted`, `invoice.payment_failed`. `PAST_DUE` retains tier until `currentPeriodEnd` (matches existing `getEffectiveTier` logic).
|
||
6. [AC6] **Downgrade / cancel:** When subscription ends or is canceled at period end, tier reverts to BASIC after period end; Redis usage keys are **not** deleted (monthly counters stay; limits change via tier only).
|
||
7. [AC7] **Billing settings page:** New `/settings/billing` (nav item + i18n). Shows current plan, period end, upgrade cards (Pro/Business), portal button. Fixes broken link from `UsageMeter` (`/settings?tab=billing` → `/settings/billing`).
|
||
8. [AC8] **Enterprise:** No self-serve Stripe checkout in this story — show **Contact sales** CTA (mailto or static link). ENTERPRISE tier assignment remains manual/admin for now.
|
||
9. [AC9] **Security & config:** Stripe secrets only server-side; webhook raw body preserved for signature verification; `.env.example` documents all `STRIPE_*` price IDs. Feature flag `NEXT_PUBLIC_FEATURE_BILLING_ENABLED` gates checkout UI when false.
|
||
10. [AC10] **Regression:** Stories 3.1–3.5 unchanged for users without paid subscription. BYOK tier gating (3.5) respects post-Stripe tier. Host-pays (3.4) bills host tier limits. No destructive DB commands.
|
||
|
||
---
|
||
|
||
## Tasks / Subtasks
|
||
|
||
- [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/`
|
||
|
||
---
|
||
|
||
## Dev Notes
|
||
|
||
### Epic context
|
||
|
||
| Story | Relevance to 3.6 |
|
||
|-------|------------------|
|
||
| 3.1 | `Subscription` model, `getEffectiveTier`, `TIER_LIMITS`, `UsageMeter`, `/api/usage/current` — **tier source of truth after Stripe** |
|
||
| 3.2–3.3 | Unchanged; paid tier unlocks more features in `TIER_LIMITS` |
|
||
| 3.4 | Host quota uses host's `getEffectiveTier(billingOwnerId)` — upgrades host account unlocks guest AI |
|
||
| 3.5 | BYOK provider lists tier-gated — **on downgrade** deactivate disallowed BYOK keys (minimal: reject saves; optional `isActive=false`) |
|
||
| 4.x | GDPR/SSO separate epics |
|
||
|
||
### Critical brownfield reality
|
||
|
||
**Already implemented (do NOT rebuild):**
|
||
|
||
- Prisma `Subscription` model with `stripeCustomerId`, `stripeSubscriptionId`, `stripePriceId`, period fields, statuses (`ACTIVE`, `PAST_DUE`, `CANCELED`, `TRIALING`, `INACTIVE`).
|
||
- `lib/entitlements.ts` — `getUserInfo`, `getEffectiveTier`, `TIER_LIMITS`, `getUserQuotas` (Redis + tier).
|
||
- `GET /api/usage/current` — powers sidebar meter.
|
||
- `components/usage-meter.tsx` — paywall modal; links to **broken** `/settings?tab=billing`.
|
||
|
||
**Not implemented (this story):**
|
||
|
||
- No `stripe` package in `package.json`.
|
||
- No `lib/stripe.ts`, no `/api/billing/*` routes.
|
||
- No `/settings/billing` page; `SettingsNav` has no billing section.
|
||
- No webhook handler.
|
||
- New users have **no** `Subscription` row — `getUserInfo` returns `{ tier: 'BASIC', status: 'INACTIVE' }` (correct).
|
||
|
||
```653:674:memento-note/prisma/schema.prisma
|
||
model Subscription {
|
||
id String @id @default(cuid())
|
||
userId String @unique
|
||
tier SubscriptionTier @default(BASIC)
|
||
status SubscriptionStatus @default(ACTIVE)
|
||
stripeCustomerId String? @unique
|
||
stripeSubscriptionId String? @unique
|
||
stripePriceId String?
|
||
// ...
|
||
currentPeriodStart DateTime
|
||
currentPeriodEnd DateTime
|
||
user User @relation(...)
|
||
}
|
||
```
|
||
|
||
**Important:** First webhook upsert must set valid `currentPeriodStart` / `currentPeriodEnd` from Stripe subscription object (`current_period_start`, `current_period_end`).
|
||
|
||
### Tier ↔ Stripe price mapping
|
||
|
||
Align with `docs/gtm-pricing-strategy.md`:
|
||
|
||
| Tier | Monthly (EUR) | Annual (EUR) | Self-serve in 3.6 |
|
||
|------|-----------------|--------------|-------------------|
|
||
| BASIC | Free | — | N/A (default) |
|
||
| PRO | €9.90 | €99 | Yes |
|
||
| BUSINESS | €29.90 | €299 | Yes |
|
||
| ENTERPRISE | Custom | Custom | **No** (contact sales) |
|
||
|
||
Create products/prices in Stripe Dashboard (test mode first). Map via env vars — **never hardcode** `price_xxx` in source.
|
||
|
||
```typescript
|
||
// lib/billing/stripe-prices.ts (recommended)
|
||
export function resolvePriceId(tier: 'PRO' | 'BUSINESS', interval: 'month' | 'year'): string {
|
||
const map = {
|
||
PRO: { month: process.env.STRIPE_PRICE_PRO_MONTHLY!, year: process.env.STRIPE_PRICE_PRO_ANNUAL! },
|
||
BUSINESS: { month: process.env.STRIPE_PRICE_BUSINESS_MONTHLY!, year: process.env.STRIPE_PRICE_BUSINESS_ANNUAL! },
|
||
}
|
||
return map[tier][interval]
|
||
}
|
||
|
||
export function priceIdToTier(priceId: string): SubscriptionTier | null {
|
||
// compare against all env price ids
|
||
}
|
||
```
|
||
|
||
### `getEffectiveTier` — preserve existing behavior
|
||
|
||
```163:175:memento-note/lib/entitlements.ts
|
||
export async function getEffectiveTier(userId: string): Promise<SubscriptionTier> {
|
||
const { tier, status, currentPeriodEnd } = await getUserInfo(userId);
|
||
if (status === 'ACTIVE' || status === 'TRIALING') return tier;
|
||
if ((status === 'PAST_DUE' || status === 'CANCELED') && currentPeriodEnd) {
|
||
if (new Date() < new Date(currentPeriodEnd)) return tier;
|
||
}
|
||
return 'BASIC';
|
||
}
|
||
```
|
||
|
||
Webhook mapping:
|
||
|
||
| Stripe status | Prisma `SubscriptionStatus` |
|
||
|---------------|------------------------------|
|
||
| active | ACTIVE |
|
||
| trialing | TRIALING |
|
||
| past_due | PAST_DUE |
|
||
| canceled, unpaid | CANCELED (keep period end) |
|
||
| incomplete_expired | INACTIVE → effective BASIC |
|
||
|
||
### Checkout flow (UX-aligned)
|
||
|
||
Per `docs/ux-design-specification.md`:
|
||
|
||
- **Zero-redirect preferred:** Stripe Embedded Checkout in modal over editor/settings.
|
||
- **Success:** Close modal → invalidate usage query → meter shows higher limits / "Unlimited" where applicable.
|
||
- **No new topbar** — billing lives under existing settings shell (`settings/layout.tsx`).
|
||
|
||
Suggested API contract:
|
||
|
||
```typescript
|
||
// POST /api/billing/create-checkout
|
||
// Response for embedded:
|
||
{ clientSecret: string, sessionId: string }
|
||
// Or redirect fallback:
|
||
{ url: string }
|
||
```
|
||
|
||
Metadata on Checkout Session / Subscription:
|
||
|
||
```json
|
||
{ "userId": "<cuid>", "tier": "PRO" }
|
||
```
|
||
|
||
Always set `customer_email` from session user if creating new Stripe Customer.
|
||
|
||
### Webhook route (Next.js App Router)
|
||
|
||
Use **one** path: `app/api/billing/webhook/route.ts` (matches `saas-deployment-prep.md` §3.3).
|
||
|
||
```typescript
|
||
import { headers } from 'next/headers'
|
||
import { stripe } from '@/lib/stripe'
|
||
|
||
export async function POST(req: Request) {
|
||
const body = await req.text() // NOT req.json()
|
||
const sig = (await headers()).get('stripe-signature')!
|
||
const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)
|
||
// dispatch...
|
||
}
|
||
```
|
||
|
||
Register webhook events in Stripe Dashboard for test endpoint.
|
||
|
||
### Customer portal
|
||
|
||
```typescript
|
||
const portal = await stripe.billingPortal.sessions.create({
|
||
customer: subscription.stripeCustomerId,
|
||
return_url: `${origin}/settings/billing`,
|
||
})
|
||
return { url: portal.url }
|
||
```
|
||
|
||
### Files — expected touch list
|
||
|
||
**NEW**
|
||
|
||
- `memento-note/lib/stripe.ts`
|
||
- `memento-note/lib/billing/stripe-prices.ts`
|
||
- `memento-note/lib/billing/sync-subscription-from-stripe.ts`
|
||
- `memento-note/app/api/billing/create-checkout/route.ts`
|
||
- `memento-note/app/api/billing/portal/route.ts`
|
||
- `memento-note/app/api/billing/webhook/route.ts`
|
||
- `memento-note/app/api/billing/status/route.ts`
|
||
- `memento-note/app/(main)/settings/billing/page.tsx`
|
||
- `memento-note/components/settings/billing-plans.tsx` (or similar)
|
||
- `memento-note/tests/unit/billing-price-map.test.ts`
|
||
- `memento-note/tests/unit/billing-sync.test.ts`
|
||
|
||
**UPDATE**
|
||
|
||
- `memento-note/package.json` — stripe deps
|
||
- `memento-note/.env.example`
|
||
- `memento-note/components/settings/SettingsNav.tsx` — billing nav item
|
||
- `memento-note/components/usage-meter.tsx` — fix billing href; optional embedded checkout trigger
|
||
- `memento-note/locales/*.json` (15 files) — `billing.*` keys
|
||
- `memento-note/components/settings/index.ts` — export if needed
|
||
|
||
**READ BEFORE MODIFY**
|
||
|
||
| File | Current state | What 3.6 changes |
|
||
|------|---------------|------------------|
|
||
| `lib/entitlements.ts` | Tier from `Subscription` table | No logic change unless adding `TRIALING` tier limits (use paid tier while TRIALING) |
|
||
| `components/usage-meter.tsx` | Modal → broken `/settings?tab=billing` | Point to `/settings/billing`; refresh quotas on return |
|
||
| `app/(main)/settings/layout.tsx` | Shell for settings | Host new billing page |
|
||
| `prisma/schema.prisma` | Subscription model exists | **No schema change required** unless adding `StripeWebhookEvent` idempotency table (optional) |
|
||
|
||
### Scope boundaries (do NOT implement in 3.6)
|
||
|
||
- Seat-based Enterprise billing / Stripe Licensing API (`saas-deployment-prep.md` §D) — defer
|
||
- AI credit add-ons (+100 packs) — defer
|
||
- Trial without card (14-day) — optional flag only; no custom trial logic unless Stripe Dashboard trial configured on prices
|
||
- `UsageLog` analytics pipeline / MRR dashboard — defer
|
||
- Workspace-level billing (team pays once) — defer; subscription remains **per User** as schema defines today
|
||
- Changing `TIER_LIMITS` numbers — out of scope (product already codified in entitlements.ts)
|
||
- Tax/VAT invoicing beyond Stripe defaults
|
||
|
||
### Product decisions (document in Dev Agent Record)
|
||
|
||
| Decision | Recommendation |
|
||
|----------|----------------|
|
||
| Checkout UX | Embedded Checkout in modal; redirect fallback OK |
|
||
| Upgrade path | BASIC → PRO or BUSINESS; BUSINESS upgrade from PRO via new checkout or portal |
|
||
| Proration | Let Stripe handle via Dashboard settings |
|
||
| Currency | EUR default prices in Stripe; multi-currency via Stripe adaptive pricing later |
|
||
| Webhook idempotency | Upsert by `stripeSubscriptionId` sufficient for v1 |
|
||
| 3.5 interaction | On tier downgrade in webhook, call helper to deactivate BYOK keys for disallowed providers |
|
||
|
||
### Testing standards
|
||
|
||
- Unit tests with mocked Stripe objects (no live API in CI).
|
||
- Manual: Stripe CLI webhook forward + test card `4242...`.
|
||
- Verify: after webhook, `GET /api/usage/current` shows PRO limits (e.g. `chat` limit 100).
|
||
- Verify: canceled sub past `currentPeriodEnd` → BASIC limits.
|
||
|
||
### Database safety
|
||
|
||
Per `CLAUDE.md`: backup before any migration. **This story should not require schema migration** if `Subscription` model is sufficient. If adding `StripeEvent` table for idempotency, backup + `prisma migrate dev` only.
|
||
|
||
---
|
||
|
||
## Dev Agent Guardrails
|
||
|
||
### Technical requirements
|
||
|
||
- **Stripe API version:** Pin in `lib/stripe.ts` (e.g. `apiVersion: '2024-11-20.acacia'` or latest stable at implementation time — check Stripe docs).
|
||
- **Auth:** All billing routes except webhook require `auth()` session.
|
||
- **Webhook:** No session auth; signature only.
|
||
- **Performance:** Do not call Stripe API inside `canUseFeature` hot path — tier already in PG.
|
||
- **402 responses:** Unchanged; upgrade CTA routes to billing page.
|
||
|
||
### Architecture compliance
|
||
|
||
- Brownfield Next.js under `memento-note/`.
|
||
- Billing domain in `lib/billing/*` + `app/api/billing/*` — do not scatter Stripe calls in chat/brainstorm routes.
|
||
- i18n: zero hardcoded UI strings (FR/EN reference, all 15 locales).
|
||
|
||
### Library / framework requirements
|
||
|
||
- Official `stripe` Node SDK (server).
|
||
- `@stripe/stripe-js` + `@stripe/react-stripe-js` only if using Embedded Checkout on client.
|
||
- Alternative: redirect Checkout only (fewer deps, worse UX) — document choice in Dev Agent Record.
|
||
|
||
### File structure requirements
|
||
|
||
- Webhook and checkout routes under `app/api/billing/` (not `app/api/webhook/stripe/` — avoid duplicate handlers).
|
||
|
||
---
|
||
|
||
## Previous Story Intelligence
|
||
|
||
**Source:** `docs/3-5-secure-byok-management.md`
|
||
|
||
- Tier gating for BYOK providers: PRO vs BUSINESS lists — **webhook tier downgrade** should deactivate keys for providers no longer allowed.
|
||
- `QuotaExceededError.byokConfigured` — billing page is separate upsell path from BYOK.
|
||
|
||
**Source:** `docs/3-4-host-pays-session-logic.md`
|
||
|
||
- Guest AI uses host tier; upgrading host via Stripe unlocks collaborative AI for guests.
|
||
|
||
**Source:** `docs/3-1-freemium-quota-tracking.md`
|
||
|
||
- `Subscription` + `getEffectiveTier` already built; 3.6 fills the **payment** side.
|
||
- Review note: period fields on Subscription — webhook must populate from Stripe.
|
||
|
||
**Source:** `docs/3-1` code review deferrals
|
||
|
||
- "Subscription requires period fields" — addressed by Stripe webhook sync in 3.6.
|
||
|
||
---
|
||
|
||
## Git Intelligence Summary
|
||
|
||
| Commit | Insight |
|
||
|--------|---------|
|
||
| `1fcea6e` | Brainstorm/AI surfaces live — paid tier unlocks `brainstorm_*` limits for hosts |
|
||
| `41596c2` | Env-based provider keys pattern — mirror for `STRIPE_*` env vars |
|
||
| `195e845` | Security-conscious patterns — webhook signature verification mandatory |
|
||
|
||
---
|
||
|
||
## Latest Technical Information
|
||
|
||
- **Stripe Embedded Checkout (2024+):** Supports `ui_mode: 'embedded'` on Checkout Sessions; returns `client_secret` for front-end mount — fits UX "overlay" requirement.
|
||
- **Stripe webhooks:** Use `constructEvent` on raw body; subscribe to subscription lifecycle events.
|
||
- **Next.js App Router:** For webhook route, read raw body with `req.text()`; do not use body parser middleware that consumes JSON globally for that route.
|
||
- **Prisma:** `upsert` on `userId` or `stripeSubscriptionId` for idempotent sync.
|
||
|
||
---
|
||
|
||
## Project Context Reference
|
||
|
||
| Document | Use |
|
||
|----------|-----|
|
||
| `docs/epics.md` | Story 3.6 AC + FR16 |
|
||
| `docs/gtm-pricing-strategy.md` | Prices, annual discount, Enterprise positioning |
|
||
| `docs/ux-design-specification.md` | Zero-redirect, overlay checkout, meter refresh |
|
||
| `memento-note/docs/saas-deployment-prep.md` | §3.3 endpoints, §4.1 Stripe products, §B env vars |
|
||
| `docs/3-1-freemium-quota-tracking.md` | Entitlements + UsageMeter foundation |
|
||
| `docs/3-5-secure-byok-management.md` | Tier downgrade + BYOK interaction |
|
||
| `docs/3-4-host-pays-session-logic.md` | Host tier drives guest billing |
|
||
| `CLAUDE.md` | Database safety |
|
||
| `AGENTS.md` | i18n 15 locales, French communication |
|
||
|
||
---
|
||
|
||
## Dev Agent Record
|
||
|
||
### Agent Model Used
|
||
|
||
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
|
||
|
||
- Story ID: 3.6
|
||
- Story Key: `3-6-stripe-subscription-tiers`
|
||
- File: `docs/3-6-stripe-subscription-tiers.md`
|
||
- Status: **review**
|
||
- Completion Note: Ultimate context engine analysis completed — comprehensive developer guide created.
|