-- ============================================================================ -- Fix Stripe Subscription Period Dates -- ============================================================================ -- This migration fixes the subscription logic to use subscription_items -- for current_period_end (the actual billing cycle end date) instead of -- subscriptions.current_period_end (which is NULL for ongoing subscriptions) -- -- Changes: -- 1. Remove is_paying column from profiles -- 2. Add plan enum column to profiles (trial, standard, none) -- 3. Update all functions to use subscription_items.current_period_end -- ============================================================================ -- ============================================================================ -- Create plan enum type -- ============================================================================ do $$ begin if not exists (select 1 from pg_type where typname = 'subscription_plan') then create type subscription_plan as enum ('none', 'trial', 'standard'); end if; end $$; -- ============================================================================ -- Update profiles table -- ============================================================================ -- Remove is_paying column alter table public.profiles drop column if exists is_paying; -- Remove subscription_tier column (replaced with plan) alter table public.profiles drop column if exists subscription_tier; -- Add plan column alter table public.profiles add column if not exists plan subscription_plan default 'none'; -- ============================================================================ -- Updated Helper Functions -- ============================================================================ -- Function to check if a user is a paying subscriber (using subscription_items) create or replace function public.is_paying_user(user_uuid uuid) returns boolean language plpgsql security definer as $$ declare has_active_subscription boolean; begin select exists( select 1 from stripe.subscriptions s inner join stripe.customers c on c.id = s.customer inner join stripe.subscription_items si on si.subscription = s.id where (c.metadata->>'user_id')::uuid = user_uuid and s.status::text in ('active', 'trialing') and si.current_period_end is not null and to_timestamp(si.current_period_end) > now() ) into has_active_subscription; return has_active_subscription; end; $$; -- Function to get user's subscription status (using subscription_items) create or replace function public.get_user_subscription_status(user_uuid uuid) returns table ( subscription_id text, status text, current_period_start integer, current_period_end integer, cancel_at_period_end boolean, price_id text, product_name text, plan subscription_plan ) language plpgsql security definer as $$ begin return query select s.id, s.status::text, si.current_period_start, si.current_period_end, s.cancel_at_period_end, si.price as price_id, p.name as product_name, case when s.status::text = 'trialing' then 'trial'::subscription_plan when s.status::text in ('active', 'past_due') then 'standard'::subscription_plan else 'none'::subscription_plan end as plan from stripe.subscriptions s inner join stripe.customers c on c.id = s.customer inner join stripe.subscription_items si on si.subscription = s.id left join stripe.prices pr on pr.id = si.price left join stripe.products p on p.id = pr.product where (c.metadata->>'user_id')::uuid = user_uuid and s.status::text in ('active', 'trialing', 'past_due') and si.current_period_end is not null order by si.current_period_end desc limit 1; end; $$; -- ============================================================================ -- Updated Trigger Function -- ============================================================================ -- Function to update profile subscription plan create or replace function public.update_profile_subscription_status() returns trigger as $$ declare v_user_id uuid; v_plan subscription_plan; v_customer_id text; begin -- Get customer ID based on which table triggered this if TG_TABLE_NAME = 'subscriptions' then v_customer_id := new.customer; elsif TG_TABLE_NAME = 'subscription_items' then -- Get customer ID from the subscription select customer into v_customer_id from stripe.subscriptions where id = new.subscription; else -- Unknown table, skip return new; end if; -- Skip if no customer_id found if v_customer_id is null then return new; end if; -- Extract user_id from customer metadata select (metadata->>'user_id')::uuid into v_user_id from stripe.customers where id = v_customer_id; -- Skip if no user_id found if v_user_id is null then return new; end if; -- Determine the user's current plan select case when exists( select 1 from stripe.subscriptions s inner join stripe.customers c on c.id = s.customer inner join stripe.subscription_items si on si.subscription = s.id where (c.metadata->>'user_id')::uuid = v_user_id and s.status::text = 'trialing' and si.current_period_end is not null and to_timestamp(si.current_period_end) > now() ) then 'trial'::subscription_plan when exists( select 1 from stripe.subscriptions s inner join stripe.customers c on c.id = s.customer inner join stripe.subscription_items si on si.subscription = s.id where (c.metadata->>'user_id')::uuid = v_user_id and s.status::text in ('active', 'past_due') and si.current_period_end is not null and to_timestamp(si.current_period_end) > now() ) then 'standard'::subscription_plan else 'none'::subscription_plan end into v_plan; -- Update the user's profile update public.profiles set plan = v_plan where id = v_user_id; return new; end; $$ language plpgsql security definer; -- Recreate trigger (it already exists from previous migration) drop trigger if exists update_profile_on_subscription_change on stripe.subscriptions; create trigger update_profile_on_subscription_change after insert or update on stripe.subscriptions for each row execute function public.update_profile_subscription_status(); -- Also create trigger on subscription_items since that's where period dates are drop trigger if exists update_profile_on_subscription_item_change on stripe.subscription_items; create trigger update_profile_on_subscription_item_change after insert or update on stripe.subscription_items for each row execute function public.update_profile_subscription_status(); -- ============================================================================ -- Updated Views -- ============================================================================ -- View for active subscriptions with user info (using subscription_items) create or replace view public.active_subscriptions as select s.id as subscription_id, (c.metadata->>'user_id')::uuid as user_id, p.email as user_email, p.first_name, p.last_name, s.status::text, to_timestamp(si.current_period_start) as current_period_start, to_timestamp(si.current_period_end) as current_period_end, s.cancel_at_period_end, pr.name as product_name, pc.currency, pc.unit_amount, pc.recurring->>'interval' as interval, p.plan from stripe.subscriptions s inner join stripe.customers c on c.id = s.customer inner join stripe.subscription_items si on si.subscription = s.id inner join public.profiles p on p.id = (c.metadata->>'user_id')::uuid left join stripe.prices pc on pc.id = si.price left join stripe.products pr on pr.id = pc.product where s.status::text in ('active', 'trialing') and si.current_period_end is not null and to_timestamp(si.current_period_end) > now() and (c.metadata->>'user_id') is not null; -- ============================================================================ -- Update RPC function to return subscription with correct period dates -- ============================================================================ -- Function to get user's subscriptions (updated to use subscription_items) create or replace function public.get_user_stripe_subscriptions() returns table ( id text, customer text, user_id uuid, status text, cancel_at_period_end boolean, current_period_start integer, current_period_end integer, created integer, canceled_at integer, trial_start jsonb, trial_end jsonb, price_id text, quantity integer, metadata jsonb ) language plpgsql security definer as $$ begin return query select s.id, s.customer, (c.metadata->>'user_id')::uuid as user_id, s.status::text, s.cancel_at_period_end, si.current_period_start, si.current_period_end, s.created, s.canceled_at, s.trial_start, s.trial_end, si.price as price_id, si.quantity, s.metadata from stripe.subscriptions s inner join stripe.customers c on c.id = s.customer left join stripe.subscription_items si on si.subscription = s.id where (c.metadata->>'user_id')::uuid = auth.uid() order by s.created desc; end; $$; -- ============================================================================ -- Comments for documentation -- ============================================================================ comment on column public.profiles.plan is 'User subscription plan: none (free), trial, or standard'; comment on function public.get_user_subscription_status is 'Returns current subscription details using subscription_items for accurate period dates';