Pular para conteúdo

0005 — Alerta por e-mail no erro terminal do pedido

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-14 · Decidido em: 2026-07-14

Contexto

Na Fase 2, um pedido pode ser rejeitado pelo X-Adm na reconciliação do retorno (decisão 0004). A máquina de status distingue dois tipos de rejeição pelo codRetorno cru: 9XX reprocessável (dá para reenfileirar) e 1XX terminal (rejeição pelos dados do pedido — irrecuperável). Hoje esse desfecho é silencioso: só quem abre o /console percebe. A operação da Maxsul precisa ser avisada quando um pedido morre assim.

Distinção-chave: é um erro do pedido/dado, não do sistema. Erros de sistema (transporte, exceção) já vão ao GlitchTip via o SentryAppender do logback — não é isso que se quer notificar aqui.

Decisão

Quando um pedido entra em ERRO_XADM com codRetorno iniciando em "1" (terminal), o app envia um e-mail aos destinatários cadastrados, no ponto único ReconciliacaoJob.reconciliar (que serve tanto o job agendado quanto a ação "Atualizar" do /console). Um pacote novo alerta/:

  • Destinatários — tabela pied_destinatario_alerta (nome + e-mail, único por lower(email)), mantida por uma tela Admin aberta /destinatarios (CRUD hard-delete), no mesmo padrão de /console (form-urlencoded, PRG com ?flash=).
  • Envio via Resend — ResendClient sobre o java.net.http.HttpClient do JDK (idioma da casa, espelha IntegradorClient), POST /emails com Authorization: Bearer; um e-mail com todos os destinatários no to[]. O corpo traz code, codRetorno, msgRetorno, chaveXadm + link {base-url}/pedido/{code}.
  • Best-effort — se desabilitado, sem chave/remetente, sem destinatários, ou se o envio falhar, o AlertaService só loga (LOG.error → GlitchTip) e retorna normal: nunca quebra a reconciliação nem altera a máquina de status.
  • Nasce desligado (pied.alerta.habilitado=false) e depende da Fase 2 — fica dormente até a integração ser ligada. Configuração em operação/configuração.

Revisão (2026-08-14) — o 1XX ganha reentrada manual no painel. O detalhe do pedido no painel (0006) passou a exibir o msgRetorno do X-Adm no erro terminal (1XX = rejeição de dado, acionável por quem cadastra, ex.: "Cliente CPF … não cadastrado.") e a oferecer um botão Re-Importar que reenfileira e reenvia. Consequência semântica: "1XX terminal" é terminal-até-corrigir, não "nunca reprocessa" — o operador corrige o dado no X-Adm e reenvia. O que não mudou: o gate automático e a reconciliação seguem tratando o 1XX como final (nunca re-tentam sozinhos), o e-mail de alerta continua só no 1XX, e o /console mantém sua regra de barrar o 1XX no reenviar. A reentrada é manual (clique do operador), nunca automática.

Revisão (2026-09-08) — reentrada manual só funciona se o espelho for re-armado. O botão Re-Importar dizia "Reenvio: ENFILEIRADO" e não reenviava: reenfileirar (pied_pedido → NA_FILA) e empurrar o dado não bastam, porque quem manda no reprocessamento é o contratos.cod_retorno do espelho — escrito pelo write-back e não tocado pelo push. O cliente do X-Adm só coleta 000|9XX, então linha em 1XX fica invisível para ele para sempre (caso real: pedido 260079421, três cliques, zero reenvios). O painel passou a chamar POST /api/v1/xadm/retorno/pedido/reenfileirar no Integrador depois do push (ordem: dado fresco primeiro, fila reaberta depois — transações separadas), e o flash agora conta a verdade (N linhas reabertas · nada a reabrir · falhou). O que não mudou: gate automático e reconciliação seguem tratando 1XX como final, e o /console mantém a regra de barrar 1XX no reenviar (decidido em 2026-09-08: só o painel faz recuperação manual).

Revisão (2026-09-10) — a reentrada padrão do 1XX é no X-Adm, à mão. O X-Adm é caminho só de ida e o re-arme só reabre o que falhou; nos casos reais (incidente dos 16 pedidos-kit) reimportar não resolvia. O painel passa a orientar o lançamento à mão + "Importado manualmente" e só oferece o Re-Importar com parte pendente (filho travado por pai 1XX) — ver 0006. O reenvio automático por dado novo da PIED deixa de pegar ERRO_XADM e fica só com o ERRO de envio. O e-mail não muda: só no 1XX, com o link do detalhe.

Revisão (2026-09-14) — a marca de "já alertado" passa a morar no banco. O e-mail sai uma vez por (code, codRetorno), porque a rodada revisita as TRAVADA a cada minuto. Essa marca vivia na RAM do ReconciliacaoJob, com a premissa de que realertar uma vez a cada reinício era desejável. Medido em produção: dois reboots do servidor reenviaram os mesmos 20 alertas antigos duas vezes (80 e-mails, 80% da cota diária do Resend, que é compartilhada com outros apps). A premissa caiu. O último código alertado fica em pied_pedido.alerta_cod_retorno (V13), e o envio só acontece quando o UPDATE … WHERE alerta_cod_retorno IS DISTINCT FROM :codRetorno grava alguma linha. A marca zera quando o pedido confirma ou é reenfileirado: uma nova rejeição depois de uma nova tentativa volta a alertar, mesmo com o mesmo código. A V13 preenche a marca dos ERRO_XADM 1XX existentes para o deploy não disparar nada. Trade-off aceito: a marca é gravada antes do envio, então um e-mail que falha (cota esgotada, Resend fora) não é reenviado. Antes, o reinício fazia papel de retry por acidente. Isso segue a linha de "Retry/fila de e-mail" abaixo: a falha fica no LOG.error (GlitchTip), e o pedido continua em Falha no painel. Preterido: manter a memória na RAM e aceitar o realerta no boot. O custo foi medido, e a cota é de todos os apps.

Consequências

  • Notificação só no 1XX terminal. 9XX reprocessável e transporte não geram e-mail (seguem o fluxo de reprocesso / GlitchTip) — evita ruído em falha transitória.
  • Superfície nova sem auth. A tela /destinatarios segue a postura atual (aberta atrás da rede, como /console e /dados); o hardening por login continua deferido para quando a Fase 2 trouxer auth.
  • Dependência externa nova (Resend) + segredo. RESEND_API_KEY e o remetente vêm de env; o domínio do remetente precisa ser verificado no Resend, senão o envio é recusado (tratado como best-effort).
  • O link /pedido/{code} do e-mail aponta para o painel colaborador (home pública + detalhe do pedido sem login) — uma feature separada (próxima linhagem). Até ela existir, o link pode não resolver; aceitável enquanto tudo está dormente.

Alternativas consideradas

  • SMTP (micronaut-email) em vez de provedor HTTP: rejeitada — preferência por API de provedor (Resend), menos infra de e-mail para operar.
  • Notificar todo estado de erro (ERRO/9XX incluídos): rejeitada — ruído; o pedido do usuário é sobre o irrecuperável, e os reprocessáveis têm caminho próprio no console.
  • Retry/fila de e-mail: fora de escopo — para um alerta operacional, best-effort com LOG.error (→ GlitchTip) basta; a falha de e-mail fica observável sem uma fila dedicada.
  • Fechar a tela com login agora (micronaut-security): rejeitada — escopo grande que valeria para todas as telas; segue a dívida já registrada, a resolver junto da auth da Fase 2.