-- ============================================================================= -- Freemium F1 — Enforcement de limite de plano (pacientes), schema-per-tenant -- -- ⚠️ APLICAR COMO supabase_admin (anexa triggers em tabelas tenant + a função de -- wiring trg_attach_business_triggers é owned por supabase_admin). -- -- Trigger genérico BEFORE INSERT em .patients que: -- 1. resolve o tenant pelo NOME DO SCHEMA (TG_TABLE_SCHEMA → tenant_schemas); -- 2. resolve o plano ATIVO do tenant em runtime (clínica via tenant_id; -- pessoal/terapeuta via owner user_id — as 6 subs pessoais têm tenant_id NULL); -- 3. lê o limite max_patients de plan_features.limits EM RUNTIME (mudar o número -- no painel passa a valer sem deploy); -- 4. conta pacientes vivos (status <> 'Arquivado') e dá RAISE parseável -- 'PLAN_LIMIT_REACHED|patients|' quando já atingiu. -- -- Sem plano ativo OU sem limite definido (planos PRO) ⇒ não bloqueia. -- Idempotente (CREATE OR REPLACE + DROP TRIGGER IF EXISTS). Tudo em public -- (subscriptions/plan_features/tenant_schemas são globais) ⇒ sobrevive ao DROP F6.3. -- ============================================================================= BEGIN; -- 1) Resolve o plano ativo de um tenant (clínica OU pessoal) ------------------ CREATE OR REPLACE FUNCTION public.tenant_active_plan_id(p_tenant_id uuid) RETURNS uuid LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp' AS $$ SELECT COALESCE( -- clínica: subscription chaveada por tenant_id (SELECT vas.plan_id FROM public.v_tenant_active_subscription vas WHERE vas.tenant_id = p_tenant_id), -- pessoal: subscription chaveada pelo owner (user_id), tenant_id NULL (SELECT s.plan_id FROM public.subscriptions s JOIN public.tenant_members tm ON tm.user_id = s.user_id AND tm.tenant_id = p_tenant_id AND tm.status = 'active' WHERE s.status = 'active' AND s.tenant_id IS NULL AND (s.current_period_end IS NULL OR s.current_period_end > now()) ORDER BY s.created_at DESC LIMIT 1) ); $$; ALTER FUNCTION public.tenant_active_plan_id(uuid) OWNER TO supabase_admin; -- 2) Lê um limite numérico do plano (busca a chave em qualquer feature) ------- -- Ex.: clinic_free guarda max_patients sob clinic_calendar; therapist_free -- sob patients.manage. Retorna o MIN (mais restritivo) se houver mais de um. CREATE OR REPLACE FUNCTION public.plan_feature_limit(p_plan_id uuid, p_limit_key text) RETURNS int LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp' AS $$ SELECT min((pf.limits->>p_limit_key)::int) FROM public.plan_features pf WHERE pf.plan_id = p_plan_id AND pf.enabled AND pf.limits ? p_limit_key AND (pf.limits->>p_limit_key) ~ '^[0-9]+$'; $$; ALTER FUNCTION public.plan_feature_limit(uuid, text) OWNER TO supabase_admin; -- 3) Trigger function de enforcement ----------------------------------------- CREATE OR REPLACE FUNCTION public.enforce_patient_plan_limit() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp' AS $$ DECLARE v_tenant uuid; v_plan uuid; v_limit int; v_count int; BEGIN SELECT tenant_id INTO v_tenant FROM public.tenant_schemas WHERE schema_name = TG_TABLE_SCHEMA; IF v_tenant IS NULL THEN RETURN NEW; END IF; -- schema não-tenant: ignora v_plan := public.tenant_active_plan_id(v_tenant); IF v_plan IS NULL THEN RETURN NEW; END IF; -- sem plano ativo: não bloqueia v_limit := public.plan_feature_limit(v_plan, 'max_patients'); IF v_limit IS NULL THEN RETURN NEW; END IF; -- plano sem limite (PRO): ilimitado EXECUTE format( 'SELECT count(*) FROM %I.patients WHERE status IS DISTINCT FROM %L', TG_TABLE_SCHEMA, 'Arquivado' ) INTO v_count; IF v_count >= v_limit THEN RAISE EXCEPTION 'PLAN_LIMIT_REACHED|patients|%', v_limit USING ERRCODE = 'P0001'; END IF; RETURN NEW; END $$; ALTER FUNCTION public.enforce_patient_plan_limit() OWNER TO supabase_admin; -- 4) Attach helper (pendura o trigger no patients de um schema) --------------- CREATE OR REPLACE FUNCTION public.attach_plan_limit_triggers(p_schema text) RETURNS int LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp' AS $$ BEGIN IF p_schema NOT LIKE 'tenant\_%' THEN RAISE EXCEPTION 'schema inválido %', p_schema; END IF; IF EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_schema = p_schema AND table_name = 'patients' ) THEN EXECUTE format('DROP TRIGGER IF EXISTS enforce_patient_plan_limit ON %I.patients', p_schema); EXECUTE format( 'CREATE TRIGGER enforce_patient_plan_limit BEFORE INSERT ON %I.patients ' 'FOR EACH ROW EXECUTE FUNCTION public.enforce_patient_plan_limit()', p_schema); RETURN 1; END IF; RETURN 0; END $$; ALTER FUNCTION public.attach_plan_limit_triggers(text) OWNER TO supabase_admin; GRANT EXECUTE ON FUNCTION public.attach_plan_limit_triggers(text) TO postgres, service_role; -- 5) Wiring: tenants NOVOS ganham o trigger de limite no clone ---------------- 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); PERFORM public.attach_plan_limit_triggers(NEW.schema_name); RETURN NULL; END $$; ALTER FUNCTION public.trg_attach_business_triggers() OWNER TO supabase_admin; -- 6) Backfill: os 9 schemas já existentes ganham o trigger agora ------------- DO $$ DECLARE r record; n int := 0; BEGIN FOR r IN SELECT schema_name FROM public.tenant_schemas LOOP n := n + public.attach_plan_limit_triggers(r.schema_name); END LOOP; RAISE NOTICE 'enforce_patient_plan_limit anexado em % schemas', n; END $$; COMMIT;