kaboot/docs/PAYMENT_FEATURE_PLAN.md
Joey Yakimowich-Payne 2e12edc249
Add Stripe payment integration for AI subscriptions
Implement subscription-based AI access with 250 generations/month at $5/month or $50/year.

Changes:
- Backend: Stripe service, payment routes, webhook handlers, generation tracking
- Frontend: Upgrade page with pricing, payment success/cancel pages, UI prompts
- Database: Add subscription fields to users, payments table, migrations
- Config: Stripe env vars to .env.example, docker-compose.prod.yml, PRODUCTION.md
- Tests: Payment route tests, component tests, subscription hook tests

Users without AI access see upgrade prompts; subscribers see remaining generation count.
2026-01-21 16:11:03 -07:00

226 lines
7.6 KiB
Markdown

# Kaboot Payment Feature Implementation Plan
## Overview
Add Stripe subscription payments to allow users to pay for AI access (`kaboot-ai-access`). Users get 250 AI generations per month for $5/month (or yearly equivalent).
### Pricing Model
- **Monthly**: $5/month for 250 AI generations
- **Yearly**: $50/year for 250 AI generations/month (save ~17%)
- **Grace Period**: 1 day after failed payment before revoking access
- **Refund Policy**: 7-day money-back guarantee
---
## Implementation Checklist
### Phase 1: Backend Infrastructure
- [x] **1.1** Add Stripe dependency to server
- `npm install stripe` in server directory
- File: `server/package.json`
- [x] **1.2** Add environment variables
- `STRIPE_SECRET_KEY` - Stripe secret key
- `STRIPE_WEBHOOK_SECRET` - Webhook signing secret
- `STRIPE_PRICE_ID_MONTHLY` - Monthly price ID
- `STRIPE_PRICE_ID_YEARLY` - Yearly price ID
- Files: `.env.example`, `docs/PRODUCTION.md`
- [x] **1.3** Database migration - Add subscription and generation tracking
- Add `stripe_customer_id` to users table
- Add `subscription_status` (none, active, past_due, canceled)
- Add `subscription_id` for Stripe subscription ID
- Add `subscription_current_period_end` for billing cycle
- Add `generation_count` for current period usage
- Add `generation_reset_date` for when to reset count
- Create `payments` table for payment history
- File: `server/src/db/schema.sql`
- [x] **1.4** Create Stripe service
- Initialize Stripe client
- Create/retrieve customer helper
- Create checkout session helper
- Create customer portal session helper
- File: `server/src/services/stripe.ts`
- [x] **1.5** Create payments routes
- `POST /api/payments/checkout` - Create Stripe Checkout session
- `POST /api/payments/webhook` - Handle Stripe webhooks (raw body)
- `GET /api/payments/status` - Get subscription & generation status
- `POST /api/payments/portal` - Create customer portal session
- File: `server/src/routes/payments.ts`
- [x] **1.6** Implement webhook handlers
- `checkout.session.completed` - Activate subscription, set generation quota
- `customer.subscription.updated` - Sync status changes
- `customer.subscription.deleted` - Mark as canceled
- `invoice.payment_failed` - Set past_due status
- `invoice.paid` - Reset generation count on renewal
- File: `server/src/routes/payments.ts`
- [x] **1.7** Update AI access middleware
- Check subscription status OR existing group membership
- Check generation count against limit (250)
- Increment generation count on AI use
- Return remaining generations in response
- Files: `server/src/middleware/auth.ts`, `server/src/routes/ai.ts` (or equivalent)
- [x] **1.8** Register payments router in main app
- File: `server/src/index.ts`
### Phase 2: Frontend - Upgrade Page
- [x] **2.1** Create UpgradePage component
- Pricing card with monthly/yearly toggle
- Feature comparison (Free vs Pro)
- CTA button triggering Stripe Checkout
- Trust signals (secure payment, money-back guarantee)
- File: `components/UpgradePage.tsx`
- [x] **2.2** Create PaymentResult component
- Success state with confetti
- Cancel/return state
- File: `components/PaymentResult.tsx`
- [x] **2.3** Add routes to App.tsx
- `/upgrade` route
- `/payment/success` route
- `/payment/cancel` route
- File: `App.tsx`
- [x] **2.4** Create payments API service (integrated in UpgradePage)
- `createCheckoutSession(planType: 'monthly' | 'yearly')`
- `getSubscriptionStatus()`
- `createPortalSession()`
- File: `services/paymentsApi.ts`
- [x] **2.5** Update UI to show generation usage
- Show remaining generations in preferences/header
- Show upgrade CTA when generations low or user is free tier
- Files: Various components
- [x] **2.6** Add upgrade prompts in AI generation flow
- When user tries AI generation without access
- When user is low on generations
- Files: Components using AI generation
### Phase 3: Production Updates
- [x] **3.1** Update docker-compose.prod.yml
- Add Stripe environment variables to backend service
- File: `docker-compose.prod.yml`
- [x] **3.2** Update PRODUCTION.md documentation
- Add Stripe configuration section
- Add webhook setup instructions
- Add Stripe Dashboard product setup
- File: `docs/PRODUCTION.md`
- [x] **3.3** Update setup-prod.sh script (not needed - manual env config)
- Prompt for Stripe keys during setup
- File: `scripts/setup-prod.sh`
### Phase 4: Testing
- [ ] **4.1** Test with Stripe test mode
- Use test API keys
- Test card: 4242 4242 4242 4242
- [ ] **4.2** Test webhook locally
- Use Stripe CLI: `stripe listen --forward-to localhost:3001/api/payments/webhook`
- [ ] **4.3** Test full payment flow
- Checkout → Success → Access granted → Generations work
- [ ] **4.4** Test generation limits
- Verify count increments
- Verify block at 250
- Verify reset on renewal
---
## Database Schema Changes
```sql
-- Add subscription fields to users table
ALTER TABLE users ADD COLUMN stripe_customer_id TEXT UNIQUE;
ALTER TABLE users ADD COLUMN subscription_status TEXT DEFAULT 'none';
ALTER TABLE users ADD COLUMN subscription_id TEXT;
ALTER TABLE users ADD COLUMN subscription_current_period_end DATETIME;
ALTER TABLE users ADD COLUMN generation_count INTEGER DEFAULT 0;
ALTER TABLE users ADD COLUMN generation_reset_date DATETIME;
-- Payments log table
CREATE TABLE IF NOT EXISTS payments (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
stripe_payment_intent_id TEXT,
stripe_invoice_id TEXT,
amount INTEGER NOT NULL,
currency TEXT DEFAULT 'usd',
status TEXT NOT NULL,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_payments_user ON payments(user_id);
```
---
## Environment Variables
```bash
# Stripe Configuration
STRIPE_SECRET_KEY=sk_test_... # or sk_live_... for production
STRIPE_WEBHOOK_SECRET=whsec_... # From Stripe Dashboard or CLI
STRIPE_PRICE_ID_MONTHLY=price_... # Monthly plan price ID
STRIPE_PRICE_ID_YEARLY=price_... # Yearly plan price ID
```
---
## API Endpoints
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| `POST` | `/api/payments/checkout` | Required | Create Stripe Checkout session |
| `POST` | `/api/payments/webhook` | Stripe Sig | Handle Stripe webhook events |
| `GET` | `/api/payments/status` | Required | Get subscription & generation status |
| `POST` | `/api/payments/portal` | Required | Create Stripe Customer Portal session |
---
## Stripe Dashboard Setup
1. Create Product: "Kaboot AI Pro"
2. Add Monthly Price: $5.00/month
3. Add Yearly Price: $50.00/year
4. Copy Price IDs to environment variables
5. Set up Webhook endpoint: `https://your-domain.com/api/payments/webhook`
6. Subscribe to events:
- `checkout.session.completed`
- `customer.subscription.created`
- `customer.subscription.updated`
- `customer.subscription.deleted`
- `invoice.paid`
- `invoice.payment_failed`
---
## Generation Tracking Logic
1. On subscription activation: Set `generation_count = 0`, `generation_reset_date = period_end`
2. On each AI generation: Increment `generation_count`
3. Before AI generation: Check `generation_count < 250`
4. On `invoice.paid` (renewal): Reset `generation_count = 0`, update `generation_reset_date`
5. Return `remaining_generations = 250 - generation_count` in API responses
---
## Notes
- Existing `kaboot-ai-access` group users (via Authentik) get unlimited access (grandfathered)
- Subscription users get 250 generations/month regardless of Authentik group
- Both access methods are valid - check either condition in middleware