Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Implement billing_historicals table #1163

Merged
merged 8 commits into from
Aug 29, 2023
230 changes: 230 additions & 0 deletions supabase/migrations/24_billing_historicals.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@

-- Always use a transaction, y'all.
begin;

-- Historical record of tenant billing statements. This structure
-- comes from the return value of `billing_report_202308`.
create table billing_historicals (
billed_prefix catalog_tenant not null,
jshearer marked this conversation as resolved.
Show resolved Hide resolved
billed_month timestamptz not null,
report jsonb not null,

check (date_trunc('month', billed_month) = billed_month),
unique (billed_prefix, billed_month)
);
alter table billing_historicals enable row level security;
grant all on billing_historicals to postgres;
jshearer marked this conversation as resolved.
Show resolved Hide resolved

create policy "Users must be authorized to their catalog tenant"
on billing_historicals as permissive for select
using (exists(
select 1 from auth_roles('admin') r where billed_prefix ^@ r.role_prefix
));
grant select on billing_historicals to authenticated;

-- Calculate the specified month's billing report for every tenant
-- and save those reports to billing_historicals.
create function internal.freeze_billing_month(billed_month timestamptz)
returns integer as $$
declare
tenant_row record;
tenant_count integer = 0;
begin
for tenant_row in select tenant as tenant_name from tenants loop
tenant_count = tenant_count + 1;
insert into billing_historicals
select
tenant_row.tenant_name as billed_prefix,
jshearer marked this conversation as resolved.
Show resolved Hide resolved
date_trunc('month', billed_month) as billed_month,
report
from billing_report_202308(tenant_row.tenant_name, date_trunc('month', billed_month)) as report;
jshearer marked this conversation as resolved.
Show resolved Hide resolved
end loop;
return tenant_count;
end
$$ language plpgsql volatile;

comment on table billing_historicals is
'Historical billing statements frozen from `billing_report_202308()`.';
comment on column billing_historicals.billed_prefix is
'The tenant for this statement';
comment on column billing_historicals.billed_month is
'The month for this statement';
comment on column billing_historicals.report is
'The report generated by billing_report_202308()';
jshearer marked this conversation as resolved.
Show resolved Hide resolved


-- Billing report which is effective August 2023.
create or replace function billing_report_202308(billed_prefix catalog_prefix, billed_month timestamptz)
returns jsonb as $$
#variable_conflict use_variable
declare
-- Auth checks
has_admin_grant boolean;
has_bypassrls boolean;
-- Retrieved from tenants table.
data_tiers integer[];
usage_tiers integer[];
recurring_usd_cents integer;

-- Calculating tiered usage.
tier_rate integer;
tier_pivot integer;
tier_count numeric;
remainder numeric;

-- Calculating adjustments.
adjustment internal.billing_adjustments;

-- Aggregated outputs.
line_items jsonb = '[]';
processed_data_gb numeric;
subtotal_usd_cents integer;
task_usage_hours numeric;
begin

-- Ensure `billed_month` is the truncated start of the billed month.
billed_month = date_trunc('month', billed_month);

-- Verify that the user has an admin grant for the requested `billed_prefix`.
perform 1 from auth_roles('admin') as r where billed_prefix ^@ r.role_prefix;
has_admin_grant = found;

-- Check whether user has bypassrls flag
perform 1 from pg_roles where rolname = session_user and rolbypassrls = true;
has_bypassrls = found;

if not has_bypassrls and not has_admin_grant then
-- errcode 28000 causes PostgREST to return an HTTP 403
-- see: https://www.postgresql.org/docs/current/errcodes-appendix.html
-- and: https://postgrest.org/en/stable/errors.html#status-codes
raise 'You are not authorized for the billed prefix %', billed_prefix using errcode = 28000;
end if;

