Skip to content

API de Dashboard Financeiro

Endpoints para o painel administrativo financeiro. Fornece KPIs, series temporais para graficos, lista de inadimplentes, analise de churn e exportacao Excel.

Prefixo: /api/v1/admin/finance Autenticacao: Bearer token (JWT) + user_type='admin'


Endpoints

Metodo Endpoint Descricao
GET /admin/finance/overview KPIs consolidados (MRR, ARR, inadimplentes, churn)
GET /admin/finance/revenue Serie temporal de receita (diario/mensal)
GET /admin/finance/payments Dinheiro recebido por dia/mes (razao de pagamentos)
GET /admin/finance/overdue Lista de clientes inadimplentes
GET /admin/finance/churn Analise de churn mensal
GET /admin/finance/export Exportar dados financeiros em Excel (.xlsx)

Filtros de periodo (ADR-0128)

Todas as rotas de agregacao aceitam recorte por data e devolvem, no corpo, o recorte que de fato aplicaram.

Parametro Tipo Onde vale Sobre qual data
start / end YYYY-MM-DD overview, revenue, payments, churn, overdue, ltv ver tabela abaixo
gateway asaas | stripe | pagarme | pagseguro overview, revenue, payments, churn, overdue —
payment_method credit_card | boleto | pix overview, revenue, payments, churn, overdue —

start/end sao dias de Brasilia (ADR-0137): start=2026-10-22 comeca as 00:00 BRT (03:00 UTC) e end=2026-10-22 vai ate 23:59:59 BRT (02:59:59 UTC do dia 23). end e inclusivo. O cliente manda o dia; a API converte. Instante com hora (...T03:00:00Z) e group_by=hour nao sao aceitos: o recorte por dia de Brasilia ja e o que eles reconstruiriam (pedido de 23/10, respondido com o caso 77 x 72 na secao 2c).

Rota start/end recortam por
overview os campos de movimento (ver secao 1)
revenue o periodo da serie
payments, payments/entries a primeira data em que o gateway confirmou o pagamento
churn o intervalo; vence months quando os dois vem juntos
overdue next_billing_date (data de vencimento)
ltv created_at do cliente (safra de aquisicao)

Param desconhecido devolve 422

Ate a ADR-0128, param de query nao declarado era descartado em silencio: a rota respondia 200 e o painel nao tinha como distinguir isso de filtro aplicado. Agora cada rota declara o que sabe aplicar, e o resto e 422:

GET /api/v1/admin/finance/overview?foo=1   ->  422 VALIDATION_ERROR

applied_filters

overview, revenue, churn, overdue e ltv devolvem o recorte resolvido — numa rota com janela padrao, ele vem preenchido, nao null:

{
  "applied_filters": {
    "start": "2026-09-01",
    "end": "2026-09-12",
    "gateway": null,
    "payment_method": null
  }
}

O ltv nao aceita gateway/payment_method: o LTV e por cliente, que pode ter trocado de gateway ao longo da vida — aceitar o parametro seria descarta-lo.

null so quando nao existe resposta (ADR-0129)

Nenhum campo vem null por dificuldade de calculo. Sob filtro, todos respondem:

Campo Como responde ao recorte
overview.total_customers clientes distintos nas assinaturas do recorte (sem filtro, total de customers)
overview.payment_success_rate recorta por payload.payment.billingType da cobranca
churn.by_reason.admin_cancelled conta as assinaturas apontadas pelo log de auditoria
churn.by_reason.customer_cancelled total de canceladas menos admin e chargeback

Os motivos de by_reason particionam o total sob qualquer filtro.

Quando nao ha resposta, payment_success_rate sai da resposta — nao vem null (ADR-0130). Sao dois casos:

  • nenhum desfecho de cobranca na janela;
  • gateway=stripe combinado com payment_method: os eventos invoice.* nao guardam o metodo da cobranca, e o cus_... deles nao casa com nenhum stripe_customer_id nosso — o dado nao existe em lugar nenhum.

Distinga ausente de zero: body.get('payment_success_rate') ausente significa "nao apuravel", nunca "0%".

Taxa de sucesso = desfecho da cobranca, por vencimento (ADR-0136)

