xtablo-source/supabase/migrations_backup/36_fix_stripe_subscription_dates.sql

291 lines
9.6 KiB
MySQL
Raw Normal View History

2025-11-03 08:47:34 +00:00
-- ============================================================================
-- 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';