From 0abea08c4f41bff2fbf9aad1b64a6a6543ad48d6 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Mon, 3 Nov 2025 09:47:34 +0100 Subject: [PATCH] Add migrations --- sql/35_stripe_wrappers.sql | 360 +++++++++++++++++++++++ sql/36_fix_stripe_subscription_dates.sql | 290 ++++++++++++++++++ sql/37_secure_active_subscriptions.sql | 91 ++++++ sql/38_remove_function.sql | 3 + sql/39_grant_access_to_schema.sql | 7 + sql/cleanup_old_stripe_functions.sql | 68 +++++ 6 files changed, 819 insertions(+) create mode 100644 sql/35_stripe_wrappers.sql create mode 100644 sql/36_fix_stripe_subscription_dates.sql create mode 100644 sql/37_secure_active_subscriptions.sql create mode 100644 sql/38_remove_function.sql create mode 100644 sql/39_grant_access_to_schema.sql create mode 100644 sql/cleanup_old_stripe_functions.sql diff --git a/sql/35_stripe_wrappers.sql b/sql/35_stripe_wrappers.sql new file mode 100644 index 0000000..5476d15 --- /dev/null +++ b/sql/35_stripe_wrappers.sql @@ -0,0 +1,360 @@ +-- ============================================================================ +-- 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'; + diff --git a/sql/36_fix_stripe_subscription_dates.sql b/sql/36_fix_stripe_subscription_dates.sql new file mode 100644 index 0000000..aee0cc7 --- /dev/null +++ b/sql/36_fix_stripe_subscription_dates.sql @@ -0,0 +1,290 @@ +-- ============================================================================ +-- 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'; + + diff --git a/sql/37_secure_active_subscriptions.sql b/sql/37_secure_active_subscriptions.sql new file mode 100644 index 0000000..6d5ec1a --- /dev/null +++ b/sql/37_secure_active_subscriptions.sql @@ -0,0 +1,91 @@ +-- ============================================================================ +-- Secure Active Subscriptions Access +-- ============================================================================ +-- This migration fixes a security issue with the active_subscriptions view +-- which could expose all users' subscription data. +-- +-- Changes: +-- 1. Drop the public.active_subscriptions view (insecure) +-- 2. Create a secure function that only returns the current user's subscription +-- 3. Add RLS-compatible function for admin access if needed +-- ============================================================================ + +-- ============================================================================ +-- Drop the insecure view +-- ============================================================================ + +drop view if exists public.active_subscriptions; + +-- ============================================================================ +-- Create secure function to get current user's active subscription +-- ============================================================================ + +-- Function to get the authenticated user's active subscription +create or replace function public.get_my_active_subscription() +returns table ( + subscription_id text, + user_id uuid, + user_email text, + first_name text, + last_name text, + status text, + current_period_start timestamp with time zone, + current_period_end timestamp with time zone, + cancel_at_period_end boolean, + product_name text, + currency text, + unit_amount integer, + billing_interval text, + plan subscription_plan +) +language plpgsql +security definer +-- Set search path for security +set search_path = public, stripe +as $$ +begin + -- Only return data for the authenticated user + return query + 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 billing_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 (c.metadata->>'user_id')::uuid = auth.uid() -- Filter by authenticated user only! + and s.status::text in ('active', 'trialing') + and si.current_period_end is not null + and to_timestamp(si.current_period_end) > now() + order by si.current_period_end desc + limit 1; +end; +$$; + +-- ============================================================================ +-- Grant appropriate permissions +-- ============================================================================ + +-- Grant access to authenticated users for their own subscription +grant execute on function public.get_my_active_subscription() to authenticated; + +-- ============================================================================ +-- Comments for documentation +-- ============================================================================ + +comment on function public.get_my_active_subscription is + 'Returns the current authenticated user''s active subscription (secure, RLS-compliant)'; diff --git a/sql/38_remove_function.sql b/sql/38_remove_function.sql new file mode 100644 index 0000000..0012e52 --- /dev/null +++ b/sql/38_remove_function.sql @@ -0,0 +1,3 @@ +grant execute on function public.get_user_stripe_subscriptions() to authenticated; + +drop function if exists public.get_my_active_subscriptions(); diff --git a/sql/39_grant_access_to_schema.sql b/sql/39_grant_access_to_schema.sql new file mode 100644 index 0000000..ea50843 --- /dev/null +++ b/sql/39_grant_access_to_schema.sql @@ -0,0 +1,7 @@ +GRANT USAGE ON SCHEMA stripe TO service_role; +GRANT ALL ON ALL TABLES IN SCHEMA stripe TO service_role; +GRANT ALL ON ALL ROUTINES IN SCHEMA stripe TO service_role; +GRANT ALL ON ALL SEQUENCES IN SCHEMA stripe TO service_role; +ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA stripe GRANT ALL ON TABLES TO service_role; +ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA stripe GRANT ALL ON ROUTINES TO service_role; +ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA stripe GRANT ALL ON SEQUENCES TO service_role; \ No newline at end of file diff --git a/sql/cleanup_old_stripe_functions.sql b/sql/cleanup_old_stripe_functions.sql new file mode 100644 index 0000000..7cb5fdf --- /dev/null +++ b/sql/cleanup_old_stripe_functions.sql @@ -0,0 +1,68 @@ +-- ============================================================================ +-- Cleanup Script - Remove Old Custom Stripe Webhook Functions +-- ============================================================================ +-- This removes functions that were created by the old 36_stripe_webhooks.sql +-- They're no longer needed since we're using @supabase/stripe-sync-engine +-- ============================================================================ + +-- Drop customer event handler functions +drop function if exists public.handle_stripe_customer_created(text, text, uuid); +drop function if exists public.handle_stripe_customer_updated(text, text); +drop function if exists public.handle_stripe_customer_deleted(text); + +-- Drop product event handler functions +drop function if exists public.handle_stripe_product_upsert(text, text, text, boolean, text, jsonb); +drop function if exists public.handle_stripe_product_deleted(text); + +-- Drop price event handler functions +drop function if exists public.handle_stripe_price_upsert(text, text, boolean, text, bigint, text, integer, integer, jsonb); +drop function if exists public.handle_stripe_price_deleted(text); + +-- Drop subscription event handler functions +drop function if exists public.handle_stripe_subscription_upsert( + text, text, text, text, integer, boolean, + timestamp with time zone, timestamp with time zone, + timestamp with time zone, timestamp with time zone, timestamp with time zone +); +drop function if exists public.handle_stripe_subscription_deleted(text); + +-- ============================================================================ +-- Verify cleanup +-- ============================================================================ + +-- Check if any old functions remain +SELECT + routine_name, + routine_schema +FROM information_schema.routines +WHERE routine_schema = 'public' + AND routine_name LIKE 'handle_stripe_%' +ORDER BY routine_name; + +-- Should return 0 rows if cleanup was successful + +-- ============================================================================ +-- Keep these functions (they're still used) +-- ============================================================================ + +-- ✅ is_paying_user(uuid) - Used by frontend +-- ✅ get_user_subscription_status(uuid) - Used by frontend +-- ✅ get_user_stripe_customer_id(uuid) - Used by API +-- ✅ sync_subscription_user_id() - Trigger function for user_id sync +-- ✅ update_profile_subscription_status() - Trigger function for profile updates +-- ✅ update_updated_at_column() - Generic trigger function + +SELECT + routine_name, + routine_type +FROM information_schema.routines +WHERE routine_schema IN ('public', 'stripe') + AND ( + routine_name LIKE '%paying%' + OR routine_name LIKE '%subscription%user%' + OR routine_name LIKE '%profile%' + ) +ORDER BY routine_name; + +-- Should show the functions we want to keep +