payment_success_rate (Asaas) = das cobrancas que venceram na janela, pagas / (pagas + vencidas em aberto).

  • Conta cobrancas, nao eventos: estado de cada payload.payment.id no log.
  • Paga = teve PAYMENT_CONFIRMED ou PAYMENT_RECEIVED (a estornada tambem foi paga). A que venceu e depois foi paga conta so como sucesso.
  • Fora: cobranca apagada sem pagamento (a Asaas nao a lista) e pendente.
  • Stripe: invoice.paid / (invoice.paid + invoice.payment_failed) na janela.

Ate 14/09/2026 a apagada contava como falha: 52,5% no painel x 66,8% na Asaas.

-------|---------------| | overview.payment_success_rate | payment_method no filtro | | overview.total_customers | gateway ou payment_method no filtro | | churn.by_reason.admin_cancelled | gateway ou payment_method no filtro | | churn.by_reason.customer_cancelled | idem (deriva do anterior) |

Trate null como "nao apuravel neste recorte", nao como zero.


1. GET /admin/finance/overview

KPIs financeiros consolidados. Dados prontos para cards de dashboard.

GET /api/v1/admin/finance/overview?start=2026-09-01&end=2026-09-12
Authorization: Bearer <admin_token>

Query Parameters

Parametro Tipo Default Descricao
start YYYY-MM-DD 30 dias atras Inicio da janela de movimento
end YYYY-MM-DD hoje Fim da janela (inclusivo)
gateway string — asaas, stripe, pagarme ou pagseguro (legado, 1 registro)
payment_method string — credit_card, boleto ou pix

Duas naturezas de campo convivem aqui, e o painel precisa rotular a diferenca:

  • Saldo (mrr, arr, active_subscriptions, suspended_subscriptions, by_plan, by_payment_method, total_customers, average_ticket, overdue_count): estado da base agora. start/end nao se aplicam; gateway/payment_method sim.
  • Movimento (churn_rate, gross_revenue, net_revenue, refunded_in_period, chargebacks_in_period, payment_success_rate): seguem a janela pedida. Dinheiro (gross_revenue, net_revenue, refunded_in_period) vem do razao de pagamentos (ADR-0132) — o mesmo da secao 2b.
  • Janela no nome (churn_rate_30d, net_revenue_30d): sempre 30 dias, qualquer que seja o filtro. Campo que declara periodo no nome nao devolve outro.
  • Acumulado (total_refunded, total_chargebacks): historico inteiro, do mesmo razao de pagamentos (inclui estorno de ingresso).

Response (200 OK)

{
  "mrr": 384070,
  "mrr_stripe": 246760,
  "mrr_asaas": 137310,
  "mrr_pagarme": 0,
  "arr": 4608840,
  "total_customers": 13797,
  "active_subscriptions": 193,
  "overdue_count": 34,
  "churn_rate_30d": 4.0,
  "total_refunded": 0,
  "total_chargebacks": 0,
  "net_revenue_30d": 384070,
  "average_ticket": 1990,
  "payment_success_rate": 19.7,
  "by_payment_method": {
    "credit_card": { "count": 112, "mrr": 222880 },
    "pix": { "count": 81, "mrr": 161190 }
  },
  "by_plan": {
    "plan-basic": { "count": 193, "mrr": 384070, "name": "Plano Mensal Basico" }
  }
}

Campos