-- Fetch data & usage tiers for `billed_prefix`'s tenant.
select into data_tiers, usage_tiers
t.data_tiers, t.usage_tiers
from tenants t
where billed_prefix ^@ t.tenant
;
-- Reveal contract costs only when the computing tenant-level billing.
select into recurring_usd_cents t.recurring_usd_cents
from tenants t
where billed_prefix = t.tenant
;

-- Determine the total amount of data processing and task usage
-- under `billed_prefix` in the given `billed_month`.
select into processed_data_gb, task_usage_hours
sum(bytes_written_by_me + bytes_read_by_me) / (1024.0 * 1024 * 1024),
sum(usage_seconds) / (60.0 * 60)
from catalog_stats
where catalog_name ^@ billed_prefix
and grain = 'monthly'
and ts = billed_month
;

-- Apply a recurring service cost, if defined.
if recurring_usd_cents != 0 then
line_items = line_items || jsonb_build_object(
'description', 'Recurring service charge',
'count', 1,
'rate', recurring_usd_cents,
'subtotal', recurring_usd_cents
);
end if;

-- Apply each of the data processing tiers.
remainder = processed_data_gb;

for idx in 1..array_length(data_tiers, 1) by 2 loop
tier_rate = data_tiers[idx];
tier_pivot = data_tiers[idx+1];
tier_count = least(remainder, tier_pivot);
remainder = remainder - tier_count;

line_items = line_items || jsonb_build_object(
'description', format(
case
when tier_pivot is null then 'Data processing (at %2$s/GB)'
when idx = 1 then 'Data processing (first %sGB at %s/GB)'
else 'Data processing (next %sGB at %s/GB)'
end,
tier_pivot,
(tier_rate / 100.0)::money
),
'count', tier_count,
'rate', tier_rate,
'subtotal', round(tier_count * tier_rate)
);
end loop;

-- Apply each of the task usage tiers.
remainder = task_usage_hours;

for idx in 1..array_length(usage_tiers, 1) by 2 loop
tier_rate = usage_tiers[idx];
tier_pivot = usage_tiers[idx+1];
tier_count = least(remainder, tier_pivot);
remainder = remainder - tier_count;

line_items = line_items || jsonb_build_object(
'description', format(
case
when tier_pivot is null then 'Task usage (at %2$s/hour)'
when idx = 1 then 'Task usage (first %s hours at %s/hour)'
else 'Task usage (next %s hours at %s/hour)'
end,
tier_pivot,
(tier_rate / 100.0)::money
),
'count', tier_count,
'rate', tier_rate,
'subtotal', round(tier_count * tier_rate)
);
end loop;

-- Apply any billing adjustments.
for adjustment in select * from internal.billing_adjustments a
where a.billed_month = billed_month and a.tenant = billed_prefix
loop
line_items = line_items || jsonb_build_object(
'description', adjustment.detail,
'count', 1,
'rate', adjustment.usd_cents,
'subtotal', adjustment.usd_cents
);
end loop;

-- Roll up the final subtotal.
select into subtotal_usd_cents sum((l->>'subtotal')::numeric)
from jsonb_array_elements(line_items) l;

return jsonb_build_object(
'billed_month', billed_month,
'billed_prefix', billed_prefix,
'line_items', line_items,
'processed_data_gb', processed_data_gb,
'recurring_fee', coalesce(recurring_usd_cents, 0),
'subtotal', subtotal_usd_cents,
'task_usage_hours', task_usage_hours
);

end
$$ language plpgsql volatile security definer;

-- The following enables the regularly scheduled function that creates
-- billing_historical for every tenant at the end of every month.
-- If you want to enable it locally, then just uncomment this
-- or run it manually. More often, it's more convenient during local
-- development to manually trigger this by calling
-- internal.freeze_billing_month() whenever you want to trigger it.

-- create extension pg_cron with schema extensions;
-- select cron.schedule (
-- 'month-end billing', -- name of the cron job
-- '0 0 0 2 * ? *', -- run on the second day of every month
-- $$ select internal.freeze_billing_month(date_trunc('month', current_date - interval '1 month')) $$
-- );

commit;