F6.2 wiring: tenants novos nascem com todos os triggers de negocio

- attach_agnostic_triggers reescrito SELF-CONTAINED (dirigido por colunas: set_
  updated_at em toda tabela com updated_at + prevent_* em patient_groups). Nao
  le mais de public -> sobrevive ao DROP da F6.3.
- trigger AFTER INSERT em tenant_schemas (trg_attach_business_triggers) dispara
  os 3 attach (agnostic + schema_aware + notif) pro schema novo. clone_tenant_
  template nao precisou ser tocado (ele insere em tenant_schemas). GRANT EXECUTE
  dos attach pra postgres/service_role.
- provision_account_tenant: clone ANTES do seed (seed_determined_commitments e
  no-op se schema nao existe; precisa do schema criado primeiro)

Smoke: tenant novo nasce com 84 triggers automaticamente (vs 81 nos existentes,
que sao mais que suficientes). Provision order corrigida.

App agora testavel: dados nos schemas (F6.1) + funcoes/triggers/RPCs roteiam
(F6.2). Falta so F6.3 DROP (com OK + app testado).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-06-13 16:25:58 -03:00
parent 218d342181
commit dc7826d0b5
@@ -0,0 +1,106 @@
-- =============================================================================
-- F6.2 (wiring) — tenants NOVOS nascem com todos os triggers de negócio
--
-- ⚠️ APLICAR COMO supabase_admin.
--
-- Até aqui os 9 schemas existentes ganharam os triggers via backfills (Lotes
-- A/B/C). Um tenant NOVO (clone_tenant_template) só ganhava channel-routing +
-- RLS. Este wiring:
-- 1. attach_agnostic_triggers → SELF-CONTAINED (dirigido por colunas, não lê
-- public; sobrevive ao DROP da F6.3).
-- 2. trigger AFTER INSERT em tenant_schemas dispara os 3 attach (agnostic +
-- schema_aware + notif) pro schema novo — clone_tenant_template não precisa
-- ser tocado (ele insere em tenant_schemas).
-- 3. provision_account_tenant: clone ANTES do seed (seed é no-op se o schema
-- não existe; precisa do schema criado primeiro).
-- =============================================================================
BEGIN;
-- 1) attach_agnostic_triggers self-contained ---------------------------------
CREATE OR REPLACE FUNCTION public.attach_agnostic_triggers(p_schema text)
RETURNS int LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
DECLARE r record; v_count int := 0;
BEGIN
IF p_schema NOT LIKE 'tenant\_%' THEN RAISE EXCEPTION 'schema inválido %', p_schema; END IF;
-- set_updated_at em toda tabela do schema que tem coluna updated_at
FOR r IN
SELECT c.relname AS tab
FROM pg_class c JOIN pg_attribute a ON a.attrelid = c.oid
WHERE c.relnamespace = p_schema::regnamespace AND c.relkind = 'r'
AND a.attname = 'updated_at' AND NOT a.attisdropped AND c.relname NOT LIKE '\_%'
LOOP
EXECUTE format('DROP TRIGGER IF EXISTS set_updated_at ON %I.%I', p_schema, r.tab);
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %I.%I FOR EACH ROW EXECUTE FUNCTION public.set_updated_at()', p_schema, r.tab);
v_count := v_count + 1;
END LOOP;
-- prevent_* em patient_groups
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = p_schema AND table_name = 'patient_groups') THEN
EXECUTE format('DROP TRIGGER IF EXISTS prevent_promoting_to_system ON %I.patient_groups', p_schema);
EXECUTE format('CREATE TRIGGER prevent_promoting_to_system BEFORE UPDATE ON %I.patient_groups FOR EACH ROW EXECUTE FUNCTION public.prevent_promoting_to_system()', p_schema);
EXECUTE format('DROP TRIGGER IF EXISTS prevent_system_group_changes ON %I.patient_groups', p_schema);
EXECUTE format('CREATE TRIGGER prevent_system_group_changes BEFORE UPDATE OR DELETE ON %I.patient_groups FOR EACH ROW EXECUTE FUNCTION public.prevent_system_group_changes()', p_schema);
v_count := v_count + 2;
END IF;
RETURN v_count;
END $$;
-- 2) trigger de wiring em tenant_schemas -------------------------------------
GRANT EXECUTE ON FUNCTION public.attach_agnostic_triggers(text) TO postgres, service_role;
GRANT EXECUTE ON FUNCTION public.attach_schema_aware_triggers(text) TO postgres, service_role;
GRANT EXECUTE ON FUNCTION public.attach_notif_triggers(text) TO postgres, service_role;
CREATE OR REPLACE FUNCTION public.trg_attach_business_triggers()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
AS $$
BEGIN
PERFORM public.attach_agnostic_triggers(NEW.schema_name);
PERFORM public.attach_schema_aware_triggers(NEW.schema_name);
PERFORM public.attach_notif_triggers(NEW.schema_name);
RETURN NULL;
END $$;
ALTER FUNCTION public.trg_attach_business_triggers() OWNER TO supabase_admin;
DROP TRIGGER IF EXISTS trg_tenant_schemas_attach ON public.tenant_schemas;
CREATE TRIGGER trg_tenant_schemas_attach
AFTER INSERT ON public.tenant_schemas
FOR EACH ROW EXECUTE FUNCTION public.trg_attach_business_triggers();
-- 3) provision_account_tenant: clone ANTES do seed ---------------------------
CREATE OR REPLACE FUNCTION public.provision_account_tenant(p_user_id uuid, p_kind text, p_name text DEFAULT NULL::text)
RETURNS uuid LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_tenant_id uuid;
v_account_type text;
v_name text;
BEGIN
IF p_kind NOT IN ('therapist', 'clinic_coworking', 'clinic_reception', 'clinic_full') THEN
RAISE EXCEPTION 'kind inválido: "%". Use: therapist, clinic_coworking, clinic_reception, clinic_full.', p_kind USING ERRCODE = 'P0001';
END IF;
v_account_type := CASE WHEN p_kind = 'therapist' THEN 'therapist' ELSE 'clinic' END;
IF EXISTS (
SELECT 1 FROM public.tenant_members tm JOIN public.tenants t ON t.id = tm.tenant_id
WHERE tm.user_id = p_user_id AND tm.role = 'tenant_admin' AND tm.status = 'active' AND t.kind = p_kind
) THEN
RAISE EXCEPTION 'Usuário já possui um tenant do tipo "%".', p_kind USING ERRCODE = 'P0001';
END IF;
v_name := COALESCE(NULLIF(TRIM(p_name), ''),
(SELECT COALESCE(NULLIF(TRIM(pr.full_name), ''), SPLIT_PART(au.email, '@', 1))
FROM public.profiles pr JOIN auth.users au ON au.id = pr.id WHERE pr.id = p_user_id),
'Conta');
INSERT INTO public.tenants (name, kind, created_at) VALUES (v_name, p_kind, now()) RETURNING id INTO v_tenant_id;
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
VALUES (v_tenant_id, p_user_id, 'tenant_admin', 'active', now());
UPDATE public.profiles SET account_type = v_account_type WHERE id = p_user_id;
-- F6 wiring: clone PRIMEIRO (cria o schema), seed DEPOIS (escreve no schema)
PERFORM public.clone_tenant_template(v_tenant_id);
PERFORM public.seed_determined_commitments(v_tenant_id);
RETURN v_tenant_id;
END $$;
COMMIT;