Métricas mensais nas telas de pagamento (parte 4 de 4)

TLDR: Adiciona cards de métricas com comparação mês atual vs. mês anterior nas três telas de pagamento do Avo, dando visibilidade de tendência, valor pendente e saúde do sistema de retry.

Sequência: parte 4 de uma iniciativa de 4 specs escritas entre 27/01 e 03/02/2026 — depende da parte 3: interface Avo.

Contexto

As três telas de pagamento não tinham nenhum contexto temporal ou totalizador:

Tela Lacuna Impacto
Ordens de pagamento lista de invoices sem contexto temporal, sem comparação entre períodos admins não sabiam quanto foi pago no mês, nem se a tendência era de alta ou baixa; difícil identificar invoices pendentes antigas
Erros de pagamento lista de erros sem agrupamento temporal nem valor financeiro afetado impossível saber se os erros estavam aumentando, ou priorizar por impacto financeiro
Terapeutas à pagar lista de profissionais sem totalizadores planejamento financeiro prejudicado, sem visão do passivo total nem de quantos profissionais estão bloqueados

Objetivos

  • Expor total pago, quantidade paga, pendências e valor pendente no mês, com variação percentual contra o mês anterior.
  • Expor volume de erros, valor afetado, erros críticos e taxa de sucesso de retry.
  • Expor total disponível, profissionais elegíveis, bloqueados por conta não aprovada e total de sessões acumuladas.
  • Adicionar scopes temporais para filtro rápido por período.

Fora de escopo

  • Exportação das métricas para CSV/Excel.
  • Gráficos de série temporal — apenas cards de métrica com comparação pontual.

Mudanças

1. Ordens de pagamento — cards mensais

Card Exibe
MonthlyPaidTotalCard valor total pago no mês (R$) + variação % vs. mês anterior
MonthlyPaidCountCard quantidade de invoices pagas no mês + variação %
PendingInvoicesCard total de invoices pendentes + quantas há mais de 7 dias (alerta)
PendingTotalCard valor total em invoices pendentes, para planejamento de caixa

Padrão dos cards de comparação:

```ruby class Avo::Cards::ProfessionalInvoices::MonthlyPaidTotalCard < Avo::Cards::MetricCard self.id = “monthly_paid_total” self.label = “Total Pago Este Mês” self.description = “Comparado com o mês anterior”

def query current_month_total = ProfessionalPaymentInvoice.paid .where(paid_at: Time.current.beginning_of_month..Time.current.end_of_month).sum(:total)

last_month_total = ProfessionalPaymentInvoice.paid
  .where(paid_at: 1.month.ago.beginning_of_month..1.month.ago.end_of_month).sum(:total)

result current_month_total
prefix "R$"

if last_month_total > 0
  change = ((current_month_total - last_month_total) / last_month_total * 100).round(1)
  suffix "#{change > 0 ? '+' : ''}#{change}% vs mês anterior"
end   end end ```

Scopes: InvoicesCurrentMonth, InvoicesLastMonth, PaidCurrentMonth, PendingOld (pendentes há mais de 7 dias), todos exibindo a contagem no nome.

2. Erros de pagamento — cards mensais

Card Exibe
MonthlyErrorsCountCard quantidade de erros no mês + variação % vs. mês anterior
MonthlyAffectedValueCard valor total (R$) afetado por erros, para priorizar por impacto
CriticalErrorsCard erros com retry_count >= 5 ou created_at < 10.days.ago + valor total
RetrySuccessRateCard % de invoices pagas após retry nos últimos 30 dias

Erros são identificados por where.not(last_error_message: [nil, ""]).

Scopes: ErrorsCurrentMonth, ErrorsLastMonth, CriticalErrors.

3. Terapeutas à pagar — cards de resumo

Card Exibe
TotalAvailableCard soma do repasse líquido de todos os profissionais elegíveis
EligibleCountCard profissionais com conta aprovada e sessões, sobre o total da lista
PendingApprovalCard profissionais com sessões mas sem conta aprovada + valor retido
TotalMeetingsCard total de sessões acumuladas esperando pagamento

O cálculo do líquido em cada card segue subtotal - CalculateFee.call(subtotal) - 3.0, com piso em zero.

Arquivos

Modificados: app/avo/resources/professional_payment_invoice.rb, app/avo/resources/payment_error.rb, app/avo/resources/invoiceable_professional.rb.

Novos: os 12 cards acima em app/avo/cards/professional_invoices/, app/avo/cards/payment_errors/ e app/avo/cards/invoiceable_professionals/, mais os 7 scopes em app/avo/scopes/.

Desvio verificado em 2026-08-06: os cards e scopes foram entregues, mas o conjunto final difere do proposto. O resource de invoices carrega hoje MonthlyPaidTotalCard, PendingTotalCard, TotalInvoicesCard, MonthlyPaidCountCard, PendingInvoicesCard e TotalMeetingsCard — TotalInvoicesCard não estava previsto aqui. Em invoiceable_professionals/ existem também total_pending_card.rb, total_professionals_card.rb e next_month_card.rb, fora do escopo original. Os cards de invoiceable_professionals continuam consumindo ProfessionalPaymentInvoices::InvoiceableProfessionalsQuery, cuja remoção era objetivo da parte 1.

Como verificar

  • Abrir cada uma das três telas no Avo e confirmar que os cards renderizam sem erro, mesmo com o mês anterior zerado (não deve haver divisão por zero).
  • Conferir o valor de MonthlyPaidTotalCard contra ProfessionalPaymentInvoice.paid.where(paid_at: <mês>).sum(:total) no console.
  • Aplicar cada scope temporal e confirmar que a contagem exibida no nome bate com a lista filtrada.

Documentação

As telas e cards atuais estão descritos em invoice_payment_flow.