Pular para conteúdo

Implantação — integração PIED → X-Adm (Maxsul)

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

O que é

Guia de implantação e operação da vertical que leva pedidos, clientes e produtos da PIED (sistema do cliente Maxsul) ao X-Adm, via Integrador. Cobre o que subir, com que variáveis, como conferir que subiu e — o que mais importa no pior dia — como desligar.

O desenho (arquitetura, fluxo de captura, máquina de status, modelo de dados) é do livro: Documentação Completa. Aqui não se re-digita. A referência de cada variável, uma a uma, é de Configuração; aqui está o que o operador precisa na ordem em que implanta.

Quando usar

  • Ao provisionar o maxsul-pied em produção (Coolify) pela primeira vez.
  • Ao atualizar a versão implantada.
  • Ao ligar a Fase 2 (a integração nasce desligada — decisão 0001).
  • Ao precisar parar o envio ao X-Adm sem derrubar a captura (§ Reversão).

Pré-requisitos

  • Postgres compartilhado com o Integrador provisionado e acessível.
  • DNS de pied.maxsul.xadm.biz apontando para o Coolify.
  • Integrador implantado e alcançável, com o INTEGRADOR_API_TOKEN à mão.
  • Token da API da PIED e acesso ao painel dela (o webhook é cadastrado à mão lá).
  • Secrets do §Variáveis à mão (cofre da X-Adm).

Topologia de operação

O que sobe, onde, e o que cada peça precisa. O porquê desta topologia (por que captura raw primeiro, por que caminho robusto) está no livro e na decisão 0002.

%% caption: O que se implanta — um serviço nosso, um banco compartilhado, três externos
flowchart TB
    PIED["API + Webhook da PIED<br/>EXTERNO — fora do nosso controle"]
    APP["maxsul-pied<br/>pied.maxsul.xadm.biz<br/>(recurso Coolify)"]
    DB[("Postgres<br/>COMPARTILHADO com o Integrador")]
    INT["Integrador<br/>(outro app X-Adm)"]
    RESEND["Resend<br/>EXTERNO — e-mail de alerta"]

    PIED -->|"POST /webhook/pied"| APP
    APP -->|"poll GET (token)"| PIED
    APP --> DB
    APP -->|"PUT/DELETE /api/v1/xadm (Bearer)"| INT
    APP -->|"POST /emails"| RESEND
    INT --> DB
