Pular para conteúdo

Configuração — env vars e toggles

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-15

Toda a configuração é por variável de ambiente (no Coolify, nas envs do recurso). Não há arquivo de configuração em produção; o .env.example na raiz do repo documenta o conjunto.

Banco de dados

Variável Obrigatória Uso
DATASOURCES_DEFAULT_URL Sim JDBC do Postgres 18 (ex.: jdbc:postgresql://host:5432/maxsul_pied).
DATASOURCES_DEFAULT_USERNAME Sim Usuário do banco.
DATASOURCES_DEFAULT_PASSWORD Sim Senha do banco.

As migrações (Flyway) rodam automaticamente no start — a V1__landing_pied.sql cria as três tabelas landing. Requer Postgres 18+ (uuidv7() nativo).

PIED

Variável Default Uso
PIED_BASE_URL https://backend-pied-prod.piedadmin.com.br/api Base da API REST da PIED.
PIED_TOKEN (vazio) Token Bearer fornecido pela PIED. Sem ele, o poll loga aviso e não roda.
PIED_WEBHOOK_SECRET (vazio) Segredo compartilhado do webhook (header X-Pied-Secret). Vazio = aceita sem validar (MVP).

Toggles

Variável Default O que liga
PIED_WEBHOOK_HABILITADO true Recepção do POST /webhook/pied. Desligado → 503.
PIED_RETENCAO_HABILITADO true Job de limpeza do landing raw (pied_rest/pied_webhook).
PIED_RETENCAO_DIAS 7 Janela de retenção do raw — linhas mais antigas são apagadas. O dado útil vive nas tabelas normalizadas (guardam o próprio payload).
PIED_RETENCAO_INTERVALO 24h Intervalo do job de retenção (Duration).
PIED_WATCHDOG_HABILITADO true Vigia o silêncio do webhook: alerta no GlitchTip por duas réguas, a que vier primeiro. Modo de falha: a PIED desativa o webhook após uma falha de entrega (404 ou timeout) e não volta sozinha — ver recadastro do webhook, decisão 0016 e 0023.
PIED_WATCHDOG_LIMITE_MIN_COMERCIAL 45 Régua rápida: minutos de silêncio dentro do comercial (seg–sex, faixa abaixo) antes de alertar. Calibrado no histórico real — o maior silêncio legítimo medido foi 37 min. 0 desliga só esta régua.
PIED_WATCHDOG_HORA_INICIO 8 Abertura do comercial da Maxsul (hora local, 0–23).
PIED_WATCHDOG_HORA_FIM 18 Fechamento do comercial (hora local, 1–24).
PIED_WATCHDOG_LIMITE_HORAS 24 Rede de fora do horário: limite de horas úteis (dia útil inteiro, seg–sex; sáb/dom não contam) sem webhook antes de alertar.
PIED_WATCHDOG_INTERVALO 5m Intervalo do job de verificação (Duration). Tem que ser bem menor que a régua comercial.
PIED_GATE_PRIMEIRA_CAPTURA_DESDE (vazio) Resgate da 1ª captura já paga (decisão 0023): data de ativação AAAA-MM-DD. Pedido visto já pago na primeira captura — webhook perdido num apagão, poll REST o alcança depois de pago — vai direto para NA_FILA se foi criado depois desta data. Vazio desliga (default): só o Enfileirar do console resgata. A data é o piso que impede o deploy de despejar legado.
PIED_GATE_PRIMEIRA_CAPTURA_JANELA_DIAS 7 Idade máxima do orderCreated para o pedido contar como novo. Cobre apagão que atravessa o fim de semana sem alcançar o backlog antigo da PIED.
PIED_POLL_HABILITADO false Job agendado de poll REST (3 entidades → pied_rest).
PIED_POLL_INTERVALO 6h Intervalo entre rodadas do poll (Duration: 6h, 1h, 30m...). Produtos/clientes re-varrem tudo a cada rodada (sem cursor incremental) e o landing (pied_rest) é append-only sem dedup — intervalo curto incha o raw; 6h dá 4 backstops/dia (o webhook cobre o tempo-real).
PIED_POLL_LIMITE_PAGINA 50 Tamanho de página nas chamadas REST (máximo aceito pela PIED: 50).
PIED_POLL_ENRIQUECIMENTO true Varre produtos/clientes na rodada periódica (re-varre tudo — sem cursor incremental). Clientes/produtos chegam por webhook (order.* + company.* que traz InscEst); com false, o poll periódico fica só nos pedidos (backstop) e para de inflar o landing. O backfill sob demanda ("Sincronizar clientes") segue funcionando.
PIED_INTEGRACAO_HABILITADA false Fase 2 (transform → push ao Integrador). Ligado: o TransformJob normaliza, aplica o gate e (em PRODUCAO) envia; a reconciliação do retorno roda. Desligado: só captura raw.

Fase 2 — transform + push ao Integrador

Variável Default Uso
PIED_INTEGRACAO_MODO PRODUCAO PRODUCAO envia automático assim que o pedido fica elegível; STAGING deixa NA_FILA até o clique no /console (decisão 0004).
PIED_INTEGRACAO_INTERVALO 5m Intervalo do job de normalização+gate (+push em PRODUCAO). Duration.
PIED_INTEGRACAO_RECONC_INTERVALO 5m Intervalo do job de reconciliação do retorno do X-Adm. Duration.
MENSAGERIA_RELAY_ENABLED true Agendador do relay da lib (xadm-mensageria), que drena a fila de saída a cada 2 min. Desligar para a entrega ao Integrador (a fila acumula; nada se perde). Nasceu false enquanto a lib não elegia réplica e um job local fazia a guarda — histórico na decisão 0014.
MENSAGERIA_RELAY_CLUSTER_LOCK true Eleição de réplica por tick (pg_try_advisory_lock), desde a lib 0.3.4. Desligar devolve a entrega dobrada sob as duas instâncias (jar + native) — medido, 92 PUT para 52 payloads distintos em 24h. Escotilha de diagnóstico; nunca em produção.
INTEGRADOR_BASE_URL (vazio) Base do Integrador (ex.: https://integrador.xadm.biz). Vazio com integração habilitada: log WARN no startup e cada envio/reconciliação vira ERRO tratado ("Integrador não configurado") — não crash. Configure para operar.
INTEGRADOR_API_TOKEN (vazio) Bearer estático do Integrador (Authorization: Bearer …).

Cada rodada do job começa pelo enriquecimento sob demanda: havendo pedido NA_FILA sem invoice (capturado só por webhook), ele busca a janela desse pedido no REST e a normalização da mesma rodada o destrava — sem isso a espera seria a do poll (PIED_POLL_INTERVALO, 6h). Exige PIED_TOKEN; sem preso não faz chamada nenhuma. Desenho, limites e diagnóstico do preso residual na decisão 0017.

O gate de envio é a transição de pagamento (payment.status → received), detectada por evento no upsert de pied_pedido (Maxsul, 2026-08-07). pied.integracao.estados-venda não governa mais o envio — sobrou só como filtro do "Puxar da PIED" (captura sob demanda do console). O de-para de forma de pagamento por pied.integracao.transform.class-forma (default financiamento→99). Só se ajustam via application.yml/env estruturada; os defaults cobrem o caso comum (mapeamento, pendências).

Como habilitar o poll

  1. Obter o token de produção da PIED e definir PIED_TOKEN.
  2. Definir PIED_POLL_HABILITADO=true (e o intervalo desejado em PIED_POLL_INTERVALO).
  3. Redeploy/restart. A primeira rodada ocorre ~1 minuto após o start; pedidos começam com carga completa (cursor vazio) e as rodadas seguintes usam ?lastUpdateAfter a partir de pied_cursor.
  4. Conferir: SELECT entidade, count(*) FROM pied_rest GROUP BY 1; e o cursor em pied_cursor.

Como habilitar a integração (fase 2)

  1. Definir INTEGRADOR_BASE_URL e INTEGRADOR_API_TOKEN (Bearer estático do Integrador).
  2. Confirmar o de-para e o conjunto de estados do gate com a Maxsul (pendências P1/P2); ajustar pied.integracao.estados-venda/transform.class-forma se preciso — os defaults cobrem o comum.
  3. Recomendado no 1º deploy: PIED_INTEGRACAO_MODO=STAGING — os pedidos elegíveis ficam NA_FILA e você confere e solta pelo /console (puxar → conferir de-para → enfileirar → enviar → atualizar) antes de deixar automático. Validado, mudar para PRODUCAO.
  4. PIED_INTEGRACAO_HABILITADA=true + redeploy. Não há despejo do histórico: o gate é a transição de pagamento (só entra o pedido que passa a received num update depois do enable; o backlog já-pago não re-transiciona).

Alerta de erro terminal do pedido (e-mail via Resend)

Quando um pedido é rejeitado de forma irrecuperável pelo X-Adm na reconciliação (ERRO_XADM com codRetorno 1XX terminal), os contatos cadastrados na tela /destinatarios recebem um e-mail via Resend (decisão 0005). Erros reprocessáveis (ERRO de transporte, ERRO_XADM 9XX) e de sistema (esses vão ao GlitchTip) não disparam. O envio é best-effort: se falhar, apenas loga (LOG.error → GlitchTip) e a reconciliação segue. Como depende da reconciliação, o alerta fica dormente até a Fase 2 ser ligada.

Um e-mail por pedido e código de retorno. O último código alertado fica gravado no pedido (pied_pedido.alerta_cod_retorno), então reiniciar o app (deploy, reboot do servidor) não reenvia os alertas de pedidos que já estavam travados. O pedido volta a alertar quando o código muda, quando ele é confirmado e depois trava de novo, ou quando o operador o reenfileira.

Variável Default Uso
PIED_ALERTA_HABILITADO false Liga o envio do alerta. Desligado (ou sem api-key/remetente) = no-op silencioso.
RESEND_API_KEY (vazio) Chave da API do Resend. Vazio = não envia.
PIED_ALERTA_REMETENTE (vazio) Remetente (from) do e-mail. O domínio precisa estar verificado no painel do Resend, senão o envio é recusado.
PIED_ALERTA_BASE_URL (vazio) URL pública do app p/ montar o link /pedido/{code} no corpo do e-mail (ex.: https://pied.maxsul.xadm.biz).

Para ativar: (1) criar a conta/domínio no Resend e verificar o domínio do remetente; (2) definir RESEND_API_KEY, PIED_ALERTA_REMETENTE e PIED_ALERTA_BASE_URL; (3) cadastrar os destinatários em /destinatarios; (4) PIED_ALERTA_HABILITADO=true + redeploy.

Testar o envio: na tela /destinatarios, cada contato tem um botão "Enviar teste" que dispara um e-mail de teste àquele endereço. O teste ignora o toggle PIED_ALERTA_HABILITADO (é ação manual), mas exige RESEND_API_KEY + remetente — sem eles, a tela avisa "Configure RESEND_API_KEY…". Serve de smoke-test pós-deploy do caminho do e-mail antes de ligar o alerta automático.

Console operador (/console)

Superfície única de operação da Fase 2 (absorveu o antigo /inspetor e /fila): dirige cada pedido pela máquina de estados real (pied_pedido.status: CAPTURADO→NA_FILA→ENVIANDO→ENVIADO→CONFIRMADO | ERRO_XADM | ERRO).

Rota O que faz Modo
GET /console[?status=] Lista filtrável por status, com stepper. ambos
GET /console/{code} Detalhe: bloco 1 (origem), 2 (de-para campo a campo), 3 (status+retorno). Read-only. O bloco 2 é dry-run sobre o payload atual — pode divergir do que foi efetivamente entregue, se o payload evoluiu após o envio (caso 260081441). ambos
POST /console/puxar-pied?n={1\|5} Puxa 1/5 pedidos finalizados da PIED → CAPTURADO (só elegíveis). STAGING
POST /console/{code}/enfileirar Gate manual CAPTURADO→NA_FILA — exige pagamento recebido (payment_status='received'); recusa não-pago. Cobre o pedido que veio já-pago na 1ª captura. STAGING
POST /console/{code}/enviar · POST /console/enviar-todos Envia 1 / todos os NA_FILA ao Integrador. STAGING
POST /console/{code}/atualizar Reconcilia o retorno do X-Adm agora (ENVIADO→CONFIRMADO/ERRO_XADM). ambos
POST /console/{code}/reenviar Reenvia ERRO/ERRO_XADM 9XX; barra o 1XX terminal. ambos
POST /console/{code}/renormalizar Re-normaliza do próprio payload (sem chamar a PIED). ambos
POST /console/sincronizar-clientes Varre v1/companies → preenche InscEst. ambos

Em STAGING o console é interativo (o pedido espera NA_FILA até o clique); em PRODUCAO é monitor (o pipeline anda sozinho; ficam só as ações de exceção — Atualizar/Reenviar).

⚠️ Aberto (sem auth, como o /captura) — expõe PII e tem ações ao vivo (push ao X-Adm, fetch na PIED). O gate por login das rotas de ação está deferido (hardening separado; nenhuma regressão vs. o antigo /fila, que já era aberto) — proteger antes de expor fora da rede (decisão 0004, L8).

Webhook — cadastro do lado PIED (manual)

O registro do endpoint não é automático: é feito à mão no painel da PIED (Configurações → API e Webhooks → aba Webhooks → Adicionar):

  1. Endpoint: https://pied.maxsul.xadm.biz/webhook/pied.
  2. Gatilhos: eventos de orçamento e pedido (budget.*, order.*) — e o que mais o painel oferecer: o serviço aceita qualquer evento.
  3. Autenticação: definir o segredo e replicá-lo em PIED_WEBHOOK_SECRET. Enquanto a forma exata de envio não for confirmada com a PIED, pode-se operar sem segredo (vazio) — os headers recebidos ficam registrados em pied_webhook.headers para diagnóstico.
  4. Conferir: SELECT evento, recebido_em FROM pied_webhook ORDER BY recebido_em DESC LIMIT 10;

Health check

GET /health → {"status":"UP","versao":"X.Y.Z"} (versão da release, lida do version.properties gerado no build; servido pela lib xadm-comum-web). Usado pelo healthcheck do container e pelo Coolify/Traefik.

Observabilidade — GlitchTip (erros)

Todo log ERROR — inclusive exceções não-tratadas do Micronaut — sobe para o GlitchTip via SentryAppender no logback.xml, sem instrumentar o domínio. Configurado pelo /xadm-setup (features.glitchtip no app.json).

Variável Origem Uso
SENTRY_DSN ENV do recurso Coolify no native (o alvo de deploy): o binário não tem sh nem jq no ENTRYPOINT. Só a imagem jar (Dockerfile) lê docs/app.json (features.glitchtip.dsn) via jq no entrypoint. DSN do projeto no GlitchTip, com o mesmo valor do app.json. Só SENTRY_DSN: a xadm-comum-web 0.10.0+ não lê GLITCHTIP_DSN e, com só ele, sobe com o error tracking desligado e WARN no boot. Vazio/ausente = no-op (dev/local).

O DSN é client-grade (público) — por isso vive no app.json versionado, não é segredo. O SENTRY_AUTH_TOKEN (upload de sourcemap) não se aplica a backend JVM (não há sourcemap).

Smoke-test pós-deploy

POST /test/glitchtip emite um erro de propósito para confirmar que o DSN chega ao GlitchTip sem esperar um erro real:

curl -s -X POST https://pied.maxsul.xadm.biz/test/glitchtip | jq .
# → {"status":"enviado", ...}  e o evento aparece no projeto maxsul-pied no GlitchTip

⚠️ Aberto (sem auth — micronaut-security vem transitivo da lib xadm-comum-web mas está desligado via micronaut.security.enabled=false), no mesmo espírito do /captura. Fecha na Fase 2: ligar o filtro + gate por login, ou remoção.

Diagnóstico da captura (/captura)

Superfície de debug do que foi capturado da PIED, sem acesso direto ao banco (que só existe dentro do servidor Coolify) — o análogo da tabela requests do Integrador.

Rota O que faz
GET /captura[?fonte=] Tela HTML: timeline unificada webhook + páginas REST (mais recente primeiro). ?fonte=rest\|webhook\|ambos filtra a fonte (default ambos; valor inválido → ambos).
GET /captura/{tipo}/{id} Payload cru (pretty) de uma captura; {tipo} = webhook|rest.
GET /captura/json Dump JSON agregado das 3 tabelas (pied_webhook, pied_rest, pied_cursor) com o payload aninhado, para colar de uma vez. ?limite=N por tabela (default 1000, teto 5000).
# no browser: https://pied.maxsul.xadm.biz/captura  → clica numa linha p/ ver o raw
curl -s https://pied.maxsul.xadm.biz/captura/json | jq .webhook

⚠️ Aberto e expõe PII (CNPJ, contatos) — sem auth por enquanto, diagnóstico atrás da rede. O gate por login fica deferido para um hardening separado (mesmo espírito do console; decisão 0004).

Telas e navegação (UI)

As telas são server-rendered (JTE — templates compilados, decisão 0025) com o kit de UI da casa (engenharia/java-micronaut §UI): o kit/layout.jte e o public/css/custom-theme.css são templates rastreados — vêm do repo central e não se editam por app (a /xadm-docs re-deriva quando a identidade muda). A composição inverte o antigo Thymeleaf: a página chama @template.kit.layout(...) passando o conteúdo (e um navMenu opcional); o layout monta o <head>, o header com a marca e o bloco direito (versão/modo/chip de sessão, em kit/headerRight.jte).

O que é deste app vive em src/main/jte/: kit/navMenu.jte (menu de operador + os chips MODO/versão) e componentes/jsonCellScript.jte. O CSS de componente (timeline, tiles, buckets, stepper) é o /css/app.css, que o layout.jte linka direto no <head>. Os chips ficam no slot do menu porque o kit não tem slot de status no header; editá-lo por app faria o arquivo divergir do central para sempre.

O GlobalViewModel injeta em toda tela o contrato do kit — appNome/clienteXadm/authEnabled — mais modo/staging/versao. (authEnabled=false: sem chip de sessão, o app não tem auth.)

Duas superfícies, o mesmo layout: o painel do usuário final usa o fragment navbar (marca sem menu, o chrome limpo que a decisão 0006 pedia — o layout-painel.html que ela cita foi absorvido pelo kit e não existe mais); as telas internas (operador) usam o navbarComMenu.

Painel do usuário Maxsul (decisão 0006):

Rota O que faz
GET / Dashboard ("Importação de Pedidos PIED → X-Adm"): tiles de resumo por status amigável (Pedidos em aberto/Enviando ao X-Adm/Importados no X-Adm/Falha ao importar) + lista paginada de pedidos (clicável, 100/página, por last_update); ?status=<slug> filtra por bucket.
GET /pedidos/{code} Detalhe: timeline do status + dados (cliente, documento, datas, status na PIED = deal_status, pagamento = payment.status, pago em) + itens do pedido. code inexistente → 404 amigável.
POST /pedidos/{code}/importar Botão "Importar" (home): força o pedido em aberto já pago (CAPTURADO+received) para NA_FILA (gate manual).
POST /pedidos/{code}/dispensar Botão "Dispensar" (home): marca o pedido em aberto já pago como IMPORTADO_MANUAL (já lançado à mão no X-Adm), sem enviar.
POST /pedidos/{code}/reimportar Botão "Re-Importar" (detalhe), só em Falha ao importar e só com parte do pedido pendente (achada pelo diagnóstico, ou ?pendente=1 depois de um 409 do "Importado manualmente"): reenfileira; o re-arme das linhas 1XX no espelho sai junto com a entrega (PushEnviador).
POST /pedidos/{code}/importado-manual Botão "Importado manualmente" (detalhe e home), só em Falha ao importar — o caminho padrão da Falha: ERRO_XADM → IMPORTADO_MANUAL depois que o operador lançou à mão no X-Adm o que faltou. Observação obrigatória (observacao, até 500 caracteres) e operador opcional (operador, até 100, lembrado no navegador; na home os dois vêm de prompts) — o painel não tem login, então eles são o rastro de o que e quem; gravados em importacao_manual_obs (operador no fim) e _em, exibidos no detalhe. Fecha antes a remessa no Integrador (POST /api/v1/integracao/remessas/resolver-manual → RESOLVIDA_MANUAL) e só com o 200 marca aqui: parte do pedido ainda pendente de entrega (409: remessa ABERTA, ou TRAVADA com filho 000 bloqueado — não lançar à mão; aguardar ou corrigir e Re-Importar) ou Integrador fora deixam o pedido em Falha, com o motivo no flash. Ver API do Integrador §5.

Diagnóstico da falha (detalhe, só em Falha ao importar): consulta o Integrador ao vivo na abertura da página — a remessa viva (GET /api/v1/integracao/remessas, o que foi rejeitado, inclusive formulas) e o retorno por tabela (GET /api/v1/xadm/retorno/{tabela}, o que foi gravado e o código do contrato no X-Adm). Mostra o tipo do pedido (Kit + componentes × produtos avulsos), Gravado no X-Adm (definitivo — a integração não reenvia), NÃO gravado (com os dados do envio: quantidades, valores, frete, componentes com o nome, o produto do item do kit, a venda a prazo) e avisos "Possível" tirados do texto do X-Adm: produto-kit não criado (estoque 006 com mensagem de pedido e composição rejeitada), contrato em duplicidade (códigos diferentes no estoque e no contrato — o operador escolhe qual fica antes de lançar) e item com o valor no lugar do número do pedido. O "O que fazer" é um só — não excluir o que está em Gravado, lançar à mão o que falta, marcar Importado manualmente —, exceto com parte pendente (linha no espelho que não é 006 nem 1XX, o filho de um pai rejeitado): aí orienta corrigir e Re-Importar, e só então o botão aparece. Integrador fora do ar ou sem INTEGRADOR_BASE_URL → aviso no lugar do diagnóstico, a página não cai.

Telas internas (operador) — layout :: navbarComMenu(~{componentes :: navMenu}):

Menu Rotas
(home) GET /homedev Painel de cards com a contagem de linhas por tabela + atalhos (a antiga /).
Operação /console (ver acima).
Alertas /destinatarios — CRUD dos contatos que recebem o e-mail de erro terminal (ver acima).
Banco Browser read-only, uma tela por tabela pied_* (ver abaixo).
Captura /captura (timeline) e /captura/json (dump).
Sistema /health.

Browser do banco (/dados/*)

Uma tela de listagem read-only por tabela — colunas escalares + célula JSON clicável (clica → copia o payload formatado), teto ?limite=N (default 1000, teto 5000). Cobre as 7 tabelas pied_*:

Rota Tabela Observação
GET /dados/pedidos pied_pedido Link por linha abre o /console.
GET /dados/clientes pied_cliente Expõe PII; lista todas as linhas, incl. deletadas.
GET /dados/produtos pied_produto Lista todas, incl. deletados.
GET /dados/webhooks pied_webhook Payload + headers como célula JSON.
GET /dados/rest pied_rest Páginas cruas do poll.
GET /dados/cursor pied_cursor Cursor incremental + ultima_pagina.
GET /dados/estado pied_integracao_estado Singleton com o corte go-forward.

⚠️ Tudo aberto (sem auth) e expõe PII — mesma postura do /captura//console: diagnóstico atrás da rede, hardening por login deferido. É read-only (não escreve no banco); as ações de operação ficam só no /console.

Desenvolvimento local

./gradlew run sobe o Postgres de dev via docker-compose.dev.yml (porta 5433, para não colidir com o Postgres de dev do integrador) e ativa o perfil dev (application-dev.yml). Os testes não usam esse banco: provisionam o próprio Postgres via PostgresTestResource da casa (xadm-comum-teste, Testcontainers).