Campo Tipo Descricao
mrr int Monthly Recurring Revenue (centavos)
mrr_stripe int MRR somente Stripe (centavos)
mrr_asaas int MRR somente Asaas (centavos)
mrr_pagarme int MRR somente Pagar.me (centavos). Só assinatura de cartão conta: PIX do Pagar.me é pedido avulso, sem contrato que renove
arr int Annual Recurring Revenue = MRR * 12 (centavos)
total_customers int Sem filtro, total de customers. Sob filtro, clientes distintos no recorte
active_subscriptions int Pagantes (ADR-0135): Asaas = assinatura viva, contrato nao encerrado e cobranca vigente (a mais recente ja vencida ou paga) paga sem chargeback; Stripe = acesso efetivo. Contrato conta uma vez. Ate 14/09/2026 era status=active. Lista linha a linha, com o motivo de quem nao paga: /finance/payers (secao 2d)
suspended_subscriptions int Assinaturas com status=suspended agora, qualquer motivo, inclusive sem cobranca aberta. = suspended_with_open_debt + suspended_without_open_debt (ADR-0173). Ate 23/10/2026 a descricao dizia "3+ vencidas", o que valia para 642 de 2.703
suspended_with_open_debt int Suspensas com cobranca vencida em aberto (estao em /finance/overdue). Pagar reativa
suspended_without_open_debt int Suspensas sem cobranca aberta: em geral o contrato ja foi encerrado no gateway (ADR-0116)
overdue_count int Mesmo conjunto de /finance/overdue sem busca nem janela (ADR-0134); so assinatura viva (ADR-0173)
churn_rate_30d float Taxa de churn ultimos 30 dias (%)
total_refunded int Estornos, acumulado historico (centavos), do razao. Ate 14/09/2026 somava subscriptions.total_refunded, que perdia estorno de ingresso
total_chargebacks int Chargebacks, acumulado historico (quantidade), do razao
net_revenue_30d int Recebido - estornado - chargeback nos ultimos 30 dias (centavos). Ate 14/09/2026 era MRR - estornos
average_ticket int Ticket medio = MRR / pagantes (centavos)
payment_success_rate float (pode faltar) Das cobrancas que venceram na janela, pagas / (pagas + vencidas em aberto). Ausente quando nao ha o que medir
by_payment_method dict Breakdown por metodo (credit_card, pix, boleto)
by_plan dict Breakdown por plano (count, mrr, name)
churn_rate float Taxa de churn na janela pedida (%)
gross_revenue int Pagamentos recebidos na janela (centavos)
net_revenue int Recebido - estornado - chargeback na janela (centavos). Ate 14/09/2026 era MRR - estornos — um saldo que nao mudava com o mes
refunded_in_period int Estornos na janela, pela data do estorno (centavos)
chargebacks_in_period int Chargebacks na janela (quantidade), do razao — a mesma fonte que net_revenue desconta
applied_filters object Recorte efetivamente aplicado

Uso no frontend (cards)

// Converter centavos para reais
String formatBRL(int cents) => 'R\$ ${(cents / 100).toStringAsFixed(2)}';

// Cards sugeridos:
// - MRR: formatBRL(overview.mrr)
// - ARR: formatBRL(overview.arr)
// - Assinaturas ativas: overview.active_subscriptions
// - Inadimplentes: overview.overdue_count (vermelho se > 0)
// - Churn 30d: "${overview.churn_rate_30d}%"
// - Ticket medio: formatBRL(overview.average_ticket)

2. GET /admin/finance/revenue

Serie temporal de receita. Dados formatados para graficos de linha/barra.

Semantica mudou em 14/09/2026 (ADR-0132)

gross, refunds, chargebacks, net, stripe e asaas sao fluxo — dinheiro que entrou/saiu no periodo, do razao de pagamentos — com o mesmo significado em daily e monthly: somar os dias da o mes. Antes, as duas granularidades devolviam o MRR das assinaturas vivas (no diario ele se repetia a cada dia; somar setembro dava 12x o mes). Esse saldo continua disponivel em active_mrr, que nao se soma.

GET /api/v1/admin/finance/revenue?period=monthly&start=2026-01&end=2026-03
Authorization: Bearer <admin_token>

Query Parameters

Parametro Tipo Default Descricao
period string monthly monthly ou daily
start string 6 meses atras Inicio (YYYY-MM mensal, YYYY-MM-DD diario)
end string hoje Fim (mesmo formato)
gateway string — asaas, stripe, pagarme ou pagseguro (legado, 1 registro)
payment_method string — credit_card, boleto ou pix

Response (200 OK)

{
  "period": "monthly",
  "data": [
    {
      "date": "2026-01",
      "gross": 79600,
      "refunds": 0,
      "chargebacks": 0,
      "net": 79600,
      "new_subscriptions": 7,
      "cancellations": 0,
      "stripe": 45000,
      "asaas": 34600,
      "pagarme": 0,
      "active_mrr": 81590
    },
    {
      "date": "2026-02",
      "gross": 246760,
      "refunds": 0,
      "chargebacks": 0,
      "net": 246760,
      "new_subscriptions": 85,
      "cancellations": 3,
      "stripe": 150000,
      "asaas": 96760
    },
    {
      "date": "2026-03",
      "gross": 218900,
      "refunds": 0,
      "chargebacks": 0,
      "net": 218900,
      "new_subscriptions": 0,
      "cancellations": 0,
      "stripe": 130000,
      "asaas": 88900
    }
  ],
  "totals": {
    "gross": 545260,
    "refunds": 0,
    "chargebacks": 0,
    "net": 545260,
    "new_subscriptions": 92,
    "cancellations": 3
  }
}

