xtablo-source/docs/STRIPE_MIGRATION_36.md
Arthur Belleville 2e16353f5e
etape color
2025-11-19 22:24:23 +01:00

214 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)