feat: add billing foundation and entitlement enforcement
- add workspace-scoped billing storage, usage tracking, and add-on catalog - enforce plan entitlements for search and deep research routes - expand pricing and account UI around billing state, usage, and upgrades
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
create table if not exists public.workspace_billing_accounts (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
workspace_id uuid not null references public.workspaces (id) on delete cascade,
|
||||
plan_code text,
|
||||
billing_interval text check (billing_interval in ('monthly', 'annual', 'custom')),
|
||||
status text not null check (status in ('not_configured', 'inactive', 'active', 'past_due', 'canceled')),
|
||||
current_period_starts_at timestamptz,
|
||||
current_period_ends_at timestamptz,
|
||||
cancel_at_period_end boolean not null default false,
|
||||
canceled_at timestamptz,
|
||||
trial_ends_at timestamptz,
|
||||
external_customer_ref text,
|
||||
external_subscription_ref text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
constraint workspace_billing_accounts_workspace_id_key unique (workspace_id),
|
||||
constraint workspace_billing_accounts_period_bounds_check check (
|
||||
(current_period_starts_at is null and current_period_ends_at is null)
|
||||
or (current_period_starts_at is not null and current_period_ends_at is not null)
|
||||
)
|
||||
);
|
||||
|
||||
create table if not exists public.workspace_usage_periods (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
workspace_id uuid not null references public.workspaces (id) on delete cascade,
|
||||
billing_account_id uuid not null references public.workspace_billing_accounts (id) on delete cascade,
|
||||
period_starts_at timestamptz not null,
|
||||
period_ends_at timestamptz not null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
constraint workspace_usage_periods_workspace_period_key unique (workspace_id, period_starts_at, period_ends_at),
|
||||
constraint workspace_usage_periods_bounds_check check (period_starts_at < period_ends_at)
|
||||
);
|
||||
|
||||
create table if not exists public.workspace_usage_counters (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
usage_period_id uuid not null references public.workspace_usage_periods (id) on delete cascade,
|
||||
workspace_id uuid not null references public.workspaces (id) on delete cascade,
|
||||
resource text not null check (resource in ('research_credits', 'exports', 'enrichments', 'api_requests')),
|
||||
consumed_quantity integer not null default 0 check (consumed_quantity >= 0),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
constraint workspace_usage_counters_period_resource_key unique (usage_period_id, resource)
|
||||
);
|
||||
|
||||
create table if not exists public.workspace_addon_purchases (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
workspace_id uuid not null references public.workspaces (id) on delete cascade,
|
||||
addon_code text not null check (addon_code in ('export_pack_10k', 'export_pack_50k', 'enrichment_pack_1k', 'ai_assistant_monthly', 'white_label_monthly')),
|
||||
resource text not null check (resource in ('research_credits', 'exports', 'enrichments', 'api_requests')),
|
||||
purchased_quantity integer not null check (purchased_quantity >= 0),
|
||||
remaining_quantity integer not null check (remaining_quantity >= 0),
|
||||
purchased_at timestamptz not null default now(),
|
||||
expires_at timestamptz,
|
||||
external_purchase_ref text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table if not exists public.workspace_addon_balances (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
workspace_id uuid not null references public.workspaces (id) on delete cascade,
|
||||
addon_code text not null check (addon_code in ('export_pack_10k', 'export_pack_50k', 'enrichment_pack_1k', 'ai_assistant_monthly', 'white_label_monthly')),
|
||||
resource text not null check (resource in ('research_credits', 'exports', 'enrichments', 'api_requests')),
|
||||
remaining_quantity integer not null default 0 check (remaining_quantity >= 0),
|
||||
expires_at timestamptz,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
constraint workspace_addon_balances_workspace_addon_resource_key unique (workspace_id, addon_code, resource)
|
||||
);
|
||||
|
||||
create index if not exists workspace_billing_accounts_status_idx on public.workspace_billing_accounts (status);
|
||||
create index if not exists workspace_billing_accounts_plan_code_idx on public.workspace_billing_accounts (plan_code);
|
||||
create index if not exists workspace_usage_periods_workspace_id_idx on public.workspace_usage_periods (workspace_id);
|
||||
create index if not exists workspace_usage_counters_workspace_id_idx on public.workspace_usage_counters (workspace_id);
|
||||
create index if not exists workspace_addon_purchases_workspace_id_idx on public.workspace_addon_purchases (workspace_id);
|
||||
create index if not exists workspace_addon_balances_workspace_id_idx on public.workspace_addon_balances (workspace_id);
|
||||
|
||||
drop trigger if exists set_workspace_billing_accounts_updated_at on public.workspace_billing_accounts;
|
||||
create trigger set_workspace_billing_accounts_updated_at
|
||||
before update on public.workspace_billing_accounts
|
||||
for each row
|
||||
execute function public.set_updated_at();
|
||||
|
||||
drop trigger if exists set_workspace_usage_periods_updated_at on public.workspace_usage_periods;
|
||||
create trigger set_workspace_usage_periods_updated_at
|
||||
before update on public.workspace_usage_periods
|
||||
for each row
|
||||
execute function public.set_updated_at();
|
||||
|
||||
drop trigger if exists set_workspace_usage_counters_updated_at on public.workspace_usage_counters;
|
||||
create trigger set_workspace_usage_counters_updated_at
|
||||
before update on public.workspace_usage_counters
|
||||
for each row
|
||||
execute function public.set_updated_at();
|
||||
|
||||
drop trigger if exists set_workspace_addon_purchases_updated_at on public.workspace_addon_purchases;
|
||||
create trigger set_workspace_addon_purchases_updated_at
|
||||
before update on public.workspace_addon_purchases
|
||||
for each row
|
||||
execute function public.set_updated_at();
|
||||
|
||||
drop trigger if exists set_workspace_addon_balances_updated_at on public.workspace_addon_balances;
|
||||
create trigger set_workspace_addon_balances_updated_at
|
||||
before update on public.workspace_addon_balances
|
||||
for each row
|
||||
execute function public.set_updated_at();
|
||||
|
||||
insert into public.workspace_billing_accounts (
|
||||
workspace_id,
|
||||
plan_code,
|
||||
billing_interval,
|
||||
status,
|
||||
current_period_starts_at,
|
||||
current_period_ends_at
|
||||
)
|
||||
select w.id, 'starter_monthly', 'monthly', 'active', now(), now() + interval '1 month'
|
||||
from public.workspaces w
|
||||
where not exists (
|
||||
select 1
|
||||
from public.workspace_billing_accounts billing
|
||||
where billing.workspace_id = w.id
|
||||
);
|
||||
|
||||
update public.workspace_billing_accounts
|
||||
set
|
||||
plan_code = 'starter_monthly',
|
||||
billing_interval = 'monthly',
|
||||
status = 'active',
|
||||
current_period_starts_at = coalesce(current_period_starts_at, now()),
|
||||
current_period_ends_at = coalesce(current_period_ends_at, now() + interval '1 month')
|
||||
where plan_code is null and status = 'not_configured';
|
||||
Reference in New Issue
Block a user