Uso no frontend (graficos)

// Grafico de linha: Receita mensal
// X: data[].date
// Y1: data[].gross (dinheiro recebido no periodo)
// Y2: data[].net (recebido - estornos - chargebacks)
// Nao somar data[].active_mrr: e saldo, nao fluxo

// Grafico de barras empilhadas: Stripe vs Asaas
// X: data[].date
// Stack 1: data[].stripe
// Stack 2: data[].asaas

// Grafico de linha: Novas vs Cancelamentos
// X: data[].date
// Y1: data[].new_subscriptions (verde)
// Y2: data[].cancellations (vermelho)

2b. GET /admin/finance/payments

Dinheiro movimentado por dia ou mes: pagamentos, estornos e chargebacks (ADR-0132). E a resposta para "quanto entrou de dinheiro neste periodo".

GET /api/v1/admin/finance/payments?start=2026-09-01&end=2026-09-13&group_by=day
Authorization: Bearer <admin_token>

Query Parameters

Parametro Tipo Default Descricao
start YYYY-MM-DD 30 dias atras Inicio (inclusivo)
end YYYY-MM-DD hoje Fim (inclusivo)
group_by string day day ou month
gateway string — asaas, stripe, pagarme ou pagseguro
payment_method string — credit_card, boleto ou pix

Response (200 OK)

{
  "group_by": "day",
  "coverage_start": "2026-03-20",
  "data": [
    {
      "date": "2026-09-13",
      "paid_cents": 107460,
      "count": 54,
      "refunded_cents": 0,
      "refund_count": 0,
      "chargeback_cents": 0,
      "chargeback_count": 0,
      "net_cents": 107460,
      "by_method": { "pix": { "paid_cents": 59700, "count": 30 }, "credit_card": { "paid_cents": 47760, "count": 24 } },
      "by_gateway": { "asaas": { "paid_cents": 107460, "count": 54 } },
      "by_source": { "subscription": { "paid_cents": 107460, "count": 54 } }
    }
  ],
  "totals": { "paid_cents": 107460, "count": 54, "net_cents": 107460, "...": "mesmos campos, sem date" },
  "applied_filters": { "start": "2026-09-01", "end": "2026-09-13", "gateway": null, "payment_method": null }
}

Regras

  • Um pagamento conta uma vez. O cartao na Asaas emite PAYMENT_CONFIRMED e, ~30 dias depois, PAYMENT_RECEIVED: conta na primeira data, mesmo que a segunda caia em outro mes.
  • Estorno conta na data do estorno; estorno parcial soma so o que saiu.
  • data traz todos os periodos da janela, inclusive os sem movimento (zeros). Com group_by=month, o primeiro e o ultimo mes cobrem so a parte dentro da janela — use start no dia 1 para meses inteiros; applied_filters diz o recorte.
  • Data do cartao = confirmacao, nao liquidacao: o pagamento entra no dia em que o cliente pagou. O dinheiro do cartao cai na conta ~30 dias depois, entao o total do mes nao bate com o extrato bancario (agosto/2026: R$ 46,9 mil confirmados x R$ 44,5 mil liquidados).
  • by_source: subscription ou ticket (Ticket Store). by_method/by_gateway/by_source quebram so os pagamentos.
  • Fatura da Stripe entra como metodo unknown: o payload nao informa o metodo (ADR-0130). PIX pela Stripe (PaymentIntent) nao e notificado ao endpoint e fica fora.
  • coverage_start: primeiro pagamento registrado. Antes dele, zero nao quer dizer que nao entrou dinheiro. Sai da resposta quando nao ha pagamento registrado.
  • Dia e mes de Brasilia (ADR-0137).

2c. GET /admin/finance/payments/entries

Os mesmos pagamentos que o overview soma em gross_revenue, um por linha (ADR-0172). totals.paid_cents e igual ao gross_revenue do mesmo recorte.

