0017 — Enriquecimento sob demanda do pedido preso na fila¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-08 · Decidido em: 2026-09-04
Contexto¶
O pedido chega por duas vias: webhook (tempo real) e poll REST (backstop, a cada 6h). O payload do
webhook não traz o objeto invoice (aba Faturamento) — só o REST traz. E o invoice é o insumo
do cliente do Faturamento (0009), então o PushService tem um
guard de prontidão: sem invoice, o enfileiramento devolve PULADO — sem marcar ERRO, sem
mudar o status.
Resultado: pedido pago que chega por webhook fica em NA_FILA até o próximo poll — até ~6h
parado, sem erro, sem alerta e (antes da revisão do 0006) anunciado ao
cliente como "Enviando ao X-Adm". Aconteceu em produção no 1º pedido real: 260078922 chegou por
webhook às 14:17Z de 2026-09-04, ficou preso, e só saiu (ENVIADO 14:58Z, CONFIRMADO 15:00Z)
depois de alguém puxar a página do REST na mão.
Restrição do contrato da PIED: não existe busca de pedido por id. Só
GET /requests/order/{página}/{limite}?lastUpdateAfter=AAAA-MM-DD — ou seja, "buscar aquele pedido"
é sempre varrer a janela daquela data. E a PIED impõe dois tetos por hora (chamadas e tempo de
processamento), então varredura é um recurso escasso.
Decisão¶
O TransformJob abre cada rodada (5 min) chamando o novo integracao/EnriquecimentoService:
havendo NA_FILA sem invoice, ele busca a janela no REST, grava o raw em pied_rest e a
normalização da mesma rodada aplica o payload completo — o pedido destrava em ≤5 min em vez de
~6h.
- No job que já sabe quem está preso, não num job novo: o
TransformJobjá roda a cada 5 min, já tem guarda de cluster (0014) e já é o dono da fila. Zero superfície nova (sem toggle, sem lock, sem@Scheduledextra). - Antes da normalização, não depois: assim a página baixada é aproveitada na mesma rodada.
- Janela do preso mais antigo, menos 1 dia.
lastUpdateAftertem granularidade de dia e semântica after; usar a data exata arriscaria excluir o próprio pedido. Reprocessar um dia a mais é barato (upsert idempotente porcode). - Custo contido, por construção: nenhuma chamada quando não há preso (o caminho comum); para
assim que todos os presos apareceram numa página; teto de 5 páginas por rodada; e falha de
rede/cota (429) vira
WARNsem derrubar a rodada — é backstop, e o poll de 6h continua sendo a rede de segurança. - Só grava raw. Quem transforma em
pied_pedidoé a normalização, como no poll — o serviço não duplica regra de negócio nem avança o cursor do poll (é leitura lateral, como o "Puxar da PIED").
Alternativas consideradas¶
- Disparar no webhook (event-driven puro), ao detectar
order.*seminvoice: era o pedido literal ("assim que chegar o pagamento") e daria latência de segundos. Preterida: sem endpoint por id, cada disparo ainda varre páginas, e uma rajada de webhooks vira uma rajada de varreduras contra os tetos horários — precisaria de debounce e teto próprios para chegar onde o job já chega. Ganho real (5 min → segundos) não paga o risco de cota. - Job dedicado (
EnriquecimentoJob, intervalo e toggle próprios): preterida por over-engineering — mais um@Scheduled, mais uma config, mais um advisory lock e mais testes para fazer o que oTransformJobfaz na mesma cadência. - Deixar como estava (esperar o poll): rejeitada — 6h de atraso silencioso num pedido pago é o pior dos mundos: o cliente não vê erro, o operador não vê alerta e o X-Adm não recebe.
- Encurtar
PIED_POLL_INTERVALO: rejeitada — produtos/clientes não têm cursor incremental, então cada rodada re-varre tudo e inchapied_rest. Barateia o sintoma inflando o landing.
Consequências¶
- Espera do pedido preso cai de ~6h para ≤5 min; o caso "preso" deixa de depender de alguém perceber e clicar em "Puxar da PIED" (que continua existindo como atalho manual).
- Uma chamada REST a mais por rodada apenas quando há preso — o estado normal (fila pronta ou vazia) não gasta cota.
- Novo componente
EnriquecimentoService+PiedPedidoRepository.presosSemInvoice(...); oTransformJobganhou uma dependência. NA_FILAdeixa de ser um estado de espera longa e passa a ser transitório. Pedido preso além de uma rodada vira anomalia — as causas residuais estão abaixo.
Quando ainda prende (e como sair)¶
O enriquecimento encurta a espera, não a elimina. Preso por mais de uma rodada é uma destas:
| Causa | Sinal |
|---|---|
PIED_TOKEN vazio |
Enriquecimento: PIED_TOKEN vazio — pulado. |
PIED_INTEGRACAO_HABILITADA=false |
O TransformJob sai antes de tudo — nem enriquece nem drena. Pedido posto em NA_FILA pelo gate manual do /console fica parado |
| Cota (429), PIED fora do ar, timeout | Enriquecimento: falha ao buscar na PIED — … (retoma na próxima rodada). |
| Preso fora do teto de 5 páginas | … N ainda sem dado. rodada após rodada |
| Pedido sumiu da janela na PIED | Idem, e nunca resolve sozinho |
| Guard de recência recusando o REST | O enriquecimento acha o pedido (0 ainda sem dado.), o pied_rest tem o invoice, e mesmo assim o pied_pedido.payload nunca ganha o invoice — ver 0018 A.1 |
Diagnóstico:
-- na fila E sem invoice = ainda esperando dado
SELECT code, deal_status, payment_status, atualizado_em
FROM pied_pedido
WHERE status = 'NA_FILA'
AND jsonb_typeof(payload -> 'invoice') IS DISTINCT FROM 'object';
Se essa consulta acusa preso e o pied_rest já tem o invoice do mesmo code, o problema não
é captura: é o guard de recência recusando o snapshot do REST — nesse caso nem "Puxar da PIED"
nem "Re-normalizar" resolvem (ambos passam pelo mesmo upsert), e o conserto está em
0018 A.1. Comparar as duas fontes:
SELECT p.code, p.last_update AS no_pedido,
(i ->> 'lastUpdate') AS no_rest,
jsonb_typeof(i -> 'invoice') AS invoice_no_rest
FROM pied_pedido p
JOIN pied_rest r ON r.entidade = 'pedidos'
JOIN LATERAL jsonb_array_elements(r.payload -> 'data' -> 'items') i ON i ->> 'code' = p.code
WHERE p.status = 'NA_FILA'
ORDER BY r.buscado_em DESC LIMIT 20;
Conserto manual — o botão "Puxar da PIED" do /console (peek: grava o raw, não avança o cursor,
idempotente por code; n aceita 1 ou 5 e só normaliza os estados-venda):
curl -X POST "https://pied.maxsul.xadm.biz/console/puxar-pied?n=5" \
-H "Content-Type: application/x-www-form-urlencoded" --data ""
Depois disso o pedido sai na rodada seguinte (≤5 min), ou na hora pelo Enviar do detalhe
(POST /console/{code}/enviar). O CONFIRMADO + chave_xadm fecham pelo ReconciliacaoJob.
Relacionadas¶
- 0006 — Painel colaborador: a revisão de prontidão do bucket saiu do mesmo incidente — enquanto o pedido espera dado, o painel diz "Pedidos em aberto", não "Enviando ao X-Adm".
- 0009 — Cliente do Faturamento: por que o
invoiceé obrigatório. - Configuração:
PIED_POLL_INTERVALO, toggles e ações do/console.