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 porlower(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 —
ResendClientsobre ojava.net.http.HttpClientdo JDK (idioma da casa, espelhaIntegradorClient),POST /emailscomAuthorization: Bearer; um e-mail com todos os destinatários noto[]. O corpo trazcode,codRetorno,msgRetorno,chaveXadm+ link{base-url}/pedido/{code}. - Best-effort — se desabilitado, sem chave/remetente, sem destinatários, ou se o envio falhar, o
AlertaServicesó 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
msgRetornodo 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/consolemanté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 é ocontratos.cod_retornodo espelho — escrito pelo write-back e não tocado pelo push. O cliente do X-Adm só coleta000|9XX, então linha em1XXfica invisível para ele para sempre (caso real: pedido 260079421, três cliques, zero reenvios). O painel passou a chamarPOST /api/v1/xadm/retorno/pedido/reenfileirarno 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/consolemanté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 pegarERRO_XADMe fica só com oERROde 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 asTRAVADAa cada minuto. Essa marca vivia na RAM doReconciliacaoJob, 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 empied_pedido.alerta_cod_retorno(V13), e o envio só acontece quando oUPDATE … WHERE alerta_cod_retorno IS DISTINCT FROM :codRetornograva 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 dosERRO_XADM1XX 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 noLOG.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
/destinatariossegue a postura atual (aberta atrás da rede, como/consolee/dados); o hardening por login continua deferido para quando a Fase 2 trouxer auth. - Dependência externa nova (Resend) + segredo.
RESEND_API_KEYe 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.