R-010 — Recebimento de eventos do accounts — gravado antes do 2xx, 3 tentativas

TLDR: todo webhook do accounts é gravado em incoming_events antes de a resposta sair, deduplicado pelo event_id e processado por um ProcessIncomingEventJob com até 3 tentativas. failed é terminal. É a ponta de recebimento (inbox) do OutgoingEvent do accounts (outbox).

Given / When / Then

Dado um POST /api/v1/incoming_events com Authorization: Bearer <ACCOUNTS_WEBHOOK_TOKEN> e envelope completo (event_id, event_type, occurred_at, data), Quando o event_id ainda não foi recebido, Então o envelope inteiro é gravado como pending e o ProcessIncomingEventJob é enfileirado na mesma transação, e a resposta é 202. O Solid Queue grava o job no mesmo banco, então nunca existe evento pending sem job.

Dado um envelope cujo event_id já está gravado, Quando ele chega de novo (retry do accounts, reenvio pelo admin, entrega concorrente), Então a resposta é 200, e nada é gravado nem enfileirado.

Dado uma requisição sem Bearer ou com token diferente de ACCOUNTS_WEBHOOK_TOKEN, Quando ela chega, Então a resposta é 401. Um JWT de usuário também não autentica este endpoint.

Dado um corpo que não é JSON, não é objeto, ou não tem algum dos quatro campos do envelope (ou tem occurred_at que não é data), Quando ele chega, Então a resposta é 422 com code: invalid_payload, e nada é gravado. O accounts trata como falha e desiste na 3ª tentativa dele.

Dado um IncomingEvent pending, Quando o job o processa, Então IncomingEvents::Process resolve o use case pelo event_type e passa o data do envelope. As escritas do use case e o processed entram numa transação só; numa falha, elas são revertidas e a tentativa é registrada fora dessa transação.

Tabela de decisão

Situação attempts antes status depois Reenfileira?
Use case devolve Success qualquer processed, com processed_at não
Use case devolve Failure ou levanta exceção 0 ou 1 pending, attempts + 1 sim, em 5s
Use case devolve Failure ou levanta exceção 2 failed, attempts: 3, com processed_at não
Sem use case para o event_type 0 ou 1 pending, attempts + 1 sim, em 5s
Sem use case para o event_type 2 failed, attempts: 3 não
Evento já não está pending — inalterado não

Restrições

  • Não há distinção entre erro retryable e definitivo: exceção, Failure e evento sem dono gastam as mesmas 3 tentativas, igual à R-001 do synapse no accounts.
  • Evento sem use case termina failed, com error_message igual a no use case registered for event: <event_type>. Hoje o registry (IncomingEvents::Process::USE_CASES) está vazio, então todo INSTALLMENT_OVERDUE recebido termina assim, mas fica gravado para reprocesso.
  • A contagem vive na linha (attempts), não no executions do ActiveJob. O intervalo é ProcessIncomingEventJob::RETRY_DELAY (5s). Não há varredura periódica: cada evento tem o seu job.
  • O payload guarda o corpo exatamente como chegou (lido de request.raw_post), sem wrap nem filtro de params.
  • Reprocesso só pelo console, sem interface:

    ruby event.update!(status: :pending, attempts: 0, error_message: nil, processed_at: nil) ProcessIncomingEventJob.perform_later(event.id)

  • Sem alerta automático (Sentry, Slack) na falha definitiva.
  • Sem assinatura HMAC do corpo: o accounts manda só o Bearer da WebhookSubscription.

Teste vinculado

modules/backend/test/models/incoming_event_test.rb (transições de status), modules/backend/test/use_cases/incoming_events/create_test.rb (gravação, dedup, envelope inválido), modules/backend/test/use_cases/incoming_events/process_test.rb (registry), modules/backend/test/jobs/process_incoming_event_job_test.rb (cada linha da tabela de decisão e o rollback) e modules/backend/test/controllers/api/v1/incoming_events_controller_test.rb (autenticação e códigos HTTP).

Referências

  • ../../specs/20260924094344_incoming_events_inbox.md: decisões e escopo
  • accounts/.project/docs/rules/synapse/incoming_event_processing.md (R-001 do synapse): a mesma regra do lado do accounts
  • accounts/.project/docs/rules/synapse/outgoing_event_delivery.md (R-002 do synapse): quem entrega os eventos que chegam aqui