Dashboard SaaS ganha seção de receita de créditos WhatsApp (Asaas)
Fecha o gap de analytics que faltava: MRR/ARR de assinatura já existia, mas não havia visão de receita dos créditos WhatsApp comprados via Asaas. Banco (migration 20260423000011) — 4 RPCs saas_admin only: - saas_wa_credits_revenue_stats(from, to): total arrecadado, count de compras, tenants únicos, créditos vendidos, ticket médio. - saas_wa_credits_top_packages(from, to): ranking top 10 pacotes por revenue, consolida nome atual se pacote foi renomeado. - saas_wa_credits_usage_summary(): snapshot atual de lifetime_purchased vs lifetime_used vs current_balance + taxa de consumo. - saas_wa_credits_revenue_evolution(from, to, bucket_days): série temporal pra sparkline. Todas com check is_saas_admin() no início + SECURITY DEFINER. Frontend: - useSaasCreditsAnalytics composable orquestra as 4 RPCs em paralelo com seleção de período (30d/90d/6m/12m) que ajusta bucket_days automaticamente. - SaasCreditsRevenueCard.vue: 4 KPIs (receita + ticket médio, compras + tenants, créditos vendidos, % consumo global), sparkline SVG com indicador de tendência, ranking top 5 pacotes. - Integrado no SaasDashboard logo antes da tabela "Distribuição por plano". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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).';
|
||||
Reference in New Issue
Block a user