diff --git a/database-novo/manual/f6_2h_clone_wiring.supabase_admin.sql b/database-novo/manual/f6_2h_clone_wiring.supabase_admin.sql new file mode 100644 index 0000000..c464f1c --- /dev/null +++ b/database-novo/manual/f6_2h_clone_wiring.supabase_admin.sql @@ -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;