forked from wangziqi/gongxue-base
feat: scaffold supabase multi-tenant backend
This commit is contained in:
181
supabase/migrations/202606210005_platform_admin_billing.sql
Normal file
181
supabase/migrations/202606210005_platform_admin_billing.sql
Normal file
@@ -0,0 +1,181 @@
|
||||
create table if not exists public.platform_saas_plans (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
code text not null unique,
|
||||
name text not null,
|
||||
description text,
|
||||
billing_cycle text not null default 'yearly' check (billing_cycle in ('monthly', 'quarterly', 'yearly', 'one_time')),
|
||||
base_amount_cents integer not null default 0,
|
||||
currency text not null default 'CNY',
|
||||
included_quotas jsonb not null default '{}'::jsonb,
|
||||
overage_prices jsonb not null default '{}'::jsonb,
|
||||
feature_flags jsonb not null default '{}'::jsonb,
|
||||
status text not null default 'active' check (status in ('active', 'archived')),
|
||||
sort_order integer not null default 0,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table if not exists public.tenant_billing_profiles (
|
||||
tenant_id uuid primary key references public.tenants(id) on delete cascade,
|
||||
billing_name text,
|
||||
tax_id text,
|
||||
contact_name text,
|
||||
contact_phone text,
|
||||
contact_email citext,
|
||||
billing_address text,
|
||||
invoice_title text,
|
||||
invoice_type text check (invoice_type in ('none', 'normal_vat', 'special_vat')),
|
||||
bank_name text,
|
||||
bank_account_masked text,
|
||||
metadata jsonb not null default '{}'::jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table if not exists public.tenant_invoices (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
tenant_id uuid not null references public.tenants(id) on delete cascade,
|
||||
invoice_no text not null unique,
|
||||
invoice_type text not null default 'subscription' check (invoice_type in ('subscription', 'service_fee', 'usage_overage', 'manual_adjustment')),
|
||||
status text not null default 'draft' check (status in ('draft', 'issued', 'paid', 'void', 'overdue')),
|
||||
currency text not null default 'CNY',
|
||||
subtotal_cents integer not null default 0,
|
||||
discount_cents integer not null default 0,
|
||||
tax_cents integer not null default 0,
|
||||
total_cents integer not null default 0,
|
||||
paid_cents integer not null default 0,
|
||||
balance_cents integer not null default 0,
|
||||
billing_period_start date,
|
||||
billing_period_end date,
|
||||
due_date date,
|
||||
issued_at timestamptz,
|
||||
paid_at timestamptz,
|
||||
note text,
|
||||
metadata jsonb not null default '{}'::jsonb,
|
||||
created_by uuid references public.platform_users(id) on delete set null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table if not exists public.tenant_invoice_items (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
tenant_id uuid not null references public.tenants(id) on delete cascade,
|
||||
invoice_id uuid not null references public.tenant_invoices(id) on delete cascade,
|
||||
item_type text not null default 'subscription',
|
||||
item_ref_id uuid,
|
||||
description text not null,
|
||||
quantity numeric(12,2) not null default 1,
|
||||
unit_amount_cents integer not null default 0,
|
||||
amount_cents integer not null default 0,
|
||||
metadata jsonb not null default '{}'::jsonb,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table if not exists public.tenant_invoice_payments (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
tenant_id uuid not null references public.tenants(id) on delete cascade,
|
||||
invoice_id uuid not null references public.tenant_invoices(id) on delete cascade,
|
||||
payment_no text not null unique,
|
||||
provider text not null default 'manual',
|
||||
method text,
|
||||
status text not null default 'paid' check (status in ('pending', 'paid', 'failed', 'refunded')),
|
||||
amount_cents integer not null default 0,
|
||||
paid_at timestamptz,
|
||||
provider_trade_no text,
|
||||
received_by uuid references public.platform_users(id) on delete set null,
|
||||
raw_payload jsonb not null default '{}'::jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists idx_saas_plans_status on public.platform_saas_plans(status, sort_order);
|
||||
create index if not exists idx_tenant_subscriptions_tenant_status on public.tenant_subscriptions(tenant_id, status, expires_at desc);
|
||||
create index if not exists idx_tenant_usage_records_lookup on public.tenant_usage_records(tenant_id, metric_key, period_start, period_end);
|
||||
create index if not exists idx_tenant_invoices_tenant_status on public.tenant_invoices(tenant_id, status, due_date desc);
|
||||
create index if not exists idx_tenant_invoice_items_invoice on public.tenant_invoice_items(invoice_id);
|
||||
create index if not exists idx_tenant_invoice_payments_invoice on public.tenant_invoice_payments(invoice_id, status);
|
||||
|
||||
alter table public.platform_saas_plans enable row level security;
|
||||
alter table public.tenant_billing_profiles enable row level security;
|
||||
alter table public.tenant_invoices enable row level security;
|
||||
alter table public.tenant_invoice_items enable row level security;
|
||||
alter table public.tenant_invoice_payments enable row level security;
|
||||
|
||||
drop policy if exists platform_admin_saas_plans on public.platform_saas_plans;
|
||||
create policy platform_admin_saas_plans on public.platform_saas_plans
|
||||
for all
|
||||
using (app.is_platform_admin())
|
||||
with check (app.is_platform_admin());
|
||||
|
||||
do $$
|
||||
declare
|
||||
table_name text;
|
||||
begin
|
||||
foreach table_name in array array[
|
||||
'tenant_billing_profiles',
|
||||
'tenant_invoices',
|
||||
'tenant_invoice_items',
|
||||
'tenant_invoice_payments'
|
||||
]
|
||||
loop
|
||||
execute format('drop policy if exists tenant_isolation on public.%I', table_name);
|
||||
execute format(
|
||||
'create policy tenant_isolation on public.%I for all using (tenant_id = app.current_tenant_id() or app.is_platform_admin()) with check (tenant_id = app.current_tenant_id() or app.is_platform_admin())',
|
||||
table_name
|
||||
);
|
||||
end loop;
|
||||
end $$;
|
||||
|
||||
do $$
|
||||
declare
|
||||
table_name text;
|
||||
begin
|
||||
foreach table_name in array array[
|
||||
'platform_saas_plans',
|
||||
'tenant_billing_profiles',
|
||||
'tenant_invoices',
|
||||
'tenant_invoice_payments'
|
||||
]
|
||||
loop
|
||||
execute format('drop trigger if exists set_updated_at on public.%I', table_name);
|
||||
execute format('create trigger set_updated_at before update on public.%I for each row execute function app.touch_updated_at()', table_name);
|
||||
end loop;
|
||||
end $$;
|
||||
|
||||
insert into public.platform_saas_plans (
|
||||
code, name, description, billing_cycle, base_amount_cents,
|
||||
included_quotas, overage_prices, feature_flags, sort_order
|
||||
)
|
||||
values
|
||||
(
|
||||
'starter_yearly',
|
||||
'合作商基础版',
|
||||
'适合单地区题库合作商,含基础品牌与域名能力。',
|
||||
'yearly',
|
||||
980000,
|
||||
'{"students":1000,"questions":5000,"storageGb":20}'::jsonb,
|
||||
'{"studentsExtraPerYearCents":500,"storageGbPerYearCents":12000}'::jsonb,
|
||||
'{"customDomain":true,"tenantBranding":true,"paymentTenantCollect":false}'::jsonb,
|
||||
10
|
||||
),
|
||||
(
|
||||
'pro_yearly',
|
||||
'合作商专业版',
|
||||
'适合多地区、多课程运营,支持更多运营工具。',
|
||||
'yearly',
|
||||
1980000,
|
||||
'{"students":5000,"questions":30000,"storageGb":100}'::jsonb,
|
||||
'{"studentsExtraPerYearCents":300,"storageGbPerYearCents":9000}'::jsonb,
|
||||
'{"customDomain":true,"tenantBranding":true,"paymentTenantCollect":true,"crmWebhook":true}'::jsonb,
|
||||
20
|
||||
)
|
||||
on conflict (code)
|
||||
do update set name = excluded.name,
|
||||
description = excluded.description,
|
||||
billing_cycle = excluded.billing_cycle,
|
||||
base_amount_cents = excluded.base_amount_cents,
|
||||
included_quotas = excluded.included_quotas,
|
||||
overage_prices = excluded.overage_prices,
|
||||
feature_flags = excluded.feature_flags,
|
||||
sort_order = excluded.sort_order,
|
||||
updated_at = now();
|
||||
Reference in New Issue
Block a user