freemium F1: enforcement de limite de pacientes (schema-per-tenant)
- therapist_free ganha max_patients=20 (clinic_free ja tinha 30) - trigger BEFORE INSERT em patients le plan_features.limits em runtime, resolve tenant por TG_TABLE_SCHEMA, plano ativo (clinica via tenant_id + pessoal via owner user_id), conta vivos (status<>Arquivado) e da RAISE PLAN_LIMIT_REACHED|patients|<n> - helpers tenant_active_plan_id / plan_feature_limit (globais, sobrevivem F6.3) - wiring: tenants novos ganham via trg_attach_business_triggers; 9 existentes backfill - testado: clinic_free bloqueia em 30, therapist_free em 20, PRO ilimitado (rollback) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
-- =============================================================================
|
||||
-- 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 <schema>.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|<limite>' 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;
|
||||
@@ -0,0 +1,34 @@
|
||||
-- =============================================================================
|
||||
-- Freemium F1 — limite de pacientes do plano therapist_free
|
||||
--
|
||||
-- clinic_free já traz max_patients=30 (em plan_features.limits da feature
|
||||
-- clinic_calendar, semeado). O therapist_free não tinha limite de pacientes.
|
||||
-- Pendura max_patients=20 na feature 'patients.manage' (a que o therapist_free
|
||||
-- já possui, enabled).
|
||||
--
|
||||
-- REGRA DE OURO: referenciar plano/feature POR KEY via subquery, nunca por uuid
|
||||
-- hardcoded (uuids divergem entre ambientes). Idempotente (merge no jsonb).
|
||||
-- O enforcement em runtime (trigger) está em manual/freemium_f1_plan_limits.
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
UPDATE public.plan_features pf
|
||||
SET limits = COALESCE(pf.limits, '{}'::jsonb) || jsonb_build_object('max_patients', 20)
|
||||
WHERE pf.plan_id = (SELECT id FROM public.plans WHERE key = 'therapist_free')
|
||||
AND pf.feature_id = (SELECT id FROM public.features WHERE key = 'patients.manage');
|
||||
|
||||
-- Sanidade: garante que o limite ficou gravado (1 linha afetada esperada).
|
||||
DO $$
|
||||
DECLARE v int;
|
||||
BEGIN
|
||||
SELECT (pf.limits->>'max_patients')::int INTO v
|
||||
FROM public.plan_features pf
|
||||
WHERE pf.plan_id = (SELECT id FROM public.plans WHERE key = 'therapist_free')
|
||||
AND pf.feature_id = (SELECT id FROM public.features WHERE key = 'patients.manage');
|
||||
IF v IS DISTINCT FROM 20 THEN
|
||||
RAISE EXCEPTION 'therapist_free max_patients esperado 20, obtido %', v;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user