212 lines
5.8 KiB
Markdown
212 lines
5.8 KiB
Markdown
# Migration 36: Fix Stripe Subscription Period Dates
|
|
|
|
## Overview
|
|
|
|
This migration fixes a critical issue with subscription period tracking and simplifies the profile subscription model.
|
|
|
|
## Problem
|
|
|
|
The previous implementation incorrectly used `subscriptions.current_period_end` to determine billing cycle end dates. However, in Stripe's data model:
|
|
- `subscriptions.current_period_end` = End of the subscription (NULL for ongoing subscriptions)
|
|
- `subscription_items.current_period_end` = End of the current billing cycle (the actual monthly due date)
|
|
|
|
## Solution
|
|
|
|
### Database Changes
|
|
|
|
#### 1. New Enum Type
|
|
Created `subscription_plan` enum with values:
|
|
- `none` - Free user (no subscription)
|
|
- `trial` - User on trial period
|
|
- `standard` - Paid subscriber
|
|
|
|
#### 2. Profile Table Updates
|
|
**Removed:**
|
|
- `is_paying` (boolean) - Replaced with plan enum
|
|
- `subscription_tier` (text) - Replaced with plan enum
|
|
|
|
**Added:**
|
|
- `plan` (subscription_plan enum) - Single source of truth for subscription status
|
|
|
|
#### 3. Updated Functions
|
|
|
|
**`is_paying_user(user_uuid)`**
|
|
- Now uses `subscription_items.current_period_end` for accurate billing cycle tracking
|
|
- Checks if subscription is active/trialing AND current period has not ended
|
|
|
|
**`get_user_subscription_status(user_uuid)`**
|
|
- Returns subscription data including:
|
|
- `current_period_start` and `current_period_end` from `subscription_items`
|
|
- `price_id` from `subscription_items.price`
|
|
- Computed `plan` enum based on subscription status
|
|
|
|
**`get_user_stripe_subscriptions()`**
|
|
- Updated to join with `subscription_items` table
|
|
- Returns `current_period_start/end` from subscription items
|
|
- Includes `price_id` and `quantity` from subscription items
|
|
|
|
**`update_profile_subscription_status()`**
|
|
- Trigger function updated to set `plan` enum instead of `is_paying` and `subscription_tier`
|
|
- Logic prioritizes: trial > standard > none
|
|
- Now correctly uses `subscription_items.current_period_end`
|
|
|
|
#### 4. New Trigger
|
|
Added trigger on `subscription_items` table:
|
|
- `update_profile_on_subscription_item_change`
|
|
- Ensures profile updates when billing cycles change
|
|
|
|
#### 5. Updated Views
|
|
|
|
**`active_subscriptions` view**
|
|
- Now joins with `subscription_items` for accurate period dates
|
|
- Includes `plan` column from profiles
|
|
- Uses `subscription_items.current_period_end` for filtering
|
|
|
|
### TypeScript Type Updates
|
|
|
|
#### Database Types (`packages/shared/src/types/database.types.ts`)
|
|
|
|
**Added enum:**
|
|
```typescript
|
|
subscription_plan: "none" | "trial" | "standard"
|
|
```
|
|
|
|
**Updated `profiles` table type:**
|
|
```typescript
|
|
{
|
|
avatar_url: string | null
|
|
email: string | null
|
|
first_name: string | null
|
|
id: string
|
|
is_temporary: boolean
|
|
last_name: string | null
|
|
last_signed_in: string | null
|
|
name: string | null
|
|
plan: Database["public"]["Enums"]["subscription_plan"] // NEW
|
|
short_user_id: string
|
|
// REMOVED: is_paying, subscription_tier
|
|
}
|
|
```
|
|
|
|
### Frontend Updates
|
|
|
|
#### Component Changes
|
|
|
|
**`SubscriptionCard.tsx`**
|
|
```typescript
|
|
// OLD:
|
|
const isPaying = user.is_paying;
|
|
|
|
// NEW:
|
|
const isPaying = user.plan !== "none";
|
|
```
|
|
|
|
**`stripe.ts` hooks**
|
|
```typescript
|
|
// OLD:
|
|
return { data: user.is_paying, isLoading: false };
|
|
|
|
// NEW:
|
|
return { data: user.plan !== "none", isLoading: false };
|
|
```
|
|
|
|
#### Test File Updates
|
|
All test files updated to use:
|
|
```typescript
|
|
plan: "none" as const
|
|
```
|
|
|
|
Instead of:
|
|
```typescript
|
|
is_paying: false,
|
|
subscription_tier: "free"
|
|
```
|
|
|
|
## Files Changed
|
|
|
|
### SQL
|
|
- ✅ `sql/36_fix_stripe_subscription_dates.sql` (NEW)
|
|
|
|
### TypeScript Types
|
|
- ✅ `packages/shared/src/types/database.types.ts`
|
|
|
|
### Frontend Components
|
|
- ✅ `apps/main/src/components/SubscriptionCard.tsx`
|
|
- ✅ `apps/main/src/hooks/stripe.ts`
|
|
|
|
### Tests
|
|
- ✅ `apps/main/src/components/ProtectedRoute.test.tsx`
|
|
- ✅ `apps/main/src/providers/UserStoreProvider.test.tsx`
|
|
- ✅ `apps/main/src/utils/testHelpers.tsx`
|
|
|
|
## Migration Steps
|
|
|
|
1. **Run the SQL migration:**
|
|
```sql
|
|
\i sql/36_fix_stripe_subscription_dates.sql
|
|
```
|
|
|
|
2. **The migration will:**
|
|
- Create `subscription_plan` enum type
|
|
- Drop `is_paying` and `subscription_tier` columns from profiles
|
|
- Add `plan` column with default `'none'`
|
|
- Update all functions to use `subscription_items.current_period_end`
|
|
- Create trigger on `subscription_items` table
|
|
- Update views to use correct period dates
|
|
|
|
3. **Existing data handling:**
|
|
- All users will default to `plan = 'none'`
|
|
- The triggers will automatically update plans when subscriptions sync
|
|
- Next webhook from Stripe will correct the plan for paying users
|
|
|
|
## Benefits
|
|
|
|
1. **Accurate Billing Dates**: Uses `subscription_items.current_period_end` for actual monthly due dates
|
|
2. **Simplified Model**: Single `plan` enum instead of two separate fields
|
|
3. **Type Safety**: Enum type prevents invalid values
|
|
4. **Clearer Logic**: `plan` is more intuitive than `is_paying` + `subscription_tier`
|
|
5. **Automatic Updates**: Triggers on both `subscriptions` and `subscription_items` ensure data consistency
|
|
|
|
## Data Flow
|
|
|
|
### Before
|
|
```
|
|
Stripe Webhook → subscriptions.current_period_end (NULL)
|
|
→ is_paying = true (wrong logic)
|
|
```
|
|
|
|
### After
|
|
```
|
|
Stripe Webhook → subscription_items.current_period_end (actual date)
|
|
→ plan = 'standard' | 'trial' | 'none'
|
|
```
|
|
|
|
## Testing
|
|
|
|
After migration:
|
|
1. ✅ TypeScript compilation passes
|
|
2. ✅ All test files updated and passing
|
|
3. ✅ Plan enum properly constrained
|
|
4. ✅ Views and functions return correct period dates
|
|
|
|
## Rollback
|
|
|
|
If needed, you can rollback by:
|
|
1. Dropping the new trigger on `subscription_items`
|
|
2. Dropping the `plan` column
|
|
3. Re-adding `is_paying` and `subscription_tier` columns
|
|
4. Reverting function definitions to use `subscriptions.current_period_end`
|
|
|
|
However, this is not recommended as the old implementation had incorrect logic.
|
|
|
|
---
|
|
|
|
**Status**: ✅ Complete
|
|
**Date**: 2025-11-03
|
|
**Breaking Changes**: Yes (profile table schema changed)
|
|
**Frontend Changes**: Yes (component and hook updates)
|
|
|
|
|
|
|
|
|
|
|