Files
Jyotisha/frontend/supabase/migrations/20260831010000_feature_pricing.sql
T
2026-08-31 03:37:44 +08:00

208 lines
8.5 KiB
PL/PgSQL

begin;
create table if not exists public.feature_pricing (
id uuid primary key default gen_random_uuid(),
feature_key text not null check (feature_key in ('chat.standard', 'chat.premium', 'rectification', 'report.full', 'report.export', 'profile.extra')),
model_tier text not null check (model_tier in ('standard', 'premium', 'internal')),
credit_cost integer not null check (credit_cost > 0),
version integer not null check (version > 0),
status text not null default 'draft' check (status in ('draft', 'published', 'retired')),
enabled boolean not null default false,
effective_from timestamptz,
effective_to timestamptz,
created_by uuid references auth.users(id) on delete set null,
updated_by uuid references auth.users(id) on delete set null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (feature_key, model_tier, version),
check (effective_to is null or effective_from is null or effective_to > effective_from)
);
create unique index if not exists feature_pricing_one_draft_idx
on public.feature_pricing(feature_key, model_tier) where status = 'draft';
create index if not exists feature_pricing_active_idx
on public.feature_pricing(feature_key, model_tier, effective_from)
where status = 'published' and enabled;
create or replace function public.resolve_feature_pricing(
p_feature_key text,
p_model_id text
)
returns table(feature_key text, model_tier text, credit_cost integer, version integer)
language plpgsql security definer set search_path = ''
as $$
declare
v_model_tier text;
begin
if p_feature_key is null or p_feature_key not in ('chat.standard', 'chat.premium', 'rectification', 'report.full', 'report.export', 'profile.extra') then
raise exception 'feature_pricing_invalid_feature' using errcode = '22023';
end if;
if p_model_id is null or btrim(p_model_id) = '' then
raise exception 'feature_pricing_model_required' using errcode = '22023';
end if;
select v.model_tier into v_model_tier
from public.model_configs c
join public.model_config_versions v on v.config_id = c.id
where c.model_id = btrim(p_model_id)
and v.status = 'published'
and v.enabled
order by v.version desc
limit 1;
if v_model_tier is null then
raise exception 'feature_pricing_model_unavailable' using errcode = '22023';
end if;
return query
select p.feature_key, p.model_tier, p.credit_cost, p.version
from public.feature_pricing p
where p.feature_key = p_feature_key
and p.model_tier = v_model_tier
and p.status = 'published'
and p.enabled
and (p.effective_from is null or p.effective_from <= clock_timestamp())
and (p.effective_to is null or p.effective_to > clock_timestamp())
order by p.version desc
limit 1;
if not found then
raise exception 'feature_pricing_missing' using errcode = '22023';
end if;
end; $$;
create or replace function public.admin_save_feature_pricing_draft(
p_actor_user_id uuid,
p_pricing_id uuid,
p_feature_key text,
p_model_tier text,
p_credit_cost integer,
p_reason text,
p_request_id text
)
returns uuid
language plpgsql security definer set search_path = ''
as $$
declare
v_id uuid;
v_version integer;
begin
if not public.admin_has_permission(p_actor_user_id, 'billing.products.write') then
raise exception 'admin_permission_denied' using errcode = '42501';
end if;
if char_length(btrim(coalesce(p_reason, ''))) not between 1 and 500 then
raise exception 'admin_reason_required' using errcode = '22023';
end if;
if p_feature_key not in ('chat.standard', 'chat.premium', 'rectification', 'report.full', 'report.export', 'profile.extra')
or p_model_tier not in ('standard', 'premium', 'internal')
or p_credit_cost is null or p_credit_cost <= 0 then
raise exception 'feature_pricing_invalid' using errcode = '22023';
end if;
if p_pricing_id is null then
select coalesce(max(version), 0) + 1 into v_version
from public.feature_pricing
where feature_key = p_feature_key and model_tier = p_model_tier;
insert into public.feature_pricing(feature_key, model_tier, credit_cost, version, status, enabled, updated_by)
values (p_feature_key, p_model_tier, p_credit_cost, v_version, 'draft', false, p_actor_user_id)
returning id into v_id;
else
update public.feature_pricing
set credit_cost = p_credit_cost, updated_by = p_actor_user_id, updated_at = clock_timestamp()
where id = p_pricing_id and status = 'draft'
returning id into v_id;
if v_id is null then
raise exception 'feature_pricing_draft_not_found' using errcode = '22023';
end if;
end if;
insert into audit.admin_audit_logs(
actor_user_id, actor_email, actor_role, action, target_type, target_id,
after_value, request_id, permission_used, reason
)
select p_actor_user_id, lower(btrim(u.email)), 'admin', 'billing.feature_pricing.draft.save',
'feature_pricing', v_id,
jsonb_build_object('featureKey', p_feature_key, 'modelTier', p_model_tier, 'creditCost', p_credit_cost),
btrim(p_request_id), 'billing.products.write', btrim(p_reason)
from identity.users u where u.id = p_actor_user_id
on conflict do nothing;
return v_id;
end; $$;
create or replace function public.admin_publish_feature_pricing(
p_actor_user_id uuid,
p_pricing_id uuid,
p_reason text,
p_request_id text
)
returns uuid
language plpgsql security definer set search_path = ''
as $$
declare
v_draft public.feature_pricing%rowtype;
v_old public.feature_pricing%rowtype;
begin
if not public.admin_has_permission(p_actor_user_id, 'billing.products.publish') then
raise exception 'admin_permission_denied' using errcode = '42501';
end if;
if char_length(btrim(coalesce(p_reason, ''))) not between 1 and 500 then
raise exception 'admin_reason_required' using errcode = '22023';
end if;
select * into v_draft from public.feature_pricing where id = p_pricing_id and status = 'draft' for update;
if not found then raise exception 'feature_pricing_draft_not_found' using errcode = '22023'; end if;
select * into v_old
from public.feature_pricing
where feature_key = v_draft.feature_key and model_tier = v_draft.model_tier
and status = 'published'
order by version desc limit 1 for update;
if v_old.id is not null then
update public.feature_pricing
set status = 'retired', enabled = false, effective_to = clock_timestamp(), updated_by = p_actor_user_id, updated_at = clock_timestamp()
where id = v_old.id;
end if;
update public.feature_pricing
set status = 'published', enabled = true, effective_from = clock_timestamp(), effective_to = null,
updated_by = p_actor_user_id, updated_at = clock_timestamp()
where id = v_draft.id;
insert into audit.admin_audit_logs(
actor_user_id, actor_email, actor_role, action, target_type, target_id,
before_value, after_value, request_id, permission_used, reason
)
select p_actor_user_id, lower(btrim(u.email)), 'admin', 'billing.feature_pricing.publish',
'feature_pricing', v_draft.id,
case when v_old.id is null then '{}'::jsonb else jsonb_build_object('id', v_old.id, 'version', v_old.version, 'status', v_old.status) end,
jsonb_build_object('featureKey', v_draft.feature_key, 'modelTier', v_draft.model_tier, 'version', v_draft.version, 'creditCost', v_draft.credit_cost),
btrim(p_request_id), 'billing.products.publish', btrim(p_reason)
from identity.users u where u.id = p_actor_user_id
on conflict do nothing;
return v_draft.id;
end; $$;
alter table public.feature_pricing enable row level security;
revoke all on table public.feature_pricing from public, anon, authenticated;
grant all on table public.feature_pricing to service_role;
drop policy if exists feature_pricing_public_select on public.feature_pricing;
revoke all on function public.resolve_feature_pricing(text, text),
public.admin_save_feature_pricing_draft(uuid, uuid, text, text, integer, text, text),
public.admin_publish_feature_pricing(uuid, uuid, text, text)
from public, anon, authenticated;
grant execute on function public.resolve_feature_pricing(text, text) to service_role;
grant execute on function public.admin_save_feature_pricing_draft(uuid, uuid, text, text, integer, text, text),
public.admin_publish_feature_pricing(uuid, uuid, text, text) to service_role;
do $$ begin
if exists(select 1 from pg_roles where rolname = 'admin_runtime') then
grant select on table public.feature_pricing to admin_runtime;
grant execute on function public.admin_save_feature_pricing_draft(uuid, uuid, text, text, integer, text, text),
public.admin_publish_feature_pricing(uuid, uuid, text, text) to admin_runtime;
end if;
end $$;
commit;