360 lines
10 KiB
PL/PgSQL
360 lines
10 KiB
PL/PgSQL
-- ============================================================================
|
|
-- 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';
|
|
|