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;