Race condition na expiração do PIX
Incidente: Order 144487 — Diego Benjamim / Marcos Windson Silva — 2026-03-14
O que aconteceu
O cliente pagou um 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 aconteceu mesmo assim (o terapeuta atendeu fora da plataforma). Nenhum payout foi gerado.
Como investigar um caso semelhante
```ruby
# 1. Find the meeting by pid
meeting = Meeting.find_by(pid: “
puts payment.status # should be expired or paid puts payment.updated_at # when it changed puts meeting.cancel_reason
2. Check all orders for this patient/professional
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. Confirm payment on the gateway
result = Zeuspay::V2::Payments::Find.new( payment_id: payment.gateway_payment_id ).perform puts result.status # :paid means money was received puts result.response_body.inspect
4. Cross-check the pixQrCodeId with the client receipt
# result.response_body[:installments][0][:billing_type_payload][:payload] # should contain the same pixQrCodeId shown in the Nubank/bank receipt ```
Como corrigir manualmente
Só proceder após confirmar que o gateway mostra paid/RECEIVED.
```ruby
# Step 1 — Fix payment status
payment = Order.find(
Step 2 — Recreate the meeting (validate: false bypasses date validations)
meeting = Meeting.new(
status: :finished,
start_at: Time.zone.parse(“
Step 3 — Find therapist fee from a recent invoice
recent = ProfessionalPaymentInvoice.where(professional_id:
Step 4 — Create the payout invoice
invoice = ProfessionalPaymentInvoice.create!(
professional_id:
Step 5 — Link meeting to invoice
ProfessionalPaymentInvoiceMeeting.create!(
professional_payment_invoice_id: invoice.id,
meeting_id: meeting.id,
total:
Step 6 — Trigger the transfer job
ProfessionalPaymentInvoiceTransferCreationJob.perform_later(invoice.id) ```
Causa raiz
Dois comportamentos que interagem criam a lacuna:
-
Payments::Expire— expira sepayment.pending?, independente de quão perto do momento 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? # <- silent failure here
Orders::ConfirmPayment.call(order: context.order)
end
O que poderia prevenir isso
- Aumentar a janela de expiração — o PIX tem até alguns minutos de latência de liquidação; 30 minutos é justo.
- Usar soft delete nas sessões — se as sessões forem recuperáveis, um webhook atrasado pode reativá-las em vez de falhar silenciosamente.
- Tratar o caso de webhook atrasado explicitamente — quando
payment.paid?e não há sessões pendentes, verificar se as sessões foram canceladas pelo sistema e recuperá-las (ou no mínimo disparar um alerta). - Adicionar um alerta — quando
ExpireJobroda e o pagamento foi confirmado no lado do gateway, disparar um alerta no Slack/Sentry para revisão manual.