166 lines
5.6 KiB
Markdown
166 lines
5.6 KiB
Markdown
# 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.
|