From cb153165c32c661a8dd750d2e9dd3bcf10952002 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Sat, 13 Jun 2026 20:01:27 -0300 Subject: [PATCH] freemium F3b: /saas/usuarios (donos) + notify_all_devs - saas_list_account_owners(): 1 linha/tenant com dono (master), nome/slug/email/ plano + selo "novo" (24h). Dev-only (is_saas_admin), cast text p/ varchar. - notify_all_devs() insere em notifications_sistema p/ cada saas_admin - trigger AFTER INSERT/UPDATE OF status em subscriptions avisa os devs com deeplink /saas/usuarios - testado em ROLLBACK: lista + notify ao inserir subscription Co-Authored-By: Claude Opus 4.8 (1M context) --- ..._f3b_saas_owners_notify.supabase_admin.sql | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 database-novo/manual/freemium_f3b_saas_owners_notify.supabase_admin.sql diff --git a/database-novo/manual/freemium_f3b_saas_owners_notify.supabase_admin.sql b/database-novo/manual/freemium_f3b_saas_owners_notify.supabase_admin.sql new file mode 100644 index 0000000..f93905c --- /dev/null +++ b/database-novo/manual/freemium_f3b_saas_owners_notify.supabase_admin.sql @@ -0,0 +1,116 @@ +-- ============================================================================= +-- Freemium F3b — /saas/usuarios (donos por tenant) + notificação aos devs +-- +-- ⚠️ APLICAR COMO supabase_admin (lê auth.users.email + cria trigger em +-- public.subscriptions; notify_user_sistema é chamada por SECURITY DEFINER). +-- +-- • saas_list_account_owners(): 1 linha por tenant com o DONO (master), +-- nome/slug/e-mail/plano + selo "novo" (24h). Dev-only (is_saas_admin). +-- • notify_all_devs(): insere em notifications_sistema p/ cada saas_admin. +-- • trigger em subscriptions: avisa os devs quando nasce/muda uma assinatura, +-- com deeplink pra /saas/usuarios. +-- ============================================================================= + +BEGIN; + +-- 1) Donos por tenant (dev-only) --------------------------------------------- +CREATE OR REPLACE FUNCTION public.saas_list_account_owners() +RETURNS TABLE ( + tenant_id uuid, + slug text, + tenant_name text, + kind text, + owner_id uuid, + owner_name text, + owner_email text, + plan_key text, + created_at timestamptz, + is_new boolean +) +LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO 'public','pg_temp' +AS $$ +BEGIN + IF NOT public.is_saas_admin() THEN + RAISE EXCEPTION 'forbidden' USING ERRCODE = '42501'; + END IF; + + RETURN QUERY + SELECT t.id, t.slug::text, t.name::text, t.kind::text, + owner.user_id, pr.full_name::text, au.email::text, + COALESCE(vas.plan_key, ps.plan_key)::text, + t.created_at, + (t.created_at > now() - interval '24 hours') + FROM public.tenants t + LEFT JOIN LATERAL ( + SELECT tm.user_id + FROM public.tenant_members tm + WHERE tm.tenant_id = t.id AND tm.role = 'tenant_admin' AND tm.status = 'active' + ORDER BY tm.created_at ASC + LIMIT 1 + ) owner ON true + LEFT JOIN public.profiles pr ON pr.id = owner.user_id + LEFT JOIN auth.users au ON au.id = owner.user_id + LEFT JOIN public.v_tenant_active_subscription vas ON vas.tenant_id = t.id + LEFT JOIN LATERAL ( + SELECT s.plan_key FROM public.subscriptions s + WHERE s.user_id = owner.user_id AND s.status = 'active' AND s.tenant_id IS NULL + ORDER BY s.created_at DESC LIMIT 1 + ) ps ON true + ORDER BY t.created_at DESC; +END $$; +ALTER FUNCTION public.saas_list_account_owners() OWNER TO supabase_admin; +REVOKE ALL ON FUNCTION public.saas_list_account_owners() FROM PUBLIC; +GRANT EXECUTE ON FUNCTION public.saas_list_account_owners() TO authenticated, service_role; + +-- 2) notify_all_devs ---------------------------------------------------------- +CREATE OR REPLACE FUNCTION public.notify_all_devs( + p_type text, p_payload jsonb, p_ref_id uuid DEFAULT NULL, p_ref_table text DEFAULT NULL +) +RETURNS int LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp' +AS $$ +DECLARE r record; n int := 0; +BEGIN + FOR r IN SELECT user_id FROM public.saas_admins LOOP + PERFORM public.notify_user_sistema(r.user_id, p_type, p_payload, NULL, p_ref_id, p_ref_table); + n := n + 1; + END LOOP; + RETURN n; +END $$; +ALTER FUNCTION public.notify_all_devs(text, jsonb, uuid, text) OWNER TO supabase_admin; + +-- 3) trigger em subscriptions ------------------------------------------------- +CREATE OR REPLACE FUNCTION public.trg_notify_devs_subscription() +RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp' +AS $$ +DECLARE v_slug text; v_title text; +BEGIN + -- só em INSERT ou quando o status muda + IF TG_OP = 'UPDATE' AND NEW.status IS NOT DISTINCT FROM OLD.status THEN + RETURN NEW; + END IF; + + SELECT t.slug INTO v_slug FROM public.tenants t WHERE t.id = NEW.tenant_id; + + v_title := CASE WHEN TG_OP = 'INSERT' THEN 'Nova assinatura' ELSE 'Assinatura atualizada' END; + + PERFORM public.notify_all_devs( + 'subscription_' || lower(TG_OP), + jsonb_build_object( + 'title', v_title, + 'detail', NEW.plan_key || ' · ' || NEW.status || COALESCE(' · ' || v_slug, ''), + 'deeplink', '/saas/usuarios', + 'plan_key', NEW.plan_key, + 'status', NEW.status + ), + NEW.id, 'subscriptions' + ); + RETURN NEW; +END $$; +ALTER FUNCTION public.trg_notify_devs_subscription() OWNER TO supabase_admin; + +DROP TRIGGER IF EXISTS trg_subscriptions_notify_devs ON public.subscriptions; +CREATE TRIGGER trg_subscriptions_notify_devs + AFTER INSERT OR UPDATE OF status ON public.subscriptions + FOR EACH ROW EXECUTE FUNCTION public.trg_notify_devs_subscription(); + +COMMIT;