Peça Onde Auth Quem é dono
maxsul-pied recurso Coolify, pied.maxsul.xadm.biz nenhuma — telas e rotas abertas, atrás da rede este repo
Postgres compartilhado com o Integrador usuário por app Integrador (dono do schema)
Integrador outro recurso Coolify Bearer em /api/v1/** repo integracao/integrador
API/Webhook da PIED externo token (poll) / segredo (webhook) PIED (terceiro)
Resend externo API key terceiro

Duas instâncias no mesmo banco (jar + native) são seguras

O app é scheduler/poller; sob 2 instâncias lado a lado (meta 50/50 jar/native) os @Scheduled são serializados por advisory lock — só uma varre por tick, a outra coalesce, e o lock solta sozinho na queda (HA/failover). Detalhe e nomes de lock em decisão 0014. A entrega M2M já é N-instâncias-safe por conta própria. O pipeline.yml já builda e deploya os dois flavors (build_jar + build_native, build.targets=['jar','native']; decisão 0015). O native está no ar desde a v0.7.0 (2026-09-02): o recurso Coolify native existe e todo release desde então sai com o trailer Deploy: jar,native. O boot do binário native (revela config de reflexão/recurso faltando, que o build não pega) é gateado pelo HEALTHCHECK do deploy — container native que não sobe não é promovido pelo Coolify.

SENTRY_DSN no native é ENV do recurso, não ponte no Dockerfile. Só o flavor jar lê o app.json com jq no entrypoint; o Dockerfile.native não tem essa ponte — é o desenho do template da casa (binário sem sh/jq no ENTRYPOINT). Então o DSN tem de estar cadastrado como variável de ambiente no recurso Coolify native; sem ela, o native roda cego no GlitchTip — mesmo com features.glitchtip.enabled: true no app.json.

O banco é compartilhado — o Flyway deste app usa histórico próprio

Os dois apps versionam migrations sobre o mesmo schema public e ambos têm V1/V2/…. Por isso o maxsul-pied grava em flyway_schema_history_pied e o Integrador fica com a tabela default (application.yml). Apontar este app para a tabela default faria os dois históricos colidirem.

Contrato de entrada

Quem alimenta o sistema é a PIED, por duas vias independentes: o webhook (push) e o poll da API REST (pull, backstop). A forma completa é de API da PIED — não se copia aqui.

O webhook não é registrado por código: é cadastrado à mão no painel da PIED (Configurações → API e Webhooks → Webhooks), apontando para https://pied.maxsul.xadm.biz/webhook/pied.

A confirmar com a PIED — como o segredo do webhook viaja

O maxsul-pied valida o segredo no header X-Pied-Secret (WebhookConfig.HEADER_SEGREDO). Esse nome é suposição nossa: a PIED nunca confirmou a forma de envio (anexo do webhook §Segurança). Fixture nossa não confirma contrato de terceiro.

Efeito de errar: se a PIED mandar o segredo com outro nome de header, todo POST responde 401 e nada é gravado — e a perda é silenciosa: o log é warn, não error, então não sobe alarme no GlitchTip. O poll (se ligado) mascara, trazendo os mesmos pedidos atrasados; com o poll desligado (o default), a captura simplesmente para.

Enquanto não confirmado: opere com PIED_WEBHOOK_SECRET vazio — os headers recebidos ficam em pied_webhook.headers para diagnóstico, e é assim que se descobre o nome real. Atenção à assimetria: segredo errado falha fechado (401, nada entra); segredo vazio falha aberto — aceita qualquer POST sem validar (WebhookController).

O contrato de saída (o que este app grava no X-Adm) é fato do Integrador, importado do raw/ dele: API do Integrador.

Superfície de operação

O que o operador olha, depois de implantado (tudo aberto, sem login — ver § Reversão):

  • https://pied.maxsul.xadm.biz/ — painel do colaborador Maxsul: pedidos por status amigável.
  • .../console — console do operador: máquina de status, e os botões que agem (Enviar, Reenviar, Enviar todos, Atualizar, Puxar da PIED).
  • .../captura e .../captura/json — a captura crua, para diagnóstico.
  • .../dados/* — browser read-only das tabelas pied_*.
  • .../destinatarios — quem recebe o e-mail de erro terminal.
  • .../health → {"status":"UP","versao":"X.Y.Z"}.

Variáveis de ambiente

Um recurso só (maxsul-pied). Segredos via secrets do Coolify, nunca no repo. Referência campo a campo em Configuração; aqui, o que decide a implantação.

Toda mudança de env exige restart do container: não há @Refreshable nem /refresh no app — os @ConfigurationProperties são preenchidos na criação do contexto. Isto vale inclusive para os toggles de § Reversão.

maxsul-pied — pied.maxsul.xadm.biz

Variável Valor / descrição
CLIENTE Maxsul (nome no cabeçalho das telas)
PORT 8080 (default)
DATASOURCES_DEFAULT_URL JDBC do Postgres compartilhado com o Integrador
DATASOURCES_DEFAULT_USERNAME usuário deste app
🔑 DATASOURCES_DEFAULT_PASSWORD segredo
PIED_BASE_URL https://backend-pied-prod.piedadmin.com.br/api (default)
🔑 PIED_TOKEN segredo — token da API da PIED. Vazio: o poll não roda (só um warn)
PIED_POLL_HABILITADO default false — ligar exige decisão consciente
PIED_POLL_INTERVALO 6h
PIED_WEBHOOK_HABILITADO default true
🔑 PIED_WEBHOOK_SECRET segredo — ver § Contrato de entrada (vazio = aceita qualquer POST)
PIED_INTEGRACAO_HABILITADA default false — a Fase 2 nasce desligada (decisão 0001)
PIED_INTEGRACAO_MODO PRODUCAO (default) ou STAGING — decisão 0004
PIED_INTEGRACAO_INTERVALO 5m (transform/push)
PIED_INTEGRACAO_RECONC_INTERVALO 5m (reconciliação)
INTEGRADOR_BASE_URL base do Integrador. Vazio: toda saída vira ERRO tratado
🔑 INTEGRADOR_API_TOKEN segredo — ver § Pares
PIED_RETENCAO_HABILITADO default true — expurgo do raw
PIED_RETENCAO_DIAS 7
PIED_ALERTA_HABILITADO default false
🔑 RESEND_API_KEY segredo
PIED_ALERTA_REMETENTE remetente do e-mail de alerta
PIED_ALERTA_BASE_URL URL pública do app, p/ o link no corpo do e-mail
SENTRY_DSN ENV do recurso native, com o valor de features.glitchtip.dsn do docs/app.json — ver o aviso do native acima. Sem ela o app roda cego no GlitchTip. GLITCHTIP_DSN não vale (a xadm-comum-web 0.10.0 lê só o SENTRY_DSN)

Pares que têm de casar

Valor que existe nos dois lados e diverge em silêncio é falha de implantação que só aparece em produção. Sintomas conferidos no código:

Ponta A (aqui) Ponta B Sintoma se divergirem
INTEGRADOR_API_TOKEN INTEGRADOR_API_TOKEN do Integrador PUT responde 401/403, aborta sem retentar → pedido vira ERRO com "Autenticação falhou no Integrador". Visível no console e reenviável. Mas na reconciliação vira FALHA_CONSULTA e o pedido segue ENVIADO — só um warn: a reconciliação trava em silêncio
PIED_WEBHOOK_SECRET segredo cadastrado no painel da PIED 401, nada gravado, sem alarme (é warn) — ver § Contrato de entrada
🔑 PIED_TOKEN token do painel da PIED Vazio → poll não roda (warn). Inválido → LOG.error com stacktrace (sobe ao GlitchTip) e a rodada segue nas outras entidades
INTEGRADOR_BASE_URL URL real do Integrador Vazio → IntegradorException "Integrador não configurado" antes de tocar a rede → pedido vira ERRO

Passos de implantação

  1. Banco. Garantir o Postgres compartilhado acessível; criar o usuário deste app. O Flyway roda no start e usa flyway_schema_history_pied (§ Topologia).
  2. App. Deploy deste repo no Coolify em pied.maxsul.xadm.biz; setar as ENV do §Variáveis. Deixe PIED_INTEGRACAO_HABILITADA=false nesta etapa — sobe capturando só (decisão 0001).
  3. Webhook na PIED. Cadastrar https://pied.maxsul.xadm.biz/webhook/pied à mão no painel da PIED, com os gatilhos budget.*/order.*. Segredo: ver o aviso do § Contrato de entrada.
  4. Conferir a captura (§ Verificação, passos 1–3) antes de ligar a Fase 2.
  5. Ligar a Fase 2, quando for a hora: PIED_INTEGRACAO_HABILITADA=true + INTEGRADOR_BASE_URL
  6. INTEGRADOR_API_TOKEN + restart. Considere PIED_INTEGRACAO_MODO=STAGING na estreia — a entrega fica represada em NA_FILA e só sai por clique no console (decisão 0004).
  7. Alerta (opcional): PIED_ALERTA_HABILITADO=true + RESEND_API_KEY + remetente, e cadastrar os destinatários em /destinatarios.

