create or replace function app.uuid_array_from_jsonb(value jsonb) returns uuid[] language sql immutable as $$ select coalesce(array_agg(item::uuid), '{}'::uuid[]) from jsonb_array_elements_text( case when jsonb_typeof(value) = 'array' then value else '[]'::jsonb end ) as t(item) where item ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' $$; create or replace function app.text_array_from_jsonb(value jsonb) returns text[] language sql immutable as $$ select coalesce(array_agg(item), '{}'::text[]) from jsonb_array_elements_text( case when jsonb_typeof(value) = 'array' then value else '[]'::jsonb end ) as t(item) where length(item) between 1 and 160 $$; create or replace function app.public_question_bank_grant_allows( grant_allowed_region_ids uuid[], grant_allowed_subject_ids uuid[], source_region_id uuid, source_subject_ids uuid[] ) returns boolean language sql immutable as $$ select ( coalesce(array_length(grant_allowed_region_ids, 1), 0) = 0 or source_region_id = any(grant_allowed_region_ids) ) and ( coalesce(array_length(grant_allowed_subject_ids, 1), 0) = 0 or coalesce(array_length(source_subject_ids, 1), 0) = 0 or source_subject_ids <@ grant_allowed_subject_ids ) $$; create or replace function app.public_question_bank_subscription_allows( subscription_metadata jsonb, plan_feature_flags jsonb, source_question_bank_id uuid, source_region_id uuid, source_subject_ids uuid[] ) returns boolean language plpgsql immutable as $$ declare plan_cfg jsonb := coalesce(plan_feature_flags #> '{publicQuestionBanks}', '{}'::jsonb); subscription_cfg jsonb := coalesce(subscription_metadata #> '{publicQuestionBankAccess}', '{}'::jsonb); plan_region_ids uuid[] := app.uuid_array_from_jsonb(plan_cfg->'allowedRegionIds'); subscription_region_ids uuid[] := app.uuid_array_from_jsonb(subscription_cfg->'allowedRegionIds'); plan_subject_ids uuid[] := app.uuid_array_from_jsonb(plan_cfg->'allowedSubjectIds'); subscription_subject_ids uuid[] := app.uuid_array_from_jsonb(subscription_cfg->'allowedSubjectIds'); plan_bank_ids uuid[] := app.uuid_array_from_jsonb(plan_cfg->'allowedQuestionBankIds'); subscription_bank_ids uuid[] := app.uuid_array_from_jsonb(subscription_cfg->'allowedQuestionBankIds'); mode text := coalesce(subscription_cfg->>'mode', plan_cfg->>'mode', 'all'); requires_region_allowlist boolean := case coalesce(subscription_cfg->>'requiresRegionAllowlist', plan_cfg->>'requiresRegionAllowlist') when 'true' then true when 'false' then false else false end; allow_all_regions boolean := ( case coalesce(subscription_cfg->>'allowAllRegions', plan_cfg->>'allowAllRegions') when 'true' then true when 'false' then false else false end ) or mode in ('all', 'all_regions', 'national', 'unlimited'); begin if coalesce(subscription_cfg->>'enabled', plan_cfg->>'enabled', 'true') = 'false' then return false; end if; if coalesce(array_length(plan_bank_ids, 1), 0) > 0 and not source_question_bank_id = any(plan_bank_ids) then return false; end if; if coalesce(array_length(subscription_bank_ids, 1), 0) > 0 and not source_question_bank_id = any(subscription_bank_ids) then return false; end if; if not allow_all_regions and source_region_id is not null then if requires_region_allowlist and coalesce(array_length(plan_region_ids, 1), 0) = 0 and coalesce(array_length(subscription_region_ids, 1), 0) = 0 then return false; end if; if coalesce(array_length(plan_region_ids, 1), 0) > 0 and not source_region_id = any(plan_region_ids) then return false; end if; if coalesce(array_length(subscription_region_ids, 1), 0) > 0 and not source_region_id = any(subscription_region_ids) then return false; end if; end if; if coalesce(array_length(plan_subject_ids, 1), 0) > 0 and coalesce(array_length(source_subject_ids, 1), 0) > 0 and not source_subject_ids <@ plan_subject_ids then return false; end if; if coalesce(array_length(subscription_subject_ids, 1), 0) > 0 and coalesce(array_length(source_subject_ids, 1), 0) > 0 and not source_subject_ids <@ subscription_subject_ids then return false; end if; return true; end; $$; create index if not exists idx_question_bank_grants_allowed_regions on public.question_bank_grants using gin(allowed_region_ids); create index if not exists idx_question_bank_grants_allowed_subjects on public.question_bank_grants using gin(allowed_subject_ids); update public.platform_saas_plans set feature_flags = jsonb_set( feature_flags, '{publicQuestionBanks}', '{"enabled":true,"mode":"limited_regions","requiresRegionAllowlist":true,"maxRegionCount":1}'::jsonb, true ), updated_at = now() where code = 'starter_yearly'; update public.platform_saas_plans set feature_flags = jsonb_set( feature_flags, '{publicQuestionBanks}', '{"enabled":true,"mode":"national","allowAllRegions":true}'::jsonb, true ), updated_at = now() where code = 'pro_yearly'; drop policy if exists question_bank_grants_eligible_tenant_read on public.question_bank_grants; create policy question_bank_grants_eligible_tenant_read on public.question_bank_grants for select using ( status = 'active' and (starts_at is null or starts_at <= now()) and (expires_at is null or expires_at > now()) and exists ( select 1 from public.question_banks qb left join lateral ( select array_agg(distinct q.subject_id) filter (where q.subject_id is not null) as subject_ids from public.questions q where q.question_bank_id = qb.id and q.status = 'published' ) ss on true where qb.id = source_question_bank_id and qb.source_scope = 'platform' and qb.status = 'active' and app.public_question_bank_grant_allows( allowed_region_ids, allowed_subject_ids, qb.region_id, coalesce(ss.subject_ids, '{}'::uuid[]) ) and ( ( grant_scope = 'all_active_tenants' and exists ( select 1 from public.tenant_subscriptions ts left join public.platform_saas_plans p on p.code = ts.plan_code where ts.tenant_id = app.current_tenant_id() and ts.status in ('trial', 'active') and (ts.expires_at is null or ts.expires_at > now()) and app.public_question_bank_subscription_allows( ts.metadata, coalesce(p.feature_flags, '{}'::jsonb), qb.id, qb.region_id, coalesce(ss.subject_ids, '{}'::uuid[]) ) ) ) or ( grant_scope in ('plans', 'mixed') and exists ( select 1 from public.tenant_subscriptions ts left join public.platform_saas_plans p on p.code = ts.plan_code where ts.tenant_id = app.current_tenant_id() and ts.status in ('trial', 'active') and (ts.expires_at is null or ts.expires_at > now()) and ts.plan_code = any(allowed_plan_codes) and app.public_question_bank_subscription_allows( ts.metadata, coalesce(p.feature_flags, '{}'::jsonb), qb.id, qb.region_id, coalesce(ss.subject_ids, '{}'::uuid[]) ) ) ) or ( grant_scope in ('tenants', 'mixed') and app.current_tenant_id() = any(allowed_tenant_ids) and ( metadata->>'requiresActiveSubscription' = 'false' or exists ( select 1 from public.tenant_subscriptions ts left join public.platform_saas_plans p on p.code = ts.plan_code where ts.tenant_id = app.current_tenant_id() and ts.status in ('trial', 'active') and (ts.expires_at is null or ts.expires_at > now()) and app.public_question_bank_subscription_allows( ts.metadata, coalesce(p.feature_flags, '{}'::jsonb), qb.id, qb.region_id, coalesce(ss.subject_ids, '{}'::uuid[]) ) ) ) ) ) ) );