GET /api/v1/admin/finance/payments/entries?start=2026-10-22&end=2026-10-22&page=0&limit=100
Authorization: Bearer <admin_token>
Parametro Tipo Default Descricao
start / end YYYY-MM-DD 30 dias Dias de Brasilia, end inclusivo
gateway / payment_method string — Como em /payments
page int 0 Pagina, base 0
limit int 100 Linhas por pagina (max 500)
{
  "total": 77, "page": 0, "limit": 100, "coverage_start": "2026-03-20",
  "totals": { "count": 77, "paid_cents": 153230 },
  "applied_filters": { "start": "2026-10-22", "end": "2026-10-22", "gateway": null, "payment_method": null },
  "data": [
    {
      "payment_id": "pay_yvniakbrupgc9gck", "gateway": "asaas", "source": "subscription",
      "payment_method": "credit_card", "amount_cents": 1990,
      "confirmed_at": "2026-10-22T10:06:00Z", "kind": "renewal", "refunded_cents": 0,
      "subscription_id": "6a…", "customer_id": "69…",
      "customer_created_at": "2025-07-28T13:51:21Z", "customer_utm_source": null
    }
  ]
}
  • Data = primeira confirmacao do gateway. No cartao Asaas e a captura, nao o dueDate: cobranca vencida recapturada conta no dia em que o dinheiro entrou. Em 22/10 foram 5 assim (vencimentos de 08/08 a 21/10, confirmedDate 22/10) — datar pelo dueDate da 72 em vez de 77 e nao fecha com o overview.
  • kind: first_charge = primeiro pagamento da assinatura (Stripe: billing_reason; Asaas: o primeiro no razao, contando a avulsa do 1º mes com desconto). Assinatura que comecou antes de coverage_start sai renewal. null = ingresso (source=ticket) ou cobranca sem assinatura identificada.
  • Cadastro novo x conta antiga: customer_created_at dentro do periodo.
  • refunded_cents: estornado desta cobranca ate agora, em qualquer data.
  • Ordem: confirmed_at. Instantes em UTC (Z); converter para exibir.

2d. GET /admin/finance/payers

Quem compoe overview.active_subscriptions e overview.mrr, uma linha por assinatura viva (active, pending, suspended), com o motivo de quem nao paga (ADR-0174). totals.paying e totals.mrr_cents sao iguais aos do overview no mesmo recorte.

GET /api/v1/admin/finance/payers?paying=false&page=0&limit=100
Authorization: Bearer <admin_token>
Parametro Tipo Default Descricao
gateway / payment_method string — Como no overview
paying bool — true so pagantes, false so nao pagantes
page / limit int 0 / 100 Base 0; limit max 500
{
  "total": 6136, "page": 0, "limit": 100,
  "totals": { "paying": 2126, "mrr_cents": 4211624, "not_paying": 4010,
              "not_paying_by_reason": { "contract_ended_at_gateway": 2253, "current_charge_unpaid": 1701 } },
  "data": [
    { "subscription_id": "6a…", "customer_id": "69…", "gateway": "asaas", "payment_method": "pix",
      "status": "suspended", "is_paying": false, "reason": "current_charge_unpaid",
      "monthly_cents": null, "months": null, "asaas_subscription_id": "sub_…", "stripe_subscription_id": null }
  ]
}
reason Significado
contract_ended_at_gateway Contrato encerrado na Asaas (sincronia diaria)
no_current_charge Nenhuma cobranca venceu nem foi paga ainda
current_charge_unpaid A cobranca vigente (mais recente ja vencida) nao foi paga
chargeback A vigente foi paga e contestada
stripe_no_contract Stripe sem assinatura recorrente (PIX avulso)
stripe_not_active A Stripe nao diz active
counted_in_another_subscription Outro documento do mesmo contrato ja conta (DEBT-100)
  • Pagante nao e status=active. Suspensa com a vigente paga conta; ativa com a vigente em aberto nao. Saldo de agora: sem start/end.

3. GET /admin/finance/overdue

Lista paginada de clientes com pagamentos em atraso. So assinatura viva (active, pending, suspended — ADR-0173): cancelada ou expirada com cobranca aberta e residuo no gateway, a encerrar la, nao cliente a acionar. Ate 23/10/2026 entravam (64 canceladas e 1 cobranca de ingresso).

GET /api/v1/admin/finance/overdue?page=0&limit=20
Authorization: Bearer <admin_token>

Query Parameters

Parametro Tipo Default Descricao
page int 0 Pagina (zero-indexed)
limit int 20 Itens por pagina (max 100)
search string — Busca server-side por username, e-mail ou nome
start YYYY-MM-DD — Vencimento a partir de (Asaas: da cobranca; demais: next_billing_date)
end YYYY-MM-DD — Vencimento ate (inclusivo)
gateway string — asaas, stripe, pagarme ou pagseguro (legado, 1 registro)
payment_method string — credit_card, boleto ou pix