Verificação

Todo passo declara o resultado esperado, conferido no código — não no que parece razoável.

  1. GET /health → {"status":"UP","versao":"X.Y.Z"}. É "UP", maiúsculo (status canônico da casa) — o shape é plano {status, versao} (norma da casa), servido pela lib xadm-comum-web.
  2. Webhook desligado (PIED_WEBHOOK_HABILITADO=false) → POST /webhook/pied responde 503 com application/problem+json e code: WEBHOOK_DESABILITADO, e não grava nada (o guard vem antes do insert em WebhookController).
  3. Webhook ligado → um evento real da PIED aparece em SELECT evento, recebido_em FROM pied_webhook ORDER BY recebido_em DESC LIMIT 10;, e em /captura. As credenciais dos headers são gravadas mascaradas (guardado por WebhookControllerTest).
  4. Segredo errado → 401 + code: SEGREDO_INVALIDO, nada gravado, e nenhum evento no GlitchTip (é warn). Se você esperava alarme, ele não vem — é este o ponto cego.
  5. Fase 2 ligada → um pedido elegível sai de CAPTURADO e chega a ENVIADO/CONFIRMADO no /console. Em MODO=STAGING ele para em NA_FILA e só sai por clique — se sair sozinho em STAGING, o modo não está valendo (confira que houve restart).
  6. Idempotência. Reenviar o mesmo estado (sem mudar valores) → o envio é pulado, não duplicado: o PushService compara o content_hash do payload com o gravado e só reescreve em mudança real. (Aqui a idempotência é por conteúdo, não por timestamp de escrita: repetir o envio de estado idêntico não gera linha nova. Se gerar, o content-hash quebrou.)

Pedido travado no X-Adm — como conferir

O X-Adm rejeita filho sem pai de forma terminal (cod_retorno 1XX não volta sozinho), e até 2026-09 este app só olhava o retorno do contratos — um pedido podia constar importado com os itens recusados. Hoje a reconciliação fecha pela remessa inteira (decisão 0021).

onde olhar o que significa
painel, bucket "Falha ao importar" o pedido tem remessa TRAVADA — alguma linha em 1XX
msg_retorno do pedido a causa: é a mensagem do item que não está bloqueado
GET /api/v1/integracao/remessas?origem=INT-PIED&status=TRAVADA no Integrador a lista completa, com tabela, codRetorno e bloqueado de cada item

Ler o bloqueado importa. Item bloqueado é colateral de um pai morto, não a causa: no 260079421 o contrato levou 120 "Cliente não cadastrado" e o item levou 120 "Pedido não cadastrado" — duas mensagens, uma causa. Corrigir o item não resolve nada; corrigir o cliente resolve os dois.

