-- ============================================================================= -- F6.4 — RPCs SaaS-admin: defaults do sistema (template + fan-out) e views -- cross-tenant (fan-out). Destrava o F6.3 (páginas SaaS deixam de ler -- public.). -- -- ⚠️ APLICAR COMO supabase_admin. -- -- Defaults (feriados nacionais, notification_templates is_default): editados -- pelo SaaS no _tenant_template (fonte da verdade, propaga p/ tenants NOVOS no -- clone) E fan-out pros schemas EXISTENTES. Só toca cópias-default (owner_id -- NULL / is_default), preserva overrides do tenant (owner_id próprio). -- Todas gated por is_saas_admin(). -- ============================================================================= BEGIN; CREATE OR REPLACE FUNCTION public._assert_saas_admin() RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp' AS $$ BEGIN IF NOT public.is_saas_admin() THEN RAISE EXCEPTION 'Apenas SaaS admin' USING ERRCODE='42501'; END IF; END $$; -- ── FERIADOS (defaults nacionais) ─────────────────────────────────────────── CREATE OR REPLACE FUNCTION public.saas_list_default_feriados(p_ano integer) RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp' AS $$ DECLARE v jsonb; BEGIN PERFORM public._assert_saas_admin(); SELECT COALESCE(jsonb_agg(jsonb_build_object('id',id,'data',data,'nome',nome,'tipo',tipo,'bloqueia_sessoes',bloqueia_sessoes) ORDER BY data),'[]'::jsonb) INTO v FROM _tenant_template.feriados WHERE EXTRACT(YEAR FROM data) = p_ano; RETURN v; END $$; CREATE OR REPLACE FUNCTION public.saas_add_default_feriado(p_data date, p_nome text, p_tipo text DEFAULT 'municipal') RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp' AS $$ DECLARE t record; v_owner uuid := auth.uid(); BEGIN PERFORM public._assert_saas_admin(); INSERT INTO _tenant_template.feriados (owner_id, tipo, nome, data, bloqueia_sessoes) VALUES (v_owner, p_tipo, p_nome, p_data, false) ON CONFLICT (data, nome) DO NOTHING; FOR t IN SELECT schema_name FROM public.tenant_schemas LOOP EXECUTE format('INSERT INTO %I.feriados (owner_id, tipo, nome, data, bloqueia_sessoes) VALUES ($1,$2,$3,$4,false) ON CONFLICT (data, nome) DO NOTHING', t.schema_name) USING v_owner, p_tipo, p_nome, p_data; END LOOP; RETURN jsonb_build_object('data', p_data, 'nome', p_nome, 'tipo', p_tipo); END $$; CREATE OR REPLACE FUNCTION public.saas_remove_default_feriado(p_data date, p_nome text) RETURNS integer LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp' AS $$ DECLARE t record; v_n int := 0; BEGIN PERFORM public._assert_saas_admin(); DELETE FROM _tenant_template.feriados WHERE data = p_data AND nome = p_nome; FOR t IN SELECT schema_name FROM public.tenant_schemas LOOP EXECUTE format('DELETE FROM %I.feriados WHERE data = $1 AND nome = $2', t.schema_name) USING p_data, p_nome; v_n := v_n + 1; END LOOP; RETURN v_n; END $$; -- ── NOTIFICATION_TEMPLATES (defaults) ─────────────────────────────────────── CREATE OR REPLACE FUNCTION public.saas_list_default_notif_templates() RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp' AS $$ DECLARE v jsonb; BEGIN PERFORM public._assert_saas_admin(); SELECT COALESCE(jsonb_agg(to_jsonb(nt) ORDER BY nt.domain, nt.event_type),'[]'::jsonb) INTO v FROM _tenant_template.notification_templates nt WHERE nt.is_default = true AND nt.deleted_at IS NULL; RETURN v; END $$; -- upsert por key (defaults têm owner_id NULL). Cria/atualiza no template + schemas. CREATE OR REPLACE FUNCTION public.saas_upsert_default_notif_template(p_payload jsonb) RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp' AS $$ DECLARE t record; v_key text := p_payload->>'key'; v_exists boolean; v_domain text := p_payload->>'domain'; v_channel text := p_payload->>'channel'; v_event text := p_payload->>'event_type'; v_body text := p_payload->>'body_text'; v_vars jsonb := COALESCE(p_payload->'variables','[]'::jsonb); v_active boolean := COALESCE((p_payload->>'is_active')::boolean, true); BEGIN PERFORM public._assert_saas_admin(); IF v_key IS NULL THEN RAISE EXCEPTION 'key obrigatório'; END IF; -- template SELECT EXISTS(SELECT 1 FROM _tenant_template.notification_templates WHERE key=v_key AND owner_id IS NULL AND is_default=true) INTO v_exists; IF v_exists THEN UPDATE _tenant_template.notification_templates SET body_text=v_body, domain=v_domain, channel=v_channel, event_type=v_event, variables=v_vars, is_active=v_active WHERE key=v_key AND owner_id IS NULL AND is_default=true; ELSE INSERT INTO _tenant_template.notification_templates (owner_id, key, domain, channel, event_type, body_text, variables, is_default, is_active) VALUES (NULL, v_key, v_domain, v_channel, v_event, v_body, v_vars, true, v_active); END IF; -- fan-out schemas (só a cópia-default; preserva overrides do tenant owner_id<>NULL) FOR t IN SELECT schema_name FROM public.tenant_schemas LOOP EXECUTE format( 'INSERT INTO %I.notification_templates (owner_id, key, domain, channel, event_type, body_text, variables, is_default, is_active) ' || 'VALUES (NULL,$1,$2,$3,$4,$5,$6,true,$7) ' || 'ON CONFLICT (owner_id, key, deleted_at) DO UPDATE SET body_text=EXCLUDED.body_text, domain=EXCLUDED.domain, ' || 'channel=EXCLUDED.channel, event_type=EXCLUDED.event_type, variables=EXCLUDED.variables, is_active=EXCLUDED.is_active', t.schema_name) USING v_key, v_domain, v_channel, v_event, v_body, v_vars, v_active; END LOOP; END $$; CREATE OR REPLACE FUNCTION public.saas_set_default_notif_template_active(p_key text, p_active boolean) RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp' AS $$ DECLARE t record; BEGIN PERFORM public._assert_saas_admin(); UPDATE _tenant_template.notification_templates SET is_active=p_active WHERE key=p_key AND owner_id IS NULL AND is_default=true; FOR t IN SELECT schema_name FROM public.tenant_schemas LOOP EXECUTE format('UPDATE %I.notification_templates SET is_active=$1 WHERE key=$2 AND owner_id IS NULL AND is_default=true', t.schema_name) USING p_active, p_key; END LOOP; END $$; CREATE OR REPLACE FUNCTION public.saas_delete_default_notif_template(p_key text) RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp' AS $$ DECLARE t record; BEGIN PERFORM public._assert_saas_admin(); UPDATE _tenant_template.notification_templates SET deleted_at=now() WHERE key=p_key AND owner_id IS NULL AND is_default=true; FOR t IN SELECT schema_name FROM public.tenant_schemas LOOP EXECUTE format('UPDATE %I.notification_templates SET deleted_at=now() WHERE key=$1 AND owner_id IS NULL AND is_default=true', t.schema_name) USING p_key; END LOOP; END $$; -- quantos tenants têm override (tenant_id<>NULL no modelo antigo = owner_id<>NULL aqui) por key CREATE OR REPLACE FUNCTION public.saas_count_notif_template_overrides(p_key text) RETURNS integer LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp' AS $$ DECLARE t record; v_n int := 0; v_has int; BEGIN PERFORM public._assert_saas_admin(); FOR t IN SELECT schema_name FROM public.tenant_schemas LOOP EXECUTE format('SELECT count(*) FROM %I.notification_templates WHERE key=$1 AND owner_id IS NOT NULL AND is_active=true AND deleted_at IS NULL', t.schema_name) INTO v_has USING p_key; v_n := v_n + v_has; END LOOP; RETURN v_n; END $$; -- ── WHATSAPP admin (cross-tenant) — substitui v_twilio_whatsapp_overview ───── CREATE OR REPLACE FUNCTION public.saas_list_all_whatsapp_channels() RETURNS jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp' AS $$ DECLARE t record; v_rows jsonb := '[]'::jsonb; v_part jsonb; BEGIN PERFORM public._assert_saas_admin(); FOR t IN SELECT ts.tenant_id, ts.schema_name, tn.name AS tenant_name FROM public.tenant_schemas ts JOIN public.tenants tn ON tn.id = ts.tenant_id LOOP EXECUTE format( 'SELECT COALESCE(jsonb_agg(jsonb_build_object(' || '''id'',nc.id, ''tenant_id'',$1::uuid, ''tenant_name'',$2::text, ''owner_id'',nc.owner_id,' || '''provider'',nc.provider, ''is_active'',nc.is_active, ''connection_status'',nc.connection_status,' || '''sender_address'',nc.sender_address, ''twilio_phone_number'',nc.twilio_phone_number,' || '''last_health_check'',nc.last_health_check,' || '''open_incident'',(SELECT i.kind FROM %1$I.whatsapp_connection_incidents i WHERE i.channel_id=nc.id AND i.resolved_at IS NULL LIMIT 1)' || ')),''[]''::jsonb) FROM %1$I.notification_channels nc WHERE nc.channel=''whatsapp'' AND nc.deleted_at IS NULL', t.schema_name) INTO v_part USING t.tenant_id, t.tenant_name; v_rows := v_rows || v_part; END LOOP; RETURN v_rows; END $$; -- grants: gated por is_saas_admin internamente, mas exposto a authenticated DO $g$ DECLARE fn text; BEGIN FOREACH fn IN ARRAY ARRAY[ 'saas_list_default_feriados(integer)','saas_add_default_feriado(date,text,text)','saas_remove_default_feriado(date,text)', 'saas_list_default_notif_templates()','saas_upsert_default_notif_template(jsonb)', 'saas_set_default_notif_template_active(text,boolean)','saas_delete_default_notif_template(text)', 'saas_count_notif_template_overrides(text)','saas_list_all_whatsapp_channels()' ] LOOP EXECUTE format('GRANT EXECUTE ON FUNCTION public.%s TO authenticated', fn); END LOOP; END $g$; COMMIT;