aiai / supabase /migrations /20240414162131_basejump-billing.sql
Mohammed Foud
first commit
a51a15b
/**
* -------------------------------------------------------
* Section - Billing
* -------------------------------------------------------
*/
/**
* Subscription Status
* Tracks the current status of the account subscription
*/
DO
$$
BEGIN
IF NOT EXISTS(SELECT 1
FROM pg_type t
JOIN pg_namespace n ON n.oid = t.typnamespace
WHERE t.typname = 'subscription_status'
AND n.nspname = 'basejump') THEN
create type basejump.subscription_status as enum (
'trialing',
'active',
'canceled',
'incomplete',
'incomplete_expired',
'past_due',
'unpaid'
);
end if;
end;
$$;
/**
* Billing customer
* This is a private table that contains a mapping of user IDs to your billing providers IDs
*/
create table if not exists basejump.billing_customers
(
-- UUID from auth.users
account_id uuid references basejump.accounts (id) on delete cascade not null,
-- The user's customer ID in Stripe. User must not be able to update this.
id text primary key,
-- The email address the customer wants to use for invoicing
email text,
-- The active status of a customer
active boolean,
-- The billing provider the customer is using
provider text
);
-- Open up access to billing_customers
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE basejump.billing_customers TO service_role;
GRANT SELECT ON TABLE basejump.billing_customers TO authenticated;
-- enable RLS for billing_customers
alter table
basejump.billing_customers
enable row level security;
/**
* Billing subscriptions
* This is a private table that contains a mapping of account IDs to your billing providers subscription IDs
*/
create table if not exists basejump.billing_subscriptions
(
-- Subscription ID from Stripe, e.g. sub_1234.
id text primary key,
account_id uuid references basejump.accounts (id) on delete cascade not null,
billing_customer_id text references basejump.billing_customers (id) on delete cascade not null,
-- The status of the subscription object, one of subscription_status type above.
status basejump.subscription_status,
-- Set of key-value pairs, used to store additional information about the object in a structured format.
metadata jsonb,
-- ID of the price that created this subscription.
price_id text,
plan_name text,
-- Quantity multiplied by the unit amount of the price creates the amount of the subscription. Can be used to charge multiple seats.
quantity integer,
-- If true the subscription has been canceled by the user and will be deleted at the end of the billing period.
cancel_at_period_end boolean,
-- Time at which the subscription was created.
created timestamp with time zone default timezone('utc' :: text, now()) not null,
-- Start of the current period that the subscription has been invoiced for.
current_period_start timestamp with time zone default timezone('utc' :: text, now()) not null,
-- End of the current period that the subscription has been invoiced for. At the end of this period, a new invoice will be created.
current_period_end timestamp with time zone default timezone('utc' :: text, now()) not null,
-- If the subscription has ended, the timestamp of the date the subscription ended.
ended_at timestamp with time zone default timezone('utc' :: text, now()),
-- A date in the future at which the subscription will automatically get canceled.
cancel_at timestamp with time zone default timezone('utc' :: text, now()),
-- If the subscription has been canceled, the date of that cancellation. If the subscription was canceled with `cancel_at_period_end`, `canceled_at` will still reflect the date of the initial cancellation request, not the end of the subscription period when the subscription is automatically moved to a canceled state.
canceled_at timestamp with time zone default timezone('utc' :: text, now()),
-- If the subscription has a trial, the beginning of that trial.
trial_start timestamp with time zone default timezone('utc' :: text, now()),
-- If the subscription has a trial, the end of that trial.
trial_end timestamp with time zone default timezone('utc' :: text, now()),
provider text
);
-- Open up access to billing_subscriptions
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE basejump.billing_subscriptions TO service_role;
GRANT SELECT ON TABLE basejump.billing_subscriptions TO authenticated;
-- enable RLS for billing_subscriptions
alter table
basejump.billing_subscriptions
enable row level security;
/**
* -------------------------
* Section - RLS Policies
* -------------------------
* This is where we define access to tables in the basejump schema
*/
create policy "Can only view own billing customer data." on basejump.billing_customers for
select
using (
basejump.has_role_on_account(account_id) = true
);
create policy "Can only view own billing subscription data." on basejump.billing_subscriptions for
select
using (
basejump.has_role_on_account(account_id) = true
);
/**
* -------------------------------------------------------
* Section - Public functions
* -------------------------------------------------------
* Each of these functions exists in the public name space because they are accessible
* via the API. it is the primary way developers can interact with Basejump accounts
*/
/**
* Returns the current billing status for an account
*/
CREATE OR REPLACE FUNCTION public.get_account_billing_status(account_id uuid)
RETURNS jsonb
security definer
set search_path = public, basejump
AS
$$
DECLARE
result jsonb;
role_result jsonb;
BEGIN
select public.current_user_account_role(get_account_billing_status.account_id) into role_result;
select jsonb_build_object(
'account_id', get_account_billing_status.account_id,
'billing_subscription_id', s.id,
'billing_enabled', case
when a.personal_account = true then config.enable_personal_account_billing
else config.enable_team_account_billing end,
'billing_status', s.status,
'billing_customer_id', c.id,
'billing_provider', config.billing_provider,
'billing_email',
coalesce(c.email, u.email) -- if we don't have a customer email, use the user's email as a fallback
)
into result
from basejump.accounts a
join auth.users u on u.id = a.primary_owner_user_id
left join basejump.billing_subscriptions s on s.account_id = a.id
left join basejump.billing_customers c on c.account_id = coalesce(s.account_id, a.id)
join basejump.config config on true
where a.id = get_account_billing_status.account_id
order by s.created desc
limit 1;
return result || role_result;
END;
$$ LANGUAGE plpgsql;
grant execute on function public.get_account_billing_status(uuid) to authenticated;
/**
* Allow service accounts to upsert the billing data for an account
*/
CREATE OR REPLACE FUNCTION public.service_role_upsert_customer_subscription(account_id uuid,
customer jsonb default null,
subscription jsonb default null)
RETURNS void AS
$$
BEGIN
-- if the customer is not null, upsert the data into billing_customers, only upsert fields that are present in the jsonb object
if customer is not null then
insert into basejump.billing_customers (id, account_id, email, provider)
values (customer ->> 'id', service_role_upsert_customer_subscription.account_id, customer ->> 'billing_email',
(customer ->> 'provider'))
on conflict (id) do update
set email = customer ->> 'billing_email';
end if;
-- if the subscription is not null, upsert the data into billing_subscriptions, only upsert fields that are present in the jsonb object
if subscription is not null then
insert into basejump.billing_subscriptions (id, account_id, billing_customer_id, status, metadata, price_id,
quantity, cancel_at_period_end, created, current_period_start,
current_period_end, ended_at, cancel_at, canceled_at, trial_start,
trial_end, plan_name, provider)
values (subscription ->> 'id', service_role_upsert_customer_subscription.account_id,
subscription ->> 'billing_customer_id', (subscription ->> 'status')::basejump.subscription_status,
subscription -> 'metadata',
subscription ->> 'price_id', (subscription ->> 'quantity')::int,
(subscription ->> 'cancel_at_period_end')::boolean,
(subscription ->> 'created')::timestamptz, (subscription ->> 'current_period_start')::timestamptz,
(subscription ->> 'current_period_end')::timestamptz, (subscription ->> 'ended_at')::timestamptz,
(subscription ->> 'cancel_at')::timestamptz,
(subscription ->> 'canceled_at')::timestamptz, (subscription ->> 'trial_start')::timestamptz,
(subscription ->> 'trial_end')::timestamptz,
subscription ->> 'plan_name', (subscription ->> 'provider'))
on conflict (id) do update
set billing_customer_id = subscription ->> 'billing_customer_id',
status = (subscription ->> 'status')::basejump.subscription_status,
metadata = subscription -> 'metadata',
price_id = subscription ->> 'price_id',
quantity = (subscription ->> 'quantity')::int,
cancel_at_period_end = (subscription ->> 'cancel_at_period_end')::boolean,
current_period_start = (subscription ->> 'current_period_start')::timestamptz,
current_period_end = (subscription ->> 'current_period_end')::timestamptz,
ended_at = (subscription ->> 'ended_at')::timestamptz,
cancel_at = (subscription ->> 'cancel_at')::timestamptz,
canceled_at = (subscription ->> 'canceled_at')::timestamptz,
trial_start = (subscription ->> 'trial_start')::timestamptz,
trial_end = (subscription ->> 'trial_end')::timestamptz,
plan_name = subscription ->> 'plan_name';
end if;
end;
$$ LANGUAGE plpgsql;
GRANT EXECUTE ON FUNCTION public.service_role_upsert_customer_subscription(uuid, jsonb, jsonb) TO service_role;