# Feature 3.4 — Stripe Integration

## Overview
Subscription billing for Analyst ($19.99/mo) and Desk ($49.99/mo) tiers. Founder codes lock a permanent discounted rate. Handles checkout, subscription management, webhooks for tier changes, and cancellation.

## Dependencies
- Feature 1.4 — Database Schema (`users.tier`, `users.stripe_customer_id`, `users.founder_status`)
- Feature 3.1 — Landing Page (pricing cards link to checkout)

## Stripe Products

| Product | Price ID (create in Stripe) | Monthly | Founder Monthly |
|---|---|---|---|
| Analyst | price_analyst_monthly | $19.99 | $14.99 |
| Analyst Founder | price_analyst_founder | $14.99 | — |
| Desk | price_desk_monthly | $49.99 | $34.99 |
| Desk Founder | price_desk_founder | $34.99 | — |

Founder pricing: separate Stripe Price objects with `metadata.is_founder: true`. Once subscribed to a founder price, it's locked — no price increase even if founder pricing ends.

## Endpoints

### POST /api/stripe/checkout
Creates a Stripe Checkout session. Redirects user to Stripe's hosted checkout.

**Request body:**
```json
{
  "tier": "analyst",
  "founder_code": "FOUNDER2026"
}
```

**Response (200):**
```json
{
  "checkout_url": "https://checkout.stripe.com/c/pay/cs_...",
  "session_id": "cs_..."
}
```

**Logic:**
1. Look up user from auth token
2. If user already has a Stripe customer ID, use it. Otherwise create one.
3. Determine price ID:
   - If `founder_code` is valid → use founder price
   - Otherwise → use standard price
4. Create Stripe Checkout Session with:
   - `customer`: Stripe customer ID
   - `line_items`: [{ price: price_id, quantity: 1 }]
   - `mode`: 'subscription'
   - `success_url`: `{BASE_URL}/scan?upgraded=true`
   - `cancel_url`: `{BASE_URL}/pricing`
   - `metadata`: { user_id, tier, is_founder }
5. Return checkout URL

### POST /api/stripe/webhook
Stripe webhook endpoint. Handles subscription lifecycle events.

**Events handled:**
| Event | Action |
|---|---|
| `checkout.session.completed` | Set user tier + stripe_customer_id + founder_status |
| `customer.subscription.updated` | Update tier if plan changed |
| `customer.subscription.deleted` | Revert user to free tier |
| `invoice.payment_failed` | Log warning, Stripe handles retry |

**Webhook verification:** Validate `stripe-signature` header using webhook signing secret.

### POST /api/stripe/portal
Creates a Stripe Customer Portal session for subscription management (cancel, update payment, view invoices).

**Response (200):**
```json
{
  "portal_url": "https://billing.stripe.com/p/session/..."
}
```

### GET /api/stripe/status
Returns current subscription status for the authenticated user.

**Response (200):**
```json
{
  "tier": "analyst",
  "is_founder": true,
  "subscription_status": "active",
  "current_period_end": "2026-04-21T00:00:00Z",
  "cancel_at_period_end": false
}
```

## Founder Code System

Simple validation — not a full promo code engine:
```
VALID_FOUNDER_CODES = ['FOUNDER2026', 'BETONBLK', 'EARLYBIRD']
```

Stored in environment variable: `FOUNDER_CODES=FOUNDER2026,VYNDR,EARLYBIRD`

When a valid founder code is used:
1. Checkout uses the founder price ID
2. `users.founder_status` set to `true`
3. Founder badge displayed in UI

Founder codes have an expiry date (env var `FOUNDER_CODE_EXPIRY`). After expiry, new subscriptions use standard pricing, but existing founder subscribers keep their rate.

## Webhook Security
- Stripe sends signature in `stripe-signature` header
- Verify using `stripe.webhooks.constructEvent(body, sig, webhook_secret)`
- Raw body required (not parsed JSON) — needs `express.raw()` middleware on webhook route
- Webhook secret stored in env: `STRIPE_WEBHOOK_SECRET`

## Service Architecture
```
src/
├── services/
│   └── stripeService.js      # Checkout, portal, webhook handling, tier updates
├── routes/
│   └── stripe.js             # POST /checkout, POST /webhook, POST /portal, GET /status
```

## Environment Variables
```
STRIPE_SECRET_KEY=sk_...
STRIPE_WEBHOOK_SECRET=whsec_...
FOUNDER_CODES=FOUNDER2026,VYNDR,EARLYBIRD
FOUNDER_CODE_EXPIRY=2026-06-30
BASE_URL=https://vyndr.app
```

## Acceptance Criteria
1. `POST /api/stripe/checkout` creates a Stripe Checkout session and returns URL
2. Founder code applies founder pricing when valid
3. Invalid/expired founder code falls back to standard pricing
4. Webhook `checkout.session.completed` updates user tier and stripe_customer_id
5. Webhook `customer.subscription.deleted` reverts user to free tier
6. Webhook signature verification rejects invalid signatures
7. `POST /api/stripe/portal` returns Stripe Customer Portal URL
8. `GET /api/stripe/status` returns current subscription info
9. Founder subscribers keep their rate permanently (locked price in Stripe)
10. All Stripe operations use the service role Supabase client (bypasses RLS)

## Test Plan

### Unit Tests (stripeService.js)
- Correct price ID selected for analyst + founder code
- Correct price ID selected for desk without founder code
- Expired founder code falls back to standard
- Tier mapping: checkout metadata → correct tier string
- Webhook event parsing: checkout.session.completed → tier update

### Integration Tests
- Checkout flow: request → returns valid URL
- Webhook: mock checkout.session.completed → user tier updated in DB
- Webhook: mock subscription.deleted → user reverted to free
- Webhook: invalid signature → 400
- Portal: returns URL for existing customer
- Status: returns correct tier and subscription info
- Auth required on checkout/portal/status (not on webhook)

## Open Questions
- **Stripe test mode:** Build and test entirely in Stripe test mode. Switch to live keys at launch. No code changes needed — just env var swap.
