-- ============================================================================ -- Stripe Integration - Custom Extensions -- ============================================================================ -- This file adds custom functionality on top of @supabase/stripe-sync-engine -- The library creates and manages all stripe.* tables - we don't modify them! -- We only: create views, add profile integration, and helper functions -- ============================================================================ -- IMPORTANT: Run the stripe-sync-engine migrations FIRST before running this! -- See: docs/STRIPE_WITH_SYNC_ENGINE.md -- ============================================================================ -- ============================================================================ -- Functions to Access Stripe Data (Security Definer) -- ============================================================================ -- Since we don't modify the stripe schema, we use security definer functions -- to access the data and filter by user_id from metadata -- ============================================================================ -- Function to get user's customer record create or replace function public.get_user_stripe_customer() returns table ( id text, email text, user_id uuid, created integer, metadata jsonb ) language plpgsql security definer as $$ begin return query select c.id, c.email, (c.metadata->>'user_id')::uuid as user_id, c.created, c.metadata from stripe.customers c where (c.metadata->>'user_id')::uuid = auth.uid(); end; $$; -- Function to get user's subscriptions create or replace function public.get_user_stripe_subscriptions() returns table ( id text, customer text, user_id uuid, status text, items jsonb, cancel_at_period_end boolean, current_period_start integer, current_period_end integer, created integer, canceled_at integer, trial_start jsonb, trial_end jsonb, 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.items, s.cancel_at_period_end, s.current_period_start, s.current_period_end, s.created, s.canceled_at, s.trial_start, s.trial_end, s.metadata from stripe.subscriptions s inner join stripe.customers c on c.id = s.customer where (c.metadata->>'user_id')::uuid = auth.uid(); end; $$; -- Function to get all stripe products (public access) create or replace function public.get_stripe_products() returns table ( id text, name text, description text, active boolean, created integer, metadata jsonb ) language plpgsql security definer as $$ begin return query select p.id, p.name, p.description, p.active, p.created, p.metadata from stripe.products p where p.active = true; end; $$; -- Function to get all stripe prices (public access) create or replace function public.get_stripe_prices() returns table ( id text, product text, active boolean, currency text, unit_amount integer, recurring jsonb, created integer, metadata jsonb ) language plpgsql security definer as $$ begin return query select pr.id, pr.product, pr.active, pr.currency, pr.unit_amount, pr.recurring, pr.created, pr.metadata from stripe.prices pr where pr.active = true; end; $$; -- ============================================================================ -- Add subscription status to profiles -- ============================================================================ -- Add is_paying column to profiles table alter table public.profiles add column if not exists is_paying boolean default false; -- Add subscription_tier column to profiles table alter table public.profiles add column if not exists subscription_tier text default 'free'; -- ============================================================================ -- Helper Functions -- ============================================================================ -- Function to check if a user is a paying subscriber 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 where (c.metadata->>'user_id')::uuid = user_uuid and s.status::text in ('active', 'trialing') and to_timestamp(s.current_period_end) > now() ) into has_active_subscription; return has_active_subscription; end; $$; -- Function to get user's subscription status create or replace function public.get_user_subscription_status(user_uuid uuid) returns table ( subscription_id text, status text, current_period_end integer, cancel_at_period_end boolean, items jsonb, product_name text ) language plpgsql security definer as $$ declare first_item jsonb; price_id text; begin return query select s.id, s.status::text, s.current_period_end, s.cancel_at_period_end, s.items, p.name as product_name from stripe.subscriptions s inner join stripe.customers c on c.id = s.customer left join lateral ( select (s.items->'data'->0->>'price')::text as extracted_price_id ) price_extract on true left join stripe.prices pr on pr.id = price_extract.extracted_price_id 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') order by s.current_period_end desc limit 1; end; $$; -- Function to get user's Stripe customer ID create or replace function public.get_user_stripe_customer_id(user_uuid uuid) returns text language plpgsql security definer as $$ declare customer_id text; begin select id into customer_id from stripe.customers where (metadata->>'user_id')::uuid = user_uuid limit 1; return customer_id; end; $$; -- Function to update profile subscription status -- Drop the function to update profile subscription status drop function if exists public.update_profile_subscription_status(); create function public.update_profile_subscription_status() returns trigger as $$ declare v_user_id uuid; 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; -- Update the user's profile when subscription changes update public.profiles set is_paying = ( select exists( select 1 from stripe.subscriptions s inner join stripe.customers c on c.id = s.customer where (c.metadata->>'user_id')::uuid = v_user_id and s.status::text in ('active', 'trialing') and to_timestamp(s.current_period_end) > now() ) ), subscription_tier = ( case when new.status::text in ('active', 'trialing') then 'standard' else 'free' end ) where id = v_user_id; return new; end; $$ language plpgsql security definer; -- Trigger to update profile when subscription changes 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(); 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(); -- ============================================================================ -- Views for convenience -- ============================================================================ -- View for active subscriptions with user info 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(s.current_period_start) as current_period_start, to_timestamp(s.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 from stripe.subscriptions s inner join stripe.customers c on c.id = s.customer inner join public.profiles p on p.id = (c.metadata->>'user_id')::uuid left join lateral ( select (s.items->'data'->0->>'price')::text as extracted_price_id ) price_extract on true left join stripe.prices pc on pc.id = price_extract.extracted_price_id left join stripe.products pr on pr.id = pc.product where s.status::text in ('active', 'trialing') and to_timestamp(s.current_period_end) > now() and (c.metadata->>'user_id') is not null; -- ============================================================================ -- Comments for documentation -- ============================================================================ comment on view public.active_subscriptions is 'Active subscriptions joined with user profile data'; comment on function public.get_user_stripe_customer is 'Returns current user''s customer record from Stripe'; comment on function public.get_user_stripe_subscriptions is 'Returns current user''s subscriptions from Stripe'; comment on function public.get_stripe_products is 'Returns all active Stripe products (public access)'; comment on function public.get_stripe_prices is 'Returns all active Stripe prices (public access)'; comment on function public.is_paying_user is 'Returns true if user has an active or trialing subscription'; comment on function public.get_user_subscription_status is 'Returns current subscription details for a user'; comment on function public.get_user_stripe_customer_id is 'Returns the Stripe customer ID for a user'; comment on function public.update_profile_subscription_status is 'Trigger function to update profile fields when subscription changes';