Spaces:
Running
Running
/** | |
* ------------------------------------------------------- | |
* 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; |