Response (200 OK)

{
  "total_overdue": 34,
  "total_amount_at_risk": 39800,
  "customers": [
    {
      "customer_id": "69b1a2b3c4d5e6f7a8b9c0d1",
      "username": "joao_silva",
      "email": "joao@example.com",
      "full_name": "Joao da Silva",
      "plan_id": "plan-basic",
      "plan_name": "Plano Mensal Basico",
      "gateway": "asaas",
      "payment_method": "credit_card",
      "payment_failures": 3,
      "last_payment_at": "2026-02-15T00:00:00+00:00",
      "next_billing_date": "2026-03-15T00:00:00+00:00",
      "days_overdue": 12,
      "subscription_status": "suspended",
      "amount": 3980,
      "open_charges": 2,
      "oldest_due_date": "2026-03-15"
    }
  ]
}

Campos do customer inadimplente

Campo Tipo Descricao
customer_id string ID do customer (MongoDB ObjectId)
username string Username
email string Email
full_name string Nome completo
plan_id string Slug do plano
plan_name string Nome do plano
gateway string Gateway (stripe/asaas)
payment_method string Metodo (credit_card/pix/boleto)
payment_failures int Falhas consecutivas de pagamento
last_payment_at string Ultimo pagamento (ISO 8601)
next_billing_date string Proxima cobranca (ISO 8601)
days_overdue int Dias desde o vencimento mais antigo em aberto
subscription_status string Status da assinatura
amount int Asaas: soma das cobrancas vencidas em aberto; demais: valor do plano (centavos)
open_charges int Cobrancas vencidas em aberto nesta linha
oldest_due_date string Vencimento mais antigo em aberto (YYYY-MM-DD)

Uso no frontend (tabela)

// Tabela com colunas:
// | Cliente | Email | Plano | Gateway | Falhas | Dias Atraso | Valor | Status |
//
// A API ja ordena: mais dias de atraso, depois maior valor
// Badge vermelho se days_overdue > 7
// Badge amarelo se payment_failures >= 3

4. GET /admin/finance/churn

Analise de churn mensal com breakdown por motivo.

GET /api/v1/admin/finance/churn?months=6
Authorization: Bearer <admin_token>

Query Parameters

Parametro Tipo Default Descricao
months int 6 Meses de calendario de historico (1-24)
start YYYY-MM-DD — Inicio do intervalo; vence months
end YYYY-MM-DD — Fim do intervalo (inclusivo)
gateway string — asaas, stripe, pagarme ou pagseguro (legado, 1 registro)
payment_method string — credit_card, boleto ou pix

Com start/end, a serie continua em buckets mensais, mas o bucket das pontas e recortado ao intervalo: 01/09 a 15/09 devolve a quinzena, nao setembro inteiro. Cada bucket declara o trecho que cobriu em period_start e period_end (este ultimo exclusivo) — sem isso o painel leria "2026-09" como o mes todo.

Formula (ADR-0170) — a mesma do overview.churn_rate/churn_rate_30d e do forecast:

  • cancelled = assinaturas cancelled com cancelled_at no bucket (updated_at so para historico sem cancelled_at).
  • active_start = comecaram antes do bucket e estao active, ou suspended com updated_at depois do inicio, ou cancelaram (cancelled_at) depois do inicio.
  • churn_rate = cancelled / active_start x 100 (1 casa).
  • overview.active_subscriptions nao e essa base: sao os pagantes de agora (ADR-0135).

Response (200 OK)

{
  "monthly": [
    {
      "month": "2026-01",
      "active_start": 33,
      "new": 7,
      "cancelled": 0,
      "active_end": 40,
      "churn_rate": 0.0
    },
    {
      "month": "2026-02",
      "active_start": 39,
      "new": 85,
      "cancelled": 3,
      "active_end": 121,
      "churn_rate": 7.7
    },
    {
      "month": "2026-03",
      "active_start": 110,
      "new": 122,
      "cancelled": 8,
      "active_end": 224,
      "churn_rate": 7.3
    }
  ],
  "by_reason": {
    "payment_failure": 0,
    "customer_cancelled": 11,
    "admin_cancelled": 0,
    "chargeback": 0,
    "expired": 2
  }
}

Uso no frontend (graficos)

// Grafico de area: Evolucao de assinantes
// X: monthly[].month
// Y: monthly[].active_end

