332 lines
7.9 KiB
Markdown
332 lines
7.9 KiB
Markdown
# Stripe Integration - CareBridge
|
||
|
||
## Overview
|
||
|
||
CareBridge uses Stripe for:
|
||
- **SaaS Subscription**: $19.99/month platform access
|
||
- **Concierge Packages**: 4 service packages (Essentials, Benefits, Concierge Plus, White-Glove)
|
||
|
||
## Architecture
|
||
|
||
### Pricing Configuration (Single Source of Truth)
|
||
|
||
**ALL pricing lives in `src/lib/config/concierge-pricing.ts`**
|
||
|
||
```typescript
|
||
export const CONCIERGE_PACKAGES: Record<PackageType, ConciergePackage> = {
|
||
essentials: {
|
||
name: 'Essentials Package',
|
||
priceRange: '$399 – $899',
|
||
payment_type: 'one_time',
|
||
pricing: {
|
||
type: 'range', // Allows any price in range
|
||
min: 39900, // $399 in cents
|
||
max: 89900 // $899 in cents
|
||
}
|
||
},
|
||
concierge_plus: {
|
||
name: 'Concierge Plus',
|
||
priceRange: '$249 – $499/month',
|
||
payment_type: 'subscription',
|
||
pricing: {
|
||
type: 'tiers', // Predefined Stripe price IDs
|
||
tiers: [
|
||
{
|
||
name: 'Light Support',
|
||
price: 24900,
|
||
priceId: process.env.NEXT_PUBLIC_STRIPE_CONCIERGE_PLUS_LIGHT_PRICE_ID,
|
||
},
|
||
// ... more tiers
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### Flexible Pricing System
|
||
|
||
CareBridge supports two pricing approaches:
|
||
|
||
1. **Tiered Pricing** (Concierge Plus, White-Glove)
|
||
- Predefined Stripe price IDs
|
||
- User selects from available tiers
|
||
- Uses Stripe's price objects
|
||
|
||
2. **Range Pricing** (Essentials, Benefits)
|
||
- Custom pricing within a range
|
||
- Uses Stripe `price_data` for dynamic pricing
|
||
- Staff can set exact price during booking
|
||
|
||
## Stripe Product Setup
|
||
|
||
### Automated Setup Script
|
||
|
||
```bash
|
||
cd scripts
|
||
./setup-carebridge-pricing.sh
|
||
```
|
||
|
||
This creates:
|
||
- SaaS subscription product ($19.99/mo)
|
||
- Concierge Plus tiers (3 prices)
|
||
- White-Glove tiers (3 prices)
|
||
- Test customer
|
||
- Generates .env.stripe with all IDs
|
||
|
||
### Environment Variables
|
||
|
||
Required in `.env.local`:
|
||
|
||
```bash
|
||
# Stripe Keys
|
||
STRIPE_SECRET_KEY=sk_test_...
|
||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
|
||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||
|
||
# SaaS Subscription
|
||
NEXT_PUBLIC_STRIPE_SAAS_MONTHLY_PRICE_ID=price_...
|
||
|
||
# Concierge Plus Tiers
|
||
NEXT_PUBLIC_STRIPE_CONCIERGE_PLUS_LIGHT_PRICE_ID=price_...
|
||
NEXT_PUBLIC_STRIPE_CONCIERGE_PLUS_STANDARD_PRICE_ID=price_...
|
||
NEXT_PUBLIC_STRIPE_CONCIERGE_PLUS_PREMIUM_PRICE_ID=price_...
|
||
|
||
# White-Glove Tiers
|
||
NEXT_PUBLIC_STRIPE_WHITE_GLOVE_STANDARD_PRICE_ID=price_...
|
||
NEXT_PUBLIC_STRIPE_WHITE_GLOVE_PREMIUM_PRICE_ID=price_...
|
||
NEXT_PUBLIC_STRIPE_WHITE_GLOVE_ENTERPRISE_PRICE_ID=price_...
|
||
```
|
||
|
||
## Creating Checkout Sessions
|
||
|
||
### SaaS Subscription
|
||
|
||
```typescript
|
||
// Fixed price - no user input needed
|
||
const response = await fetch('/api/create-subscription-checkout', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ caseId }), // Preserve case context
|
||
})
|
||
```
|
||
|
||
API route automatically uses the SaaS price ID from env vars.
|
||
|
||
### Concierge Packages
|
||
|
||
```typescript
|
||
import { createConciergeCheckout } from '@/lib/actions/concierge-actions'
|
||
|
||
// For tiered pricing (Concierge Plus, White-Glove)
|
||
const result = await createConciergeCheckout({
|
||
package_type: 'concierge_plus',
|
||
price_cents: 24900, // Must match a tier price
|
||
display_name: 'Light Support',
|
||
case_id: caseId,
|
||
})
|
||
|
||
// For range pricing (Essentials, Benefits)
|
||
const result = await createConciergeCheckout({
|
||
package_type: 'essentials',
|
||
price_cents: 50000, // Any value between min/max
|
||
display_name: 'Essentials Package',
|
||
case_id: caseId,
|
||
})
|
||
```
|
||
|
||
The action automatically:
|
||
- Validates price is within allowed range/tiers
|
||
- Creates Stripe checkout with correct mode (subscription vs payment)
|
||
- Adds metadata for webhook routing
|
||
- Returns checkout URL
|
||
|
||
## Webhook Handling
|
||
|
||
**File**: `src/app/api/webhooks/stripe/route.ts`
|
||
|
||
### Metadata-Driven Routing
|
||
|
||
Webhooks determine the type by checking `session.metadata.package_type`:
|
||
|
||
```typescript
|
||
case 'checkout.session.completed': {
|
||
if (session.metadata?.package_type) {
|
||
// Concierge package purchase
|
||
await handleConciergeCheckout(session)
|
||
} else if (session.subscription) {
|
||
// SaaS subscription
|
||
await handleSaaSSubscriptionCheckout(session)
|
||
}
|
||
break
|
||
}
|
||
```
|
||
|
||
### Required Metadata
|
||
|
||
Always include in checkout sessions:
|
||
|
||
```typescript
|
||
metadata: {
|
||
clerk_user_id: userId,
|
||
package_type: 'concierge_plus', // For concierge packages
|
||
payment_type: 'subscription', // one_time, project_based, or subscription
|
||
price_display_name: 'Light Support',
|
||
}
|
||
```
|
||
|
||
### Webhook Events
|
||
|
||
Handle these events:
|
||
|
||
- `checkout.session.completed` - Create subscription/package record
|
||
- `customer.subscription.updated` - Update subscription status
|
||
- `customer.subscription.deleted` - Mark as cancelled
|
||
|
||
## Database Structure
|
||
|
||
### SaaS Subscriptions
|
||
|
||
Table: `subscriptions`
|
||
|
||
```sql
|
||
CREATE TABLE subscriptions (
|
||
id UUID PRIMARY KEY,
|
||
clerk_user_id TEXT NOT NULL,
|
||
stripe_subscription_id VARCHAR(255),
|
||
is_active BOOLEAN DEFAULT false, -- Simple boolean, no tiers
|
||
status VARCHAR(50),
|
||
current_period_end TIMESTAMPTZ,
|
||
-- ...
|
||
)
|
||
```
|
||
|
||
### Concierge Packages
|
||
|
||
Table: `concierge_packages`
|
||
|
||
```sql
|
||
CREATE TABLE concierge_packages (
|
||
id UUID PRIMARY KEY,
|
||
clerk_user_id TEXT NOT NULL,
|
||
package_type VARCHAR(50) NOT NULL, -- essentials, benefits, etc.
|
||
payment_type VARCHAR(20) NOT NULL, -- one_time, project_based, subscription
|
||
stripe_payment_intent_id VARCHAR(255), -- For one-time
|
||
stripe_subscription_id VARCHAR(255), -- For subscriptions
|
||
price_paid_cents INT NOT NULL,
|
||
price_display_name VARCHAR(100),
|
||
status VARCHAR(20) DEFAULT 'pending',
|
||
-- ...
|
||
)
|
||
```
|
||
|
||
## Testing
|
||
|
||
### Local Webhook Testing
|
||
|
||
```bash
|
||
# Terminal 1: Start webhook listener
|
||
stripe listen --forward-to localhost:3000/api/webhooks/stripe
|
||
|
||
# Copy the webhook secret (whsec_...) to .env.local as STRIPE_WEBHOOK_SECRET
|
||
|
||
# Terminal 2: Start dev server
|
||
npm run dev
|
||
```
|
||
|
||
### Test Cards
|
||
|
||
- Success: `4242 4242 4242 4242`
|
||
- Requires Auth: `4000 0025 0000 3155`
|
||
- Declined: `4000 0000 0000 9995`
|
||
|
||
Expiry: Any future date | CVC: Any 3 digits | ZIP: Any 5 digits
|
||
|
||
### Testing Checklist
|
||
|
||
1. ✅ SaaS subscription checkout
|
||
2. ✅ Concierge package with tiers
|
||
3. ✅ Concierge package with custom price
|
||
4. ✅ Webhook creates database records
|
||
5. ✅ Success page displays correctly
|
||
6. ✅ Billing page shows all subscriptions
|
||
|
||
## Common Mistakes
|
||
|
||
### ❌ Mistake #1: Not validating price
|
||
|
||
```typescript
|
||
// WRONG - No validation
|
||
await createConciergeCheckout({
|
||
price_cents: 1000000, // Way above max!
|
||
})
|
||
|
||
// CORRECT - Use the action, it validates automatically
|
||
const result = await createConciergeCheckout({
|
||
package_type: 'essentials',
|
||
price_cents: 50000,
|
||
display_name: 'Essentials',
|
||
})
|
||
|
||
if (!result.success) {
|
||
// Handle validation error
|
||
}
|
||
```
|
||
|
||
### ❌ Mistake #2: Hardcoding price IDs
|
||
|
||
```typescript
|
||
// WRONG - Hardcoded
|
||
const priceId = 'price_abc123'
|
||
|
||
// CORRECT - Use env vars
|
||
const priceId = process.env.NEXT_PUBLIC_STRIPE_SAAS_MONTHLY_PRICE_ID
|
||
```
|
||
|
||
### ❌ Mistake #3: Not including metadata
|
||
|
||
```typescript
|
||
// WRONG - Webhook won't know how to route
|
||
const session = await stripe.checkout.sessions.create({
|
||
line_items: [...],
|
||
mode: 'subscription',
|
||
})
|
||
|
||
// CORRECT - Include routing metadata
|
||
const session = await stripe.checkout.sessions.create({
|
||
line_items: [...],
|
||
mode: 'subscription',
|
||
metadata: {
|
||
clerk_user_id: userId,
|
||
package_type: 'concierge_plus',
|
||
payment_type: 'subscription',
|
||
},
|
||
})
|
||
```
|
||
|
||
### ❌ Mistake #4: Forgetting to await Stripe responses
|
||
|
||
```typescript
|
||
// WRONG - Missing await
|
||
const session = stripe.checkout.sessions.create({...})
|
||
|
||
// CORRECT
|
||
const session = await stripe.checkout.sessions.create({...})
|
||
```
|
||
|
||
## Changing Pricing
|
||
|
||
To change pricing (add tiers, adjust prices, etc.):
|
||
|
||
1. Update `src/lib/config/concierge-pricing.ts`
|
||
2. Create new Stripe products/prices
|
||
3. Update environment variables
|
||
4. No backend code changes needed!
|
||
|
||
The flexible architecture supports any pricing changes without touching server actions or webhooks.
|
||
|
||
## Documentation
|
||
|
||
- `STRIPE-SETUP-GUIDE.md` - Complete setup instructions
|
||
- `CAREBRIDGE-PRICING-IMPLEMENTATION.md` - Technical deep dive
|
||
- `scripts/README.md` - Script documentation
|