dc2363b4e1
DB (supabase_admin, manual/f6_4_saas_admin_rpcs.supabase_admin.sql): RPCs
gated por is_saas_admin que operam no _tenant_template + fan-out pros schemas:
- Feriados defaults: saas_list/add/remove_default_feriado (template + todos schemas)
- Notif templates defaults: saas_list/upsert/set_active/delete_default_notif_
template + saas_count_notif_template_overrides (so a copia-default owner_id NULL;
preserva overrides do tenant)
- saas_list_all_whatsapp_channels: fan-out cross-tenant (substitui a view
v_twilio_whatsapp_overview) com tenant_name + open_incident por canal
Frontend:
- SaasFeriadosPage / SaasNotificationTemplatesPage: supabase.from(tenant) ->
RPCs saas_*_default
- SaasWhatsappPage: gestao por-tenant-selecionado via supabase.schema(tenant_
<slug>) (RLS permite saas_admin); overview via saas_list_all_whatsapp_channels
- twilioWhatsappService.getAllChannels: v_twilio_whatsapp_overview -> RPC
Verificado: ZERO supabase.from('<tabela_tenant>') publico restante no FE
(so SaaS-admin estava pendente). Build passa. F6.3 agora desbloqueada.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
182 lines
9.8 KiB
PL/PgSQL
182 lines
9.8 KiB
PL/PgSQL
-- =============================================================================
|
|
-- 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.<tabela_tenant>).
|
|
--
|
|
-- ⚠️ 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;
|