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