Como se resolve uma Falha. Lançando à mão no X-Adm o que o diagnóstico do detalhe aponta em "NÃO gravado" e marcando Importado manualmente — o X-Adm é caminho só de ida, e o que ele gravou não volta (não excluir nada que esteja em "Gravado"). Dado novo da PIED não devolve à fila um pedido em Falha; só o ERRO de envio volta sozinho.

Quando clicar Reimportar. Só quando o botão aparece: parte do pedido ainda está pendente, travada por um pai rejeitado (ex. cliente não cadastrado) — aí lançar à mão duplicaria, e o "Importado manualmente" responde 409. Corrija o pai (no X-Adm ou na PIED) e clique. O botão enfileira; o re-arme da linha no espelho sai junto com a entrega, não no clique — se saísse antes, o cliente do ERP poderia coletar a linha com o dado velho.

Se o pedido fica ENVIADO sem remessa por mais de 30 min, o log emite um WARN — é sinal de que o push não virou manifesto no Integrador, e o app não muda o status por conta própria.

Smoke pós-deploy — quem confere isso sozinho

Os passos 1–6 acima são a conferência manual. Desde a constituição 1.4.1 o pipeline roda um smoke contra produção depois de todo deploy (decisão 0020, norma smoke-producao), porque o POST /api/ci/deploy é assíncrono: sem ele o CI ficava verde antes de o container novo servir.

camada o que afirma
identidade o commit do /health é o sha desta entrega (não o do container velho)
rotas as seis rotas de docs/app.json → smoke.routes respondem 200, sem seguir redirect
log-guarantee nenhuma exceção nova no GlitchTip desde a entrega (reincidência avisa, não reprova)
veredito reprovou → reverte para <imagem>:<sha anterior>-<target>, confirma, e fica vermelho

Duas consequências operacionais:

  • GLITCHTIP_API_TOKEN (read-only) tem de existir nos secrets do repo no GitHub. O app.json declara smoke.glitchtip, e declarar sem o secret reprova o smoke — de propósito: o modo de falhar caro não é o vermelho, é o verde provando menos do que diz.
  • Não pode podar a tag imutável <imagem>:<sha>-<target> que está no /health de um recurso em produção: ela é o alvo do rollback. Retenção mínima: as 10 últimas por (imagem, target).

O rollback do smoke troca a imagem. É coisa diferente da tabela abaixo, que para o envio ao X-Adm com o mesmo container de pé.

Reversão

Parar o envio ao X-Adm sem derrubar a captura. Leia a tabela antes de agir: este sistema tem mais de uma via de saída, e o toggle que parece o kill switch não corta todas.

PIED_INTEGRACAO_HABILITADA=false NÃO é kill switch da saída

O toggle é consultado em três pontos — TransformJob, ReconciliacaoJob e IntegradorClientFactory (este só decide logar um WARN). Nenhum botão do console o consulta, e o PushService não consulta toggle nenhum. Com a integração "desligada", quem abrir /console e clicar Enviar envia ao X-Adm — e o /console não tem autenticação.

Caminho Efeito
Parar o container (Coolify) Para tudo. Mais simples e garantido — prefira este
PIED_INTEGRACAO_HABILITADA=false Para o TransformJob (gate, normalização, push automático) e a reconciliação agendada. ⚠️ Não para os botões do console: Enviar, Enviar todos, Reenviar e Atualizar seguem saindo
INTEGRADOR_BASE_URL="" Corta toda saída, inclusive os botões — falha antes da rede. Custo: cada tentativa marca o pedido como ERRO (reversível depois por Reenviar)
PIED_INTEGRACAO_MODO=STAGING ⚠️ Não é kill switch. Para só o drenarFila(); o gate → NA_FILA, a normalização, a reconciliação inteira e todos os botões continuam
PIED_WEBHOOK_HABILITADO=false Para a entrada por webhook (503, sem gravar). ⚠️ Não para o poll (se ligado) nem os botões "Puxar da PIED"/"Sincronizar clientes"
PIED_POLL_HABILITADO=false Para o poll agendado. ⚠️ Não para o webhook nem os botões de captura sob demanda — o CapturaSobDemandaService ignora os toggles e bate na PIED do mesmo jeito
PIED_ALERTA_HABILITADO=false Para o e-mail automático de erro terminal. ⚠️ Não para o "Enviar teste" da tela /destinatarios, que ignora o toggle de propósito

Corte garantido sem parar o container: PIED_INTEGRACAO_HABILITADA=false + INTEGRADOR_BASE_URL="" + restart. O primeiro para o automático; o segundo fecha a porta dos botões. Só o primeiro não basta enquanto o console estiver alcançável.

Em qualquer caso, a captura pode seguir rodando: o raw continua entrando e o atraso de entrega sai quando religar. Nada se perde por desligar a saída — é para isso que a captura vem primeiro (decisão 0001).

Reverter versão: redeploy da tag anterior (Coolify). As migrations são aditivas.