R-010 — Recebimento de eventos do accounts — gravado antes do 2xx, 3 tentativas
TLDR: todo webhook do accounts é gravado em
incoming_eventsantes de a resposta sair, deduplicado peloevent_ide processado por umProcessIncomingEventJobcom até 3 tentativas.failedé terminal. É a ponta de recebimento (inbox) doOutgoingEventdo 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,
Failuree evento sem dono gastam as mesmas 3 tentativas, igual àR-001do synapse no accounts. - Evento sem use case termina
failed, comerror_messageigual ano use case registered for event: <event_type>. Hoje o registry (IncomingEvents::Process::USE_CASES) está vazio, então todoINSTALLMENT_OVERDUErecebido termina assim, mas fica gravado para reprocesso. - A contagem vive na linha (
attempts), não noexecutionsdo ActiveJob. O intervalo éProcessIncomingEventJob::RETRY_DELAY(5s). Não há varredura periódica: cada evento tem o seu job. - O
payloadguarda o corpo exatamente como chegou (lido derequest.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-001do synapse): a mesma regra do lado do accountsaccounts/.project/docs/rules/synapse/outgoing_event_delivery.md(R-002do synapse): quem entrega os eventos que chegam aqui