diff --git a/database-novo/migrations/20260423000011_saas_whatsapp_credits_analytics.sql b/database-novo/migrations/20260423000011_saas_whatsapp_credits_analytics.sql
new file mode 100644
index 0000000..5effafb
--- /dev/null
+++ b/database-novo/migrations/20260423000011_saas_whatsapp_credits_analytics.sql
@@ -0,0 +1,209 @@
+-- ==========================================================================
+-- Agencia PSI — Migracao: Analytics SaaS de receita WhatsApp (Grupo 5)
+-- ==========================================================================
+-- 4 funcoes RPC pra popular cards novos no SaasDashboard:
+-- - saas_wa_credits_revenue_stats(from, to) → agregados globais
+-- - saas_wa_credits_top_packages(from, to) → ranking de pacotes
+-- - saas_wa_credits_usage_summary(from, to) → vendidos vs usados
+-- - saas_wa_credits_revenue_evolution(from,to,bucket) → serie temporal
+--
+-- Fonte: whatsapp_credit_purchases (paid) + whatsapp_credits_balance/
+-- whatsapp_credits_transactions.
+--
+-- Apenas saas_admin pode chamar (RLS da policy + SECURITY DEFINER check).
+-- ==========================================================================
+
+-- ---------------------------------------------------------------------------
+-- saas_wa_credits_revenue_stats: totais do periodo
+-- ---------------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION public.saas_wa_credits_revenue_stats(
+ p_from TIMESTAMPTZ DEFAULT (now() - interval '30 days'),
+ p_to TIMESTAMPTZ DEFAULT now()
+)
+RETURNS TABLE (
+ revenue_brl NUMERIC,
+ purchases_count INT,
+ tenants_count INT,
+ credits_sold INT,
+ avg_ticket_brl NUMERIC
+)
+LANGUAGE plpgsql
+STABLE
+SECURITY DEFINER
+SET search_path = public
+AS $$
+BEGIN
+ IF NOT public.is_saas_admin() THEN
+ RAISE EXCEPTION 'permission_denied';
+ END IF;
+
+ RETURN QUERY
+ SELECT
+ COALESCE(SUM(p.amount_brl), 0)::NUMERIC AS revenue_brl,
+ COUNT(*)::INT AS purchases_count,
+ COUNT(DISTINCT p.tenant_id)::INT AS tenants_count,
+ COALESCE(SUM(p.credits), 0)::INT AS credits_sold,
+ CASE WHEN COUNT(*) = 0 THEN 0
+ ELSE ROUND(COALESCE(AVG(p.amount_brl), 0), 2)
+ END AS avg_ticket_brl
+ FROM public.whatsapp_credit_purchases p
+ WHERE p.status = 'paid'
+ AND p.paid_at >= p_from
+ AND p.paid_at <= p_to;
+END;
+$$;
+
+REVOKE ALL ON FUNCTION public.saas_wa_credits_revenue_stats(TIMESTAMPTZ, TIMESTAMPTZ) FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION public.saas_wa_credits_revenue_stats(TIMESTAMPTZ, TIMESTAMPTZ) TO authenticated, service_role;
+
+
+-- ---------------------------------------------------------------------------
+-- saas_wa_credits_top_packages: ranking dos pacotes mais vendidos
+-- ---------------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION public.saas_wa_credits_top_packages(
+ p_from TIMESTAMPTZ DEFAULT (now() - interval '30 days'),
+ p_to TIMESTAMPTZ DEFAULT now()
+)
+RETURNS TABLE (
+ package_id UUID,
+ package_name TEXT,
+ purchases_count INT,
+ revenue_brl NUMERIC,
+ credits_sold INT
+)
+LANGUAGE plpgsql
+STABLE
+SECURITY DEFINER
+SET search_path = public
+AS $$
+BEGIN
+ IF NOT public.is_saas_admin() THEN
+ RAISE EXCEPTION 'permission_denied';
+ END IF;
+
+ RETURN QUERY
+ SELECT
+ p.package_id,
+ -- Nome snapshot do momento da compra; se tem package_id, usa o nome
+ -- atual pra consolidar pacotes renomeados
+ COALESCE(
+ (SELECT pk.name FROM public.whatsapp_credit_packages pk WHERE pk.id = p.package_id),
+ p.package_name
+ ) AS package_name,
+ COUNT(*)::INT AS purchases_count,
+ SUM(p.amount_brl)::NUMERIC AS revenue_brl,
+ SUM(p.credits)::INT AS credits_sold
+ FROM public.whatsapp_credit_purchases p
+ WHERE p.status = 'paid'
+ AND p.paid_at >= p_from
+ AND p.paid_at <= p_to
+ GROUP BY p.package_id, p.package_name
+ ORDER BY revenue_brl DESC
+ LIMIT 10;
+END;
+$$;
+
+REVOKE ALL ON FUNCTION public.saas_wa_credits_top_packages(TIMESTAMPTZ, TIMESTAMPTZ) FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION public.saas_wa_credits_top_packages(TIMESTAMPTZ, TIMESTAMPTZ) TO authenticated, service_role;
+
+
+-- ---------------------------------------------------------------------------
+-- saas_wa_credits_usage_summary: vendidos vs usados (snapshot atual)
+-- ---------------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION public.saas_wa_credits_usage_summary()
+RETURNS TABLE (
+ lifetime_purchased INT,
+ lifetime_used INT,
+ current_balance INT,
+ usage_rate NUMERIC,
+ tenants_with_balance INT
+)
+LANGUAGE plpgsql
+STABLE
+SECURITY DEFINER
+SET search_path = public
+AS $$
+BEGIN
+ IF NOT public.is_saas_admin() THEN
+ RAISE EXCEPTION 'permission_denied';
+ END IF;
+
+ RETURN QUERY
+ SELECT
+ COALESCE(SUM(lifetime_purchased), 0)::INT AS lifetime_purchased,
+ COALESCE(SUM(lifetime_used), 0)::INT AS lifetime_used,
+ COALESCE(SUM(balance), 0)::INT AS current_balance,
+ CASE WHEN COALESCE(SUM(lifetime_purchased), 0) = 0 THEN 0
+ ELSE ROUND(100.0 * COALESCE(SUM(lifetime_used), 0) / SUM(lifetime_purchased), 1)
+ END AS usage_rate,
+ COUNT(*)::INT AS tenants_with_balance
+ FROM public.whatsapp_credits_balance;
+END;
+$$;
+
+REVOKE ALL ON FUNCTION public.saas_wa_credits_usage_summary() FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION public.saas_wa_credits_usage_summary() TO authenticated, service_role;
+
+
+-- ---------------------------------------------------------------------------
+-- saas_wa_credits_revenue_evolution: serie temporal
+-- ---------------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION public.saas_wa_credits_revenue_evolution(
+ p_from TIMESTAMPTZ DEFAULT (now() - interval '30 days'),
+ p_to TIMESTAMPTZ DEFAULT now(),
+ p_bucket_days INT DEFAULT 7
+)
+RETURNS TABLE (
+ bucket_start TIMESTAMPTZ,
+ purchases_count INT,
+ revenue_brl NUMERIC
+)
+LANGUAGE plpgsql
+STABLE
+SECURITY DEFINER
+SET search_path = public
+AS $$
+BEGIN
+ IF NOT public.is_saas_admin() THEN
+ RAISE EXCEPTION 'permission_denied';
+ END IF;
+
+ RETURN QUERY
+ WITH purchases AS (
+ SELECT p.paid_at, p.amount_brl
+ FROM public.whatsapp_credit_purchases p
+ WHERE p.status = 'paid'
+ AND p.paid_at >= p_from
+ AND p.paid_at <= p_to
+ ),
+ bucketed AS (
+ SELECT
+ p_from + (
+ FLOOR(EXTRACT(EPOCH FROM (paid_at - p_from)) / (p_bucket_days * 86400))::INT
+ * p_bucket_days * interval '1 day'
+ ) AS bucket_start,
+ amount_brl
+ FROM purchases
+ )
+ SELECT
+ bucket_start,
+ COUNT(*)::INT AS purchases_count,
+ SUM(amount_brl)::NUMERIC AS revenue_brl
+ FROM bucketed
+ GROUP BY bucket_start
+ ORDER BY bucket_start;
+END;
+$$;
+
+REVOKE ALL ON FUNCTION public.saas_wa_credits_revenue_evolution(TIMESTAMPTZ, TIMESTAMPTZ, INT) FROM PUBLIC;
+GRANT EXECUTE ON FUNCTION public.saas_wa_credits_revenue_evolution(TIMESTAMPTZ, TIMESTAMPTZ, INT) TO authenticated, service_role;
+
+
+COMMENT ON FUNCTION public.saas_wa_credits_revenue_stats(TIMESTAMPTZ, TIMESTAMPTZ) IS
+ 'Totais de receita WhatsApp creditos no periodo (saas_admin).';
+COMMENT ON FUNCTION public.saas_wa_credits_top_packages(TIMESTAMPTZ, TIMESTAMPTZ) IS
+ 'Ranking pacotes mais vendidos no periodo (saas_admin).';
+COMMENT ON FUNCTION public.saas_wa_credits_usage_summary() IS
+ 'Snapshot atual: vendidos lifetime vs usados vs saldo (saas_admin).';
+COMMENT ON FUNCTION public.saas_wa_credits_revenue_evolution(TIMESTAMPTZ, TIMESTAMPTZ, INT) IS
+ 'Serie temporal de receita em buckets de N dias (saas_admin).';
diff --git a/src/components/dashboard/SaasCreditsRevenueCard.vue b/src/components/dashboard/SaasCreditsRevenueCard.vue
new file mode 100644
index 0000000..24a0da3
--- /dev/null
+++ b/src/components/dashboard/SaasCreditsRevenueCard.vue
@@ -0,0 +1,153 @@
+
+
+
+
+