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-piedem 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.bizapontando 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)..../capturae.../captura/json— a captura crua, para diagnóstico..../dados/*— browser read-only das tabelaspied_*..../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¶
- 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). - App. Deploy deste repo no Coolify em
pied.maxsul.xadm.biz; setar as ENV do §Variáveis. DeixePIED_INTEGRACAO_HABILITADA=falsenesta etapa — sobe capturando só (decisão 0001). - Webhook na PIED. Cadastrar
https://pied.maxsul.xadm.biz/webhook/piedà mão no painel da PIED, com os gatilhosbudget.*/order.*. Segredo: ver o aviso do § Contrato de entrada. - Conferir a captura (§ Verificação, passos 1–3) antes de ligar a Fase 2.
- Ligar a Fase 2, quando for a hora:
PIED_INTEGRACAO_HABILITADA=true+INTEGRADOR_BASE_URL INTEGRADOR_API_TOKEN+ restart. ConsiderePIED_INTEGRACAO_MODO=STAGINGna estreia — a entrega fica represada emNA_FILAe só sai por clique no console (decisão 0004).- 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.
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 libxadm-comum-web.- Webhook desligado (
PIED_WEBHOOK_HABILITADO=false) →POST /webhook/piedresponde503comapplication/problem+jsonecode: WEBHOOK_DESABILITADO, e não grava nada (o guard vem antes do insert emWebhookController). - 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 porWebhookControllerTest). - 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. - Fase 2 ligada → um pedido elegível sai de
CAPTURADOe chega aENVIADO/CONFIRMADOno/console. EmMODO=STAGINGele para emNA_FILAe só sai por clique — se sair sozinho em STAGING, o modo não está valendo (confira que houve restart). - Idempotência. Reenviar o mesmo estado (sem mudar valores) → o envio é pulado, não
duplicado: o
PushServicecompara ocontent_hashdo 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. Oapp.jsondeclarasmoke.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/healthde 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.