Estornos de pagamento — integração com o Asaas

TLDR: armazenar e rastrear os estornos recebidos nos webhooks do Asaas, associando-os à parcela correspondente e expondo essa informação no sistema de eventos.

Contexto

Os webhooks do Asaas trazem informação de estorno, mas ela é descartada. Não há como auditar estornos nem repassá-los aos integradores.

Os status possíveis de um estorno no Asaas:

Status Significado
PENDING Estado inicial do estorno
AWAITING_CRITICAL_ACTION_AUTHORIZATION Aguardando autorização de ação crítica
AWAITING_CUSTOMER_EXTERNAL_AUTHORIZATION Aguardando autorização externa do cliente
CANCELLED Estorno cancelado
DONE Estorno processado com sucesso

Objetivos

  • Criar novos modelos para armazenar a informação de estorno.
  • Processar os dados de estorno dos webhooks do Asaas no nível da parcela.
  • Evitar estornos duplicados por meio de constraint única composta.
  • Incluir a informação de estorno nos eventos de pagamento.
  • Armazenar a informação de split dos estornos.
  • Logar warning quando o total dos splits exceder o total do pagamento.

Fora de escopo

— (não registrado na spec original)

Mudanças

Modelo PaymentRefund

belongs_to :installment, has_many :payment_refund_splits. Local: app/models/payment_refund.rb.

ruby enum status: { pending: 'pending', awaiting_critical_action_authorization: 'awaiting_critical_action_authorization', awaiting_customer_external_authorization: 'awaiting_customer_external_authorization', cancelled: 'cancelled', done: 'done' }, _prefix: true

Campo Nome Descrição
installment_id ID da parcela Parcela associada
status Status Enum de status do estorno
value Valor Valor do estorno
date_created Data de criação Quando o estorno foi criado no Asaas
effective_date Data efetiva Quando o estorno foi efetivamente processado
description Descrição Descrição opcional
transaction_receipt_url URL do comprovante Nullable
end_to_end_identifier Identificador end-to-end Nullable
created_at Criado em Quando o registro foi criado no nosso sistema
updated_at Atualizado em Última atualização no nosso sistema

Índice único composto em (installment_id, value, date_created, effective_date).

Modelo PaymentRefundSplit

belongs_to :payment_refund. Local: app/models/payment_refund_split.rb.

Campo Nome Descrição
payment_refund_id ID do estorno Estorno associado
external_reference Referência externa O ID do split no Asaas
value Valor Valor do split
done Concluído Se o split já foi processado
created_at Criado em —
updated_at Atualizado em —

Use case ProcessRefunds

Local: app/use_cases/incoming_webhooks/payment_gateways/asaas/payments/process_refunds.rb

  • Recebe o pagamento e os dados de estorno do contexto.
  • Usa os campos únicos compostos para evitar duplicatas.
  • Cria ou atualiza os registros de estorno.
  • Processa os splits associados.
  • Loga warning quando o total do split excede installment.payment.total.
  • Trata os casos de erro.

Integração no flow

Em app/use_cases/incoming_webhooks/payment_gateways/asaas/payments/payments_flow.rb:

ruby flow( # ... steps existentes ... Payments::Update, IncomingWebhooks::PaymentGateways::Asaas::Payments::ProcessRefunds, # novo step EventsHub::EventsDispatcher::Organization::EnqueueEvent )

Payload de eventos

  • Criar PaymentRefundSerializer em app/serializers/payment_refund_serializer.rb, incluindo todos os campos exceto splits, sem campos calculados.
  • Em app/use_cases/outcome_webhooks/integration_webhooks/dispatcher/event_types/payments_payload.rb, adicionar o array de estornos ao payload do pagamento, através do relacionamento has_many.

Logging

ruby Rails.logger.error("Payment refund split total exceeds payment total for refund_id: #{refund.id}")

ActiveAdmin

Local: app/admin/payment_refunds.rb. Recurso somente leitura (actions :index, :show), sob o menu Payments, ordenado por created_at_desc.

  • Filtros: Asaas Payment ID, Payment ID, status, valor, date_created, effective_date, created_at e updated_at (date range).
  • Index: ID, link para o pagamento, Asaas Payment ID, status como status_tag, valor como moeda, date_created, effective_date, created_at.
  • Show: atributos do estorno, com link para o pagamento e para o comprovante, e um painel com a tabela de payment_refund_splits.
  • Scopes: all (default), pending, awaiting_authorization, done, cancelled.
  • CSV: exportação com os mesmos campos do index.

Como verificar

  • Testes de modelo — constraints únicas, relacionamentos e validações.
  • Testes do use case — criação de estorno novo, tratamento de duplicata, atualização, processamento de splits, casos de erro, e o log quando o total do split excede o do pagamento.
  • Testes de integração — fluxo completo do webhook com estornos, dispatch do evento com dados de estorno, serialização do payload e verificação da estrutura do estorno no payload.

Documentação

  • Estrutura do estorno no payload público: ../reference/payments/payment_webhooks.md
  • Implementação: app/models/payment_refund.rb, app/models/payment_refund_split.rb, app/use_cases/incoming_webhooks/payment_gateways/asaas/payments/process_refunds.rb, app/admin/payment_refunds.rb