5.8 KiB
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 periodstandard- Paid subscriber
2. Profile Table Updates
Removed:
is_paying(boolean) - Replaced with plan enumsubscription_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_endfor 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_startandcurrent_period_endfromsubscription_itemsprice_idfromsubscription_items.price- Computed
planenum based on subscription status
get_user_stripe_subscriptions()
- Updated to join with
subscription_itemstable - Returns
current_period_start/endfrom subscription items - Includes
price_idandquantityfrom subscription items
update_profile_subscription_status()
- Trigger function updated to set
planenum instead ofis_payingandsubscription_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_itemsfor accurate period dates - Includes
plancolumn from profiles - Uses
subscription_items.current_period_endfor filtering
TypeScript Type Updates
Database Types (packages/shared/src/types/database.types.ts)
Added enum:
subscription_plan: "none" | "trial" | "standard"
Updated profiles table type:
{
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
// OLD:
const isPaying = user.is_paying;
// NEW:
const isPaying = user.plan !== "none";
stripe.ts hooks
// 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:
plan: "none" as const
Instead of:
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
-
Run the SQL migration:
\i sql/36_fix_stripe_subscription_dates.sql -
The migration will:
- Create
subscription_planenum type - Drop
is_payingandsubscription_tiercolumns from profiles - Add
plancolumn with default'none' - Update all functions to use
subscription_items.current_period_end - Create trigger on
subscription_itemstable - Update views to use correct period dates
- Create
-
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
- All users will default to
Benefits
- Accurate Billing Dates: Uses
subscription_items.current_period_endfor actual monthly due dates - Simplified Model: Single
planenum instead of two separate fields - Type Safety: Enum type prevents invalid values
- Clearer Logic:
planis more intuitive thanis_paying+subscription_tier - Automatic Updates: Triggers on both
subscriptionsandsubscription_itemsensure 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:
- ✅ TypeScript compilation passes
- ✅ All test files updated and passing
- ✅ Plan enum properly constrained
- ✅ Views and functions return correct period dates
Rollback
If needed, you can rollback by:
- Dropping the new trigger on
subscription_items - Dropping the
plancolumn - Re-adding
is_payingandsubscription_tiercolumns - 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)