💼
VerifiedSimplesilver
Billing Security Auditor
Audit billing code for security vulnerabilities, validate inputs, and ensure payment security best practices.
billing
0v1.0.0
Handle subscription state changes, tier migrations, cancellations, and edge cases in the billing lifecycle.
# Subscription Lifecycle Manager Skill
Handle subscription state changes, tier migrations, cancellations, and edge cases in the billing lifecycle.
## When to Use This Skill
Use when dealing with:
- User upgrades (FREE → PRO/ENTERPRISE)
- User downgrades (PRO → FREE)
- Cancellations
- Payment failures and recovery
- Trial periods
- Subscription pauses/resumes
## Subscription State Machine
```
┌─────────┐
│ NONE │ (no subscription)
└────┬────┘
│
│ signup/free tier
↓
┌─────────┐
│ FREE │
└────┬────┘
│
│ upgrade checkout
↓
┌──────────────┐
│ TRIALING │ (if trial enabled)
└──────┬───────┘
│
│ trial ends + payment succeeds
↓
┌─────────┐
│ ACTIVE │ (PRO/ENTERPRISE)
└────┬────┘
│
├──────→ (payment fails) → ┌────────────┐
│ │ PAST_DUE │
│ └─────┬──────┘
│ │
│ ←───(payment recovered)───────┘
│
├──────→ (user cancels) ──→ ┌──────────────────┐
│ │ CANCEL_AT_PERIOD │
│ │ _END │
│ └────────┬─────────┘
│ │
│ ←──(user reactivates)─────────────┘
│
↓ (period ends if cancelled)
┌─────────┐
│ CANCELED│
└────┬────┘
│
│ downgrade to FREE
↓
┌─────────┐
│ FREE │
└─────────┘
```
## Common Lifecycle Scenarios
### 1. Upgrade: FREE → PRO
**Trigger**: User clicks "Upgrade to Pro"
**Flow**:
1. User clicks upgrade button
2. POST /api/billing/checkout with `priceId` for PRO tier
3. Redirect to Stripe checkout
4. User completes payment
5. Webhook: `checkout.session.completed`
6. `syncSubscriptionToDatabase()` → creates subscription record
7. `tier` = PRO, `status` = active
8. User refreshed → sees PRO features
**Edge Cases**:
- User already has active subscription → Block checkout, show "Manage Subscription"
- User had PRO before, cancelled → Allow re-upgrade (new subscription)
- Payment fails during checkout → No subscription created, user stays FREE
### 2. Downgrade: PRO → FREE
**Trigger**: User cancels in Stripe billing portal
**Flow**:
1. User clicks "Manage Subscription" → opens Stripe portal
2. User clicks "Cancel subscription"
3. Stripe sets `cancel_at_period_end = true`
4. Webhook: `customer.subscription.updated`
5. Database updated: `cancel_at_period_end = true`
6. User continues using PRO until period ends
7. Period ends → Webhook: `customer.subscription.deleted`
8. `downgradeTier()` → sets tier = FREE
9. User now subject to FREE limits
**Edge Cases**:
- User cancels then reactivates before period ends → Keep PRO, set `cancel_at_period_end = false`
- User has 3 active projects but FREE allows 1 → Need grace period or project archive flow
- User in middle of AI generation when downgraded → Allow that generation to complete
### 3. Payment Failure → Recovery
**Trigger**: Credit card declines
**Flow**:
1. Stripe retries payment automatically (configurable)
2. Webhook: `invoice.payment_failed`
3. `markSubscriptionPastDue()` → status = past_due
4. Email sent to user: "Update payment method"
5. Banner shown in app: "Payment failed"
6. User updates card in Stripe portal
7. Stripe retries payment → succeeds
8. Webhook: `invoice.payment_succeeded`
9. `updateBillingPeriod()` → status = active
10. Remove banner, send confirmation email
**Edge Cases**:
- Payment fails 4 times → Stripe cancels subscription
- User ignores emails → After X days, downgrade to FREE
- User disputes charge → Stripe notifies via webhook, handle separately
### 4. Trial Period (if enabled)
**Trigger**: User upgrades with trial
**Flow**:
1. Checkout session created with `trial_period_days = 14`
2. Subscription created with status = trialing
3. User has full PRO access
4. 3 days before trial ends → Webhook: `customer.subscription.trial_will_end`
5. Email sent: "Trial ending soon, card will be charged $19"
6. Trial ends → Stripe charges card
7. If payment succeeds → status = active
8. If payment fails → status = past_due → see payment failure flow
**Edge Cases**:
- User cancels during trial → No charge, downgrade to FREE
- User already had trial before → Don't offer trial again (track in metadata)
### 5. Subscription Pausing (future feature)
**Trigger**: User requests to pause (e.g., vacation)
**Flow**:
1. User clicks "Pause subscription" in billing portal
2. POST /api/billing/pause
3. Stripe API: `stripe.subscriptions.update(id, { pause_collection: { behavior: 'mark_uncollectible' } })`
4. Webhook: `customer.subscription.updated`
5. Database: status = paused
6. User downgraded to FREE during pause
7. User clicks "Resume" → status = active, tier = PRO
## Helper Functions
**Check if upgrade is allowed**:
```typescript
async function canUpgrade(userId: string): Promise<{ allowed: boolean; reason?: string }> {
const sub = await checkUserSubscription(userId);
// No active subscription → can upgrade
if (!sub || sub.status !== 'active') {
return { allowed: true };
}
// Already on paid tier
if (sub.tier !== 'FREE') {
return {
allowed: false,
reason: 'You already have an active subscription. Use "Manage Subscription" to change plans.'
};
}
return { allowed: true };
}
```
**Calculate prorated amount**:
```typescript
function calculateProration(
currentTier: string,
newTier: string,
daysRemaining: number
): number {
// Stripe handles this automatically, but useful for display
const currentPrice = PRICING_PLANS[currentTier].monthlyPrice;
const newPrice = PRICING_PLANS[newTier].monthlyPrice;
const dailyRate = newPrice / 30;
const credit = (currentPrice / 30) * daysRemaining;
return Math.max(0, (dailyRate * daysRemaining) - credit);
}
```
**Handle grace period for downgrade**:
```typescript
async function handleDowngradeGracePeriod(userId: string) {
// Give user 7 days to reduce projects before enforcing limits
const gracePeriodEnd = new Date();
gracePeriodEnd.setDate(gracePeriodEnd.getDate() + 7);
await createNotification(userId, {
type: 'downgrade_grace_period',
message: `You have 7 days to reduce your projects to 1 before FREE tier limits are enforced.`,
expiresAt: gracePeriodEnd,
});
// Schedule job to enforce limits after grace period
// (implement with cron or background job)
}
```
## Best Practices
1. **Always use Stripe as source of truth**: If database and Stripe disagree, trust Stripe
2. **Idempotency**: Webhooks may fire multiple times - handle gracefully
3. **Graceful degradation**: If subscription sync fails, don't block user
4. **Clear communication**: Email user about every state change
5. **Undo window**: Allow short period to undo cancellations
6. **Data retention**: Keep cancelled subscription records for analytics
## Conclusion
Subscription lifecycle is complex. Test every transition thoroughly, especially edge cases where users might lose access to features or data.
Audit billing code for security vulnerabilities, validate inputs, and ensure payment security best practices.
Debug payment issues, trace checkout flows, and analyze webhook failures in billing systems.
Optimize social media engagement through comment strategies and DM automation