Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:47:35 +08:00
commit b90acffddf
9 changed files with 2356 additions and 0 deletions

View File

@@ -0,0 +1,331 @@
# 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