feat: Accept Payments - Stripe Integration with User Tiers (#55) #56

Merged
egullickson merged 17 commits from issue-55-stripe-integration into main 2026-01-19 02:52:25 +00:00
3 changed files with 304 additions and 126 deletions
Showing only changes of commit 1cf4b78075 - Show all commits

View File

@@ -0,0 +1,55 @@
# backend/src/features/subscriptions/
Stripe payment integration for subscription tiers and donations.
## Files
| File | What | When to read |
| ---- | ---- | ------------ |
| `README.md` | Feature overview with architecture diagram | Understanding subscription flow |
## Subdirectories
| Directory | What | When to read |
| --------- | ---- | ------------ |
| `api/` | HTTP controllers and routes | API endpoint changes |
| `domain/` | Services and type definitions | Business logic changes |
| `data/` | Repository for database operations | Database queries |
| `external/stripe/` | Stripe API client wrapper | Stripe integration |
| `migrations/` | Database schema | Schema changes |
| `jobs/` | Scheduled background jobs | Grace period processing |
## Key Patterns
- Repository mapRow() converts snake_case to camelCase
- Webhook idempotency via stripe_event_id unique constraint
- Tier sync to user_profiles.subscription_tier on changes
- Grace period: 30 days after payment failure
- Vehicle selections for tier downgrades (not deleted, just gated)
## API Endpoints
### Subscriptions (Authenticated)
- GET /api/subscriptions - Current subscription
- POST /api/subscriptions/checkout - Create subscription
- POST /api/subscriptions/cancel - Schedule cancellation
- POST /api/subscriptions/reactivate - Cancel pending cancellation
- POST /api/subscriptions/downgrade - Downgrade with vehicle selection
- PUT /api/subscriptions/payment-method - Update payment
- GET /api/subscriptions/invoices - Billing history
### Donations (Authenticated)
- POST /api/donations - Create payment intent
- GET /api/donations - Donation history
### Webhooks (Public)
- POST /api/webhooks/stripe - Stripe webhook (signature verified)
## Environment Variables
- STRIPE_SECRET_KEY
- STRIPE_WEBHOOK_SECRET
- STRIPE_PRO_MONTHLY_PRICE_ID
- STRIPE_PRO_YEARLY_PRICE_ID
- STRIPE_ENTERPRISE_MONTHLY_PRICE_ID
- STRIPE_ENTERPRISE_YEARLY_PRICE_ID

View File

@@ -2,104 +2,181 @@
Stripe integration for subscription management, donations, and tier-based vehicle limits.
## Milestone 1: Core Infrastructure (COMPLETE)
## Architecture
### Database Schema
- `subscriptions` - User subscription records with Stripe integration
- `subscription_events` - Webhook event logging for idempotency
- `donations` - One-time payment tracking
- `tier_vehicle_selections` - User vehicle selections during tier downgrades
```
┌─────────────────────────────────────────────────────────────────┐
│ Frontend (React) │
│ SubscriptionPage / SubscriptionMobileScreen │
│ - TierCard, PaymentMethodForm, BillingHistory │
│ - VehicleSelectionDialog, DowngradeFlow, DonationSection │
└──────────────────────────┬──────────────────────────────────────┘
┌──────────────────────────▼──────────────────────────────────────┐
│ Backend API │
│ /api/subscriptions/* - Subscription management │
│ /api/donations/* - One-time donations │
│ /api/webhooks/stripe - Stripe webhook (public) │
└──────────────────────────┬──────────────────────────────────────┘
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌────────────┐ ┌─────────────────┐
│ Subscriptions │ │ Stripe │ │ User Profile │
│ Service │ │ Client │ │ Repository │
│ │ │ │ │ (tier sync) │
└────────┬────────┘ └─────┬──────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌────────────┐
│ Subscriptions │ │ Stripe │
│ Repository │ │ API │
└────────┬────────┘ └────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ PostgreSQL │
│ subscriptions, subscription_events, donations, │
│ tier_vehicle_selections │
└─────────────────────────────────────────────────────────────────┘
```
### Type Definitions
- **Domain Types** (`domain/subscriptions.types.ts`): Core business entities and request/response types
- **Stripe Types** (`external/stripe/stripe.types.ts`): Simplified Stripe API response types
## Database Schema
### Data Access
- **Repository** (`data/subscriptions.repository.ts`): Database operations with proper snake_case to camelCase mapping
- Subscription CRUD operations
- Event logging with idempotency checks
- Donation tracking
- Vehicle selection management
### subscriptions
Main subscription data linked to user profile.
| Column | Type | Description |
|--------|------|-------------|
| id | UUID | Primary key |
| user_id | VARCHAR(255) | FK to user_profiles.auth0_sub |
| stripe_customer_id | VARCHAR(255) | Stripe customer ID |
| stripe_subscription_id | VARCHAR(255) | Stripe subscription ID (nullable for free) |
| tier | subscription_tier | free, pro, enterprise |
| billing_cycle | billing_cycle | monthly, yearly |
| status | subscription_status | active, past_due, canceled, unpaid |
| current_period_start | TIMESTAMP | Billing period start |
| current_period_end | TIMESTAMP | Billing period end |
| grace_period_end | TIMESTAMP | Grace period expiry (30 days after payment failure) |
| cancel_at_period_end | BOOLEAN | Pending cancellation flag |
### External Integration
- **Stripe Client** (`external/stripe/stripe.client.ts`): Stripe API wrapper with error handling
- Customer creation
- Subscription lifecycle management
- Payment intent creation for donations
- Webhook event verification
- Payment method updates
### subscription_events
Webhook event log for idempotency.
| Column | Type | Description |
|--------|------|-------------|
| id | UUID | Primary key |
| subscription_id | UUID | FK to subscriptions |
| stripe_event_id | VARCHAR(255) | UNIQUE, prevents duplicate processing |
| event_type | VARCHAR(100) | Stripe event type |
| payload | JSONB | Full event payload |
## Environment Variables
### donations
One-time payment records.
| Column | Type | Description |
|--------|------|-------------|
| id | UUID | Primary key |
| user_id | VARCHAR(255) | FK to user_profiles.auth0_sub |
| stripe_payment_intent_id | VARCHAR(255) | UNIQUE |
| amount_cents | INTEGER | Amount in cents |
| currency | VARCHAR(3) | Currency code (default: usd) |
| status | donation_status | pending, succeeded, failed, canceled |
Required for Stripe integration:
- `STRIPE_SECRET_KEY` - Stripe API secret key
- `STRIPE_WEBHOOK_SECRET` - Stripe webhook signing secret
### tier_vehicle_selections
Tracks which vehicles user selected to keep during tier downgrade.
| Column | Type | Description |
|--------|------|-------------|
| id | UUID | Primary key |
| user_id | VARCHAR(255) | FK to user_profiles.auth0_sub |
| vehicle_id | UUID | FK to vehicles |
| selected_at | TIMESTAMP | Selection timestamp |
## API Endpoints
### Subscription Management (Authenticated)
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/subscriptions | Get current subscription |
| POST | /api/subscriptions/checkout | Create Stripe subscription |
| POST | /api/subscriptions/cancel | Schedule cancellation at period end |
| POST | /api/subscriptions/reactivate | Cancel pending cancellation |
| POST | /api/subscriptions/downgrade | Downgrade with vehicle selection |
| PUT | /api/subscriptions/payment-method | Update payment method |
| GET | /api/subscriptions/invoices | Get billing history |
### Donations (Authenticated)
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | /api/donations | Create donation payment intent |
| GET | /api/donations | Get donation history |
### Webhooks (Public)
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | /api/webhooks/stripe | Stripe webhook (signature verified) |
## Webhook Events Handled
| Event | Action |
|-------|--------|
| customer.subscription.created | Update subscription with Stripe subscription ID |
| customer.subscription.updated | Sync status, tier, period dates |
| customer.subscription.deleted | Mark canceled, downgrade to free tier |
| invoice.payment_succeeded | Clear grace period, mark active |
| invoice.payment_failed | Set 30-day grace period |
| payment_intent.succeeded | Mark donation as succeeded |
## Subscription Tiers
Defined in `user_profiles.subscription_tier` enum:
- `free` - Limited features (default)
- `pro` - Enhanced features
- `enterprise` - Full features
| Tier | Price | Vehicle Limit | Features |
|------|-------|---------------|----------|
| Free | $0 | 2 | Basic tracking, standard reports |
| Pro | $1.99/mo or $19.99/yr | 5 | VIN decoding, OCR, API access |
| Enterprise | $4.99/mo or $49.99/yr | Unlimited | All features, priority support |
## Billing Cycles
## Grace Period
- `monthly` - Monthly billing
- `yearly` - Annual billing
When payment fails:
1. Subscription status set to `past_due`
2. Grace period set to 30 days from failure
3. Email notifications sent: immediate, day 23, day 29
4. Daily job at 2:30 AM checks expired grace periods
5. Expired subscriptions downgraded to free tier
## Subscription Status
## Downgrade Flow
- `active` - Subscription is active
- `past_due` - Payment failed, in grace period
- `canceled` - Subscription canceled
- `unpaid` - Payment failed, grace period expired
When user downgrades to a tier with fewer vehicle allowance:
1. Check if current vehicle count > target tier limit
2. If yes, show VehicleSelectionDialog
3. User selects which vehicles to keep
4. Unselected vehicles become tier-gated (hidden, not deleted)
5. Selections saved to tier_vehicle_selections table
6. On upgrade, all vehicles become accessible again
## Milestone 2: Service Layer + Webhook Endpoint (COMPLETE)
## Environment Variables
### Service Layer
- **SubscriptionsService** (`domain/subscriptions.service.ts`): Business logic for subscription management
- Get current subscription for user
- Create new subscription (Stripe customer + free tier record)
- Upgrade subscription (create Stripe subscription with payment method)
- Cancel subscription (schedule for end of period)
- Reactivate subscription (remove pending cancellation)
- Handle Stripe webhook events with idempotency
### Webhook Processing
- **Webhook Events Handled**:
- `customer.subscription.created` - Update subscription record with Stripe subscription ID
- `customer.subscription.updated` - Update status, tier, period dates
- `customer.subscription.deleted` - Mark as canceled, downgrade to free tier
- `invoice.payment_succeeded` - Clear grace period, mark active
- `invoice.payment_failed` - Set 30-day grace period
### API Endpoints
- **WebhooksController** (`api/webhooks.controller.ts`): Webhook event handler
- **Routes** (`api/webhooks.routes.ts`): PUBLIC endpoint with rawBody support
- POST /api/webhooks/stripe - Stripe webhook receiver (no JWT auth, signature verified)
### Integration
- Syncs subscription tier changes to `user_profiles.subscription_tier` via UserProfileRepository
- Uses environment variables for Stripe price IDs (PRO/ENTERPRISE, MONTHLY/YEARLY)
## Next Steps (Future Milestones)
- M3: API endpoints for subscription management (user-facing CRUD)
- M4: Frontend integration and subscription UI
- M5: Testing and documentation
## Database Migration
Run migrations:
```bash
npm run migrate:feature subscriptions
```
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRO_MONTHLY_PRICE_ID=price_...
STRIPE_PRO_YEARLY_PRICE_ID=price_...
STRIPE_ENTERPRISE_MONTHLY_PRICE_ID=price_...
STRIPE_ENTERPRISE_YEARLY_PRICE_ID=price_...
```
## Architecture Notes
## Files
- All database columns use snake_case
- All TypeScript properties use camelCase
- Repository `mapRow()` methods handle case conversion
- Prepared statements used for all queries (no string concatenation)
- Comprehensive error logging with structured logger
- Webhook idempotency via `stripe_event_id` unique constraint
| File | Purpose |
|------|---------|
| migrations/001_subscriptions_tables.sql | Database schema |
| domain/subscriptions.types.ts | TypeScript interfaces |
| domain/subscriptions.service.ts | Business logic |
| domain/donations.service.ts | Donation logic |
| data/subscriptions.repository.ts | Database operations |
| external/stripe/stripe.client.ts | Stripe API wrapper |
| external/stripe/stripe.types.ts | Stripe type definitions |
| api/subscriptions.controller.ts | HTTP handlers |
| api/subscriptions.routes.ts | Authenticated routes |
| api/donations.controller.ts | Donation handlers |
| api/donations.routes.ts | Donation routes |
| api/webhooks.controller.ts | Webhook handler |
| api/webhooks.routes.ts | Public webhook endpoint |
| jobs/grace-period.job.ts | Daily grace period expiration job |

View File

@@ -4,20 +4,21 @@ Frontend UI for subscription management with Stripe integration.
## Overview
Provides subscription tier management, payment method updates, and billing history viewing.
Provides subscription tier management, payment method updates, billing history, donations, and vehicle selection during downgrade flow.
## Components
### TierCard
Displays subscription plan with:
- Plan name and pricing (monthly/yearly)
Subscription plan display with:
- Plan name and pricing (monthly/yearly toggle)
- Feature list
- Current plan indicator
- Upgrade/downgrade button
### PaymentMethodForm
Stripe Elements integration for:
- Credit card input with validation
- Credit card input with CardElement
- Real-time validation
- Payment method creation
- Error handling
@@ -27,85 +28,130 @@ Invoice list with:
- PDF download links
- MUI Table component
### VehicleSelectionDialog
Vehicle selection during downgrade:
- Checkbox list for each vehicle
- Counter showing selected vs allowed
- Warning about tier-gated vehicles
- Validation preventing over-selection
### DowngradeFlow
Orchestrates downgrade process:
- Checks vehicle count vs target tier limit
- Shows VehicleSelectionDialog if needed
- Submits vehicle selections to backend
### DonationSection / DonationSectionMobile
One-time donation form:
- Free-form amount input (no presets)
- $0.50 minimum (Stripe limit)
- Stripe CardElement for payment
- Donation history table
- Success feedback
## Pages
### SubscriptionPage (Desktop)
- Current plan card with status
- Three-column tier cards layout
- Payment method section
MUI-based layout:
- Current plan card with status badges
- Three-column tier comparison
- Monthly/yearly toggle
- Payment method modal
- Billing history table
- Material-UI components
- Donation section
### SubscriptionMobileScreen (Mobile)
Tailwind-based layout:
- GlassCard styling
- Stacked card layout
- Touch-friendly buttons (44px min)
- Tailwind styling
- GlassCard components
- Modal payment forms
## API Integration
All endpoints are in `/subscriptions`:
- GET `/subscriptions` - Current subscription
- POST `/subscriptions/checkout` - Upgrade subscription
- POST `/subscriptions/cancel` - Cancel subscription
- POST `/subscriptions/reactivate` - Reactivate subscription
- PUT `/subscriptions/payment-method` - Update payment method
- GET `/subscriptions/invoices` - Invoice history
### Subscriptions
| Endpoint | Method | Hook |
|----------|--------|------|
| /api/subscriptions | GET | useSubscription() |
| /api/subscriptions/checkout | POST | useCheckout() |
| /api/subscriptions/cancel | POST | useCancelSubscription() |
| /api/subscriptions/reactivate | POST | useReactivateSubscription() |
| /api/subscriptions/downgrade | POST | useDowngrade() |
| /api/subscriptions/payment-method | PUT | useUpdatePaymentMethod() |
| /api/subscriptions/invoices | GET | useInvoices() |
### Donations
| Endpoint | Method | Hook |
|----------|--------|------|
| /api/donations | POST | useCreateDonation() |
| /api/donations | GET | useDonations() |
## Hooks
- `useSubscription()` - Fetch current subscription
- `useCheckout()` - Upgrade subscription
- `useCancelSubscription()` - Cancel subscription
- `useReactivateSubscription()` - Reactivate subscription
- `useInvoices()` - Fetch invoice history
| Hook | Purpose |
|------|---------|
| useSubscription() | Fetch current subscription |
| useCheckout() | Upgrade subscription |
| useCancelSubscription() | Cancel subscription |
| useReactivateSubscription() | Reactivate subscription |
| useDowngrade() | Downgrade with vehicle selection |
| useInvoices() | Fetch billing history |
| useCreateDonation() | Create donation payment |
| useDonations() | Fetch donation history |
## Environment Setup
## Environment Variables
Required environment variable:
```bash
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_...
```
## Subscription Tiers
### Free
### Free ($0)
- 2 vehicles
- Basic tracking
- Standard reports
- Price: $0
### Pro
### Pro ($1.99/month or $19.99/year)
- Up to 5 vehicles
- VIN decoding
- OCR functionality
- API access
- Price: $1.99/month or $19.99/year
### Enterprise
### Enterprise ($4.99/month or $49.99/year)
- Unlimited vehicles
- All Pro features
- Priority support
- Price: $4.99/month or $49.99/year
## Mobile Navigation
## Routing
Add subscription screen to settings navigation:
```typescript
navigateToScreen('Subscription')
```
- Desktop: `/garage/settings/subscription`
- Mobile: `navigateToScreen('Subscription')`
## Desktop Routing
## Files
Route: `/garage/settings/subscription`
| File | Purpose |
|------|---------|
| types/subscription.types.ts | TypeScript interfaces |
| api/subscription.api.ts | API client calls |
| hooks/useSubscription.ts | React Query hooks |
| constants/plans.ts | Tier configuration |
| components/TierCard.tsx | Plan display card |
| components/PaymentMethodForm.tsx | Stripe Elements form |
| components/BillingHistory.tsx | Invoice table |
| components/VehicleSelectionDialog.tsx | Vehicle selection modal |
| components/DowngradeFlow.tsx | Downgrade orchestrator |
| components/DonationSection.tsx | Desktop donation UI |
| components/DonationSectionMobile.tsx | Mobile donation UI |
| pages/SubscriptionPage.tsx | Desktop page |
| mobile/SubscriptionMobileScreen.tsx | Mobile screen |
## Testing
Test subscription flow:
1. View current plan
2. Toggle monthly/yearly billing
3. Select upgrade tier
4. Enter payment method
5. Complete checkout
6. Verify subscription update
7. View billing history
3. Upgrade: Select tier, enter payment, complete checkout
4. Downgrade: Select vehicles to keep if over limit
5. Cancel/reactivate subscription
6. Make a donation
7. View billing and donation history