-- ============================================================================= -- Migration: 20260419000003_delete_plan_safe -- V#36 — DELETE de plans sem checagem de assinaturas ativas pode quebrar tenants. -- -- Cria RPC delete_plan_safe(plan_id) que: -- - Valida saas_admin -- - Conta subscriptions ativas (status='active') no plano -- - Se houver, RAISE EXCEPTION descritivo com a contagem -- - Se OK, desativa prices ativos e deleta o plano (atomic) -- ============================================================================= CREATE OR REPLACE FUNCTION public.delete_plan_safe( p_plan_id uuid ) RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $function$ DECLARE v_active_count int; v_plan_key text; BEGIN IF auth.uid() IS NULL THEN RAISE EXCEPTION 'Não autenticado' USING ERRCODE = '28000'; END IF; IF NOT public.is_saas_admin() THEN RAISE EXCEPTION 'Apenas saas_admin pode deletar planos' USING ERRCODE = '42501'; END IF; IF p_plan_id IS NULL THEN RAISE EXCEPTION 'plan_id obrigatório' USING ERRCODE = '22023'; END IF; SELECT key INTO v_plan_key FROM public.plans WHERE id = p_plan_id; IF v_plan_key IS NULL THEN RAISE EXCEPTION 'plano não encontrado' USING ERRCODE = '22023'; END IF; SELECT COUNT(*) INTO v_active_count FROM public.subscriptions WHERE plan_id = p_plan_id AND status = 'active'; IF v_active_count > 0 THEN RAISE EXCEPTION 'Plano % tem % assinatura(s) ativa(s); migre os tenants antes de deletar.', v_plan_key, v_active_count USING ERRCODE = 'P0001'; END IF; -- desativa preços ativos antes de deletar UPDATE public.plan_prices SET is_active = false, active_to = now() WHERE plan_id = p_plan_id AND is_active = true; DELETE FROM public.plans WHERE id = p_plan_id; RETURN jsonb_build_object( 'deleted', true, 'plan_key', v_plan_key ); END; $function$; REVOKE ALL ON FUNCTION public.delete_plan_safe(uuid) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.delete_plan_safe(uuid) TO authenticated;