xtablo-source/docs/STRIPE_MIGRATION_36.md
2025-11-18 17:09:10 +01:00

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 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:

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

  1. Run the SQL migration:

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