// Grafico de barras: New vs Cancelled por mes
// X: monthly[].month
// Barra verde: monthly[].new
// Barra vermelha: monthly[].cancelled

// Grafico de pizza: Motivos de cancelamento
// Fatias: by_reason (payment_failure, customer_cancelled, admin_cancelled, chargeback, expired)

5. GET /admin/finance/export

Exporta dados financeiros consolidados em xlsx (default), csv (zip multi-aba) ou json. Padrão visual definido em ADR-059 (título + metadados + freeze + autofilter + zebra + auto-width + formatos numéricos nativos).

Duas formas de autenticacao (para compatibilidade com download no browser):

# Opcao 1: Header (padrao, via fetch/http)
GET /api/v1/admin/finance/export?start=2026-01-01&end=2026-03-27
Authorization: Bearer <admin_token>

# Opcao 2: Query param (para window.open / <a href> no browser)
GET /api/v1/admin/finance/export?token=<jwt>&start=2026-01-01&end=2026-03-27&format=xlsx

Query Parameters

Parametro Tipo Default Descricao
format string xlsx xlsx (multi-aba), csv (zip), json (aninhado)
start string 90 dias atras Data inicio (YYYY-MM-DD)
end string hoje Data fim (YYYY-MM-DD)
token string - JWT token (alternativa ao header, para download no browser)

Response

format Content-Type Filename
xlsx application/vnd.openxmlformats-officedocument.spreadsheetml.sheet fdplay_financeiro_<start>_<end>.xlsx
csv application/zip (multi-aba) fdplay_financeiro_<start>_<end>.zip
json application/json; charset=utf-8 fdplay_financeiro_<start>_<end>.json

Abas do Excel

Aba Conteudo Colunas
Resumo Recebido, estornado, chargebacks e liquido no periodo (razao de pagamentos, end inclusivo) + MRR, ARR, ativos, clientes, inadimplentes. Valores em 66.844,10 Metrica, Valor
Assinaturas Todas as subscriptions no periodo ID, Cliente, Email, Plano, Gateway, Metodo, Status, Valor, Inicio, Expiracao, Ultimo Pagamento, Falhas, Reembolsado
Inadimplentes Customers com payment_failures > 0 Cliente, Email, Plano, Gateway, Metodo, Falhas, Ultimo Pagamento, Prox. Cobranca, Dias Atraso, Valor, Status
Cancelamentos Subscriptions canceladas no periodo Cliente, Email, Plano, Gateway, Data Cancelamento, Valor
Reembolsos Eventos de refund/chargeback no periodo Evento, Tipo, Subscription, Admin, Motivo, Data

Uso no frontend

// Botao "Exportar Excel" que faz download direto
Future<void> exportFinance(String start, String end) async {
  // Token via query param — funciona com window.open no browser
  final url =
    '$baseUrl/api/v1/admin/finance/export?token=$token&start=$start&end=$end';

  // Web: window.open dispara download nativo do browser
  if (kIsWeb) {
    html.window.open(url, '_blank');
    return;
  }

  // Mobile: salvar via dio + path_provider
  final response = await dio.get(
    url,
    options: Options(responseType: ResponseType.bytes),
  );

  final dir = await getApplicationDocumentsDirectory();
  final file = File('${dir.path}/fdplay_financeiro.xlsx');
  await file.writeAsBytes(response.data);

  // Abrir com app nativo
  OpenFile.open(file.path);
}

Integracao Flutter/Dart

Service completo

import 'dart:convert';
import 'package:http/http.dart' as http;

class FinanceService {
  final String baseUrl;
  final String token;

  FinanceService({required this.baseUrl, required this.token});

  Map<String, String> get _headers => {
    'Authorization': 'Bearer $token',
    'Content-Type': 'application/json',
  };

  /// KPIs consolidados
  Future<Map<String, dynamic>> getOverview() async {
    final r = await http.get(
      Uri.parse('$baseUrl/api/v1/admin/finance/overview'),
      headers: _headers,
    );
    return jsonDecode(r.body);
  }

  /// Serie temporal de receita
  Future<Map<String, dynamic>> getRevenue({
    String period = 'monthly',
    String? start,
    String? end,
  }) async {
    final params = <String, String>{'period': period};
    if (start != null) params['start'] = start;
    if (end != null) params['end'] = end;

    final uri = Uri.parse('$baseUrl/api/v1/admin/finance/revenue')
        .replace(queryParameters: params);
    final r = await http.get(uri, headers: _headers);
    return jsonDecode(r.body);
  }

  /// Clientes inadimplentes
  Future<Map<String, dynamic>> getOverdue({int page = 0, int limit = 20}) async {
    final uri = Uri.parse(
      '$baseUrl/api/v1/admin/finance/overdue?page=$page&limit=$limit',
    );
    final r = await http.get(uri, headers: _headers);
    return jsonDecode(r.body);
  }

  /// Analise de churn
  Future<Map<String, dynamic>> getChurn({int months = 6}) async {
    final uri = Uri.parse(
      '$baseUrl/api/v1/admin/finance/churn?months=$months',
    );
    final r = await http.get(uri, headers: _headers);
    return jsonDecode(r.body);
  }

  /// URL para download do Excel (com token na query p/ browser)
  String getExportUrl({String? start, String? end}) {
    final params = <String, String>{'token': token};
    if (start != null) params['start'] = start;
    if (end != null) params['end'] = end;
    return Uri.parse('$baseUrl/api/v1/admin/finance/export')
        .replace(queryParameters: params)
        .toString();
  }
}

Sugestao de Layout do Dashboard

+---------------------------------------------+
|  MRR         |  ARR         |  Ticket Medio  |
|  R$ 3.840,70 |  R$ 46.088   |  R$ 19,90      |
+---------------------------------------------+
|  Ativos      |  Inadimpl.   |  Churn 30d     |
|  193         |  34 (!)      |  4.0%          |
+---------------------------------------------+

[Grafico Linha: Receita Mensal - Bruto vs Liquido]
[Grafico Barras: Stripe vs Asaas por mes]

+---------------------------------------------+
| [Grafico Pizza:        | [Grafico Barras:    |
|  Metodos Pagamento]    |  Novos vs Cancela]  |
+---------------------------------------------+

[Tabela: Inadimplentes - ordenado por falhas]
| Cliente | Email | Falhas | Dias Atraso | Valor |
|---------|-------|--------|-------------|-------|

[Botao: Exportar Excel]  [Filtro: Periodo]

Errors

HTTP Code Quando
401 UNAUTHORIZED Token JWT ausente ou invalido
403 FORBIDDEN Usuario nao e admin
422 VALIDATION_ERROR Parametros de query invalidos (datas, periodo)
422 — Parametro de query nao declarado na rota (ADR-0128)

Notas Tecnicas

  • Valores em centavos: Todos os campos monetarios (mrr, arr, amount, gross, net) sao em centavos. Frontend converte: cents / 100 para exibir em reais.
  • Churn rate: Calculado como (cancelled_no_mes / ativos_inicio_mes) * 100.
  • Meses de calendario: os buckets de churn, cohort e forecast andam por mes de calendario. Ate a ADR-0128 andavam de 30 * i dias, o que repetia ou pulava um mes inteiro numa sequencia de meses de 31 dias.
  • Overdue (ADR-0134): Asaas = contrato com cobranca vencida em aberto (sem pagamento, exclusao ou estorno depois), uma linha por contrato e, se avulsa, por cliente; demais gateways = venceu ha >=1 dia e status active/pending/suspended. Ate 14/09/2026 a Asaas tambem era pelo next_billing_date: 1.608 no painel x 1.240 na Asaas.
  • Dia de Brasilia (ADR-0137): start/end sao dias de America/Sao_Paulo (start = 03:00 UTC; end inclusivo); buckets diarios/mensais e applied_filters tambem. Ate 14/09/2026 eram dias UTC.
  • Sincronizacao (ADR-0138): o painel le a Asaas pelo webhook_logs; o que o webhook nao entrega (cobranca removida, chargeback, contrato encerrado) chega pela sincronizacao diaria das 03:00.
  • Saldo x fluxo: mrr, arr, active_mrr sao saldo (estado da base); gross, net, net_revenue, paid_cents sao fluxo, lidos do razao de pagamentos (webhook_logs, ADR-0132).
  • Export: Gera .xlsx com openpyxl. Headers estilizados (fundo escuro, fonte branca). Colunas auto-dimensionadas.
  • Performance: Overview faz ~5 queries MongoDB. Revenue itera subscriptions em memoria (ok para <10k subs). Para escala maior, migrar para aggregation pipeline.