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
PaymentRefundSerializeremapp/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 relacionamentohas_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_ateupdated_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