Condição de corrida na expiração do PIX
Incidente: order 144487 — Diego Benjamim / Marcos Windson Silva — 14/03/2026
O que aconteceu
O cliente pagou o PIX às 00:25. O ExpireJob rodou às 00:54 (exatamente 30 minutos após a criação da order), encontrou o pagamento ainda pending e o expirou — destruindo todas as sessões. O webhook de confirmação do gateway chegou tarde demais.
A sessão foi realizada mesmo assim (o terapeuta atendeu fora da plataforma). Nenhum repasse foi gerado.
Causa raiz
Dois comportamentos que interagem criam a janela:
-
Payments::Expire— expira sepayment.pending?, não importa quão perto do horário do pagamento. A janela de 30 minutos não considera a latência de confirmação do PIX. -
Payments::UpdateMeetingsByGatewayStatus— só chamaOrders::ConfirmPaymentquandoorder.meetings.pending.exists?. Se as sessões já foram destruídas, o webhook silenciosamente não faz nada.
ruby
# app/use_cases/payments/update_meetings_by_gateway_status.rb
if context.payment.paid? && order.meetings.pending.exists? # <- falha silenciosa aqui
Orders::ConfirmPayment.call(order: context.order)
end
As regras envolvidas estão em R-001 e R-002.
Correção
Não houve correção sistêmica — o caso foi resolvido manualmente. O procedimento de investigação e reparo está abaixo, porque a janela continua aberta.
Como investigar um caso semelhante
```ruby
# 1. Encontrar a meeting pelo pid
meeting = Meeting.find_by(pid: “
puts payment.status # deve ser expired ou paid puts payment.updated_at # quando mudou puts meeting.cancel_reason
2. Verificar todas as orders desse paciente/profissional
Order.joins(:meetings).where(meetings: { user_client_id: meeting.user_client_id }) .each { |o| puts “#{o.id} | #{o.payment&.status} | #{o.payment&.updated_at}” }
3. Confirmar o pagamento no gateway
result = Zeuspay::V2::Payments::Find.new( payment_id: payment.gateway_payment_id ).perform puts result.status # :paid significa que o dinheiro entrou puts result.response_body.inspect
4. Conferir o pixQrCodeId com o comprovante do cliente
# result.response_body[:installments][0][:billing_type_payload][:payload] # deve conter o mesmo pixQrCodeId mostrado no comprovante do banco ```
Como corrigir manualmente
Só prossiga depois de confirmar que o gateway mostra paid/RECEIVED.
```ruby
# Passo 1 — corrigir o status do pagamento
payment = Order.find(
Passo 2 — recriar a meeting (validate: false ignora as validações de data)
meeting = Meeting.new(
status: :finished,
start_at: Time.zone.parse(“"),
end_at: Time.zone.parse("<data e hora da sessão + duração>"),
location_type: :online, # confirmar com o terapeuta
owner_id:
Passo 3 — buscar a taxa do terapeuta numa invoice recente
recent = ProfessionalPaymentInvoice.where(professional_id:
Passo 4 — criar a invoice de repasse
invoice = ProfessionalPaymentInvoice.create!(
professional_id: ,
fee_total:
Passo 5 — vincular a meeting à invoice
ProfessionalPaymentInvoiceMeeting.create!( professional_payment_invoice_id: invoice.id, meeting_id: meeting.id, total:
)
Passo 6 — disparar o job de transferência
ProfessionalPaymentInvoiceTransferCreationJob.perform_later(invoice.id) ```
Como evitar
- Aumentar a janela de expiração — o PIX tem alguns minutos de latência de liquidação; 30 minutos é apertado.
- Usar soft delete nas sessões — se as sessões forem recuperáveis, um webhook atrasado pode reativá-las em vez de falhar em silêncio.
- Tratar o caso do webhook atrasado explicitamente — quando
payment.paid?e não existem sessõespending, verificar se as sessões foram canceladas pelo sistema e recuperá-las (ou, no mínimo, disparar um alerta). - Adicionar alerta — quando o
ExpireJobrodar e o pagamento estiver confirmado do lado do gateway, disparar alerta no Slack/Sentry para revisão manual.