Pular para conteúdo

Implantação — integração de produtos Thoms → WebStorm

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-16

O que é. Guia de implantação e operação da integração que leva preço e estoque dos produtos do X-Adm (ERP da Thoms) ao e-commerce do parceiro WebStorm, sem digitação manual. Cobre a topologia, o fluxo de eventos, o contrato REST que o X-Adm usa, o site de acompanhamento e as variáveis de ambiente de cada servidor/cliente.

Quando usar. Ao provisionar/atualizar os dois serviços em produção (Coolify) e ao integrar o cliente do X-Adm.

Pré-requisitos. Banco db_thoms (PostgreSQL) provisionado e compartilhado pelos dois apps; DNS dos dois subdomínios; secrets no Coolify; a feature 008 do integrador (coluna estoque.updated_at + webhook de saída) implantada.

1. Topologia de rede

Dois serviços HTTPS (um subdomínio cada, TLS no proxy do Coolify) sobre um banco compartilhado db_thoms. O e-commerce da WebStorm é externo.

%% caption: Topologia de rede — Thoms → WebStorm (banco db_thoms compartilhado)
flowchart TB
    XADM["X-Adm (ERP Thoms)<br/>cliente Java/Dart"]
    INT["Integrador<br/>https://int.thoms.xadm.biz<br/>(Java/Micronaut)"]
    TRAD["Tradutor WebStorm-ecom<br/>https://webstorm.thoms.xadm.biz<br/>(Java/Micronaut)"]
    DB[("db_thoms (PostgreSQL)<br/>replica estoque + webstorm_ecom_request")]
    WS["API WebStorm (e-commerce)<br/>https://api.thoms.com.br/sync/erp"]
    OP["Operador/Dev X-Adm<br/>(navegador)"]

    XADM -->|"PUT /api/v1/xadm (Bearer)"| INT
    INT -->|"grava/consolida"| DB
    INT -.->|"POST /webhook/estoque-mudou (Bearer, poke)"| TRAD
    TRAD -->|"lê estoque / grava webstorm_ecom_request"| DB
    TRAD -->|"POST /sync/erp (Bearer)"| WS
    OP -->|"telas de auditoria (atrás do Coolify)"| TRAD
  • Integrador e tradutor ficam na mesma rede com acesso ao db_thoms.
  • O webhook integrador→tradutor é opcional (otimização de latência); o tradutor também tem um sweep periódico como rede de segurança — ver §2.
  • As telas do tradutor não têm auth no app: o Coolify põe o middleware de auth na frente.

2. Fluxo de eventos

%% caption: Fluxo de eventos (caminho feliz)
flowchart TB
    A["1. X-Adm: mudança de preço/estoque"]
    B["2. PUT /api/v1/xadm ao integrador (Bearer)"]
    C["3. Integrador consolida a replica estoque (updated_at avança)"]
    D["4. Integrador POST /webhook/estoque-mudou ao tradutor (poke)"]
    E["5. Tradutor varre estoque: updated_at > ultimo enviado"]
    F["6. Mapeia -> POST /sync/erp a WebStorm (lotes <=100, Bearer)"]
    G["7. Grava webstorm_ecom_request (envio + resposta)"]
    H["8. Operador acompanha nas telas"]
    S["Sweep @Scheduled (rede de seguranca): repete 5-7 se o poke falhar"]

    A --> B --> C --> D --> E --> F --> G --> H
    C -.-> S
    S -.-> E

Idempotência. O tradutor só reenvia um produto quando o estoque.updated_at avança desde o último envio com sucesso — reprocesso da mesma versão não gera POST duplicado. Mudança feita fora do X-Adm (tela admin/API do integrador) não dispara poke, mas é pega pelo sweep.

3. Contrato de ingestão do integrador

O X-Adm alimenta o integrador. Base: https://int.thoms.xadm.biz.

O contrato abaixo é fato do integrador, dono dele: chega importado fresco do raw/ dele a cada build desta doc, não copiado (constituição §1.9). Mudou lá, muda aqui no próximo build.

Endpoint e autenticação

O integrador tem um endpoint de ingestão, com dois verbos sobre o mesmo corpo:

Verbo Efeito
PUT /api/v1/xadm upsert — cria ou atualiza; registro que existia deletado é revivido
DELETE /api/v1/xadm soft delete dos registros identificados pelas chaves de negócio do corpo

Toda rota /api/** exige Authorization: Bearer <token>. O token é estático e por instalação (há uma instalação por cliente), entregue por canal seguro — sem ele a resposta é 401.

Envelope

O corpo é um objeto JSON com o campo Origem (string) e uma chave por tabela, cada uma com um array de objetos:

{
  "Origem": "PNOTAI",
  "ESTOQUE": [ { "ChaveEst": "...", "Saldo": 10 } ],
  "MUNICIPIO": [ { "CodMun": "4314902", "Estado": "RS", "NomeMun": "Porto Alegre" } ]
}

As chaves de tabela reconhecidas são 15:

CONTRATOS · ITENSPED · ITENSPEDAMX · ITPEDAMXDI · LOTEEST · ESTOQUE · MUNICIPIO · PROPRIEDADES · FONES · VEICULOS · NOTACOMPL · ITENS · ITENSLOTE · TBPCOEST · FORMULAS

O nome da chave é case-sensitive: chave com caixa errada não casa tabela nenhuma e o payload é tratado como vazio (ver Resposta). Tabela ausente do corpo é ignorada — só se envia o que mudou.

Resposta

O status HTTP não diz se deu certo. Falha de processamento responde 200 com resultado: "ERRO" — o contrato foi mantido assim por compatibilidade com o integrador antigo. Quem consome tem de olhar o campo resultado, não o status HTTP; um cliente que trate 200 como sucesso perde toda falha em silêncio.

{ "requestId": 1234, "resultado": "SUCESSO", "mensagem": null }
Campo Valor
requestId id do registro Request criado para esta chamada (rastreia o processamento)
resultado SUCESSO, ERRO ou PENDENTE
mensagem null em sucesso; o detalhe do erro quando resultado é ERRO
HTTP Quando
200 o corpo era JSON válido — inclusive quando o processamento falhou (resultado: "ERRO")
400 JSON inválido ou malformado; corpo {"requestId": null, "resultado": null, "mensagem": "JSON inválido: …"}
401 Bearer ausente ou inválido
500 falha antes do processamento (ex.: o registro da chamada não gravou). Erro de processamento não é 500 — é 200 com ERRO

Payload que não casa nenhuma tabela conhecida responde 200 com resultado: "ERRO" e a mensagem "payload sem nenhuma tabela reconhecida (verifique as chaves de array por tabela)". É guarda deliberada contra o no-op silencioso: sem ela, chave com caixa errada voltaria SUCESSO sem gravar nada.

Origem

Origem identifica a rotina do X-Adm que originou a carga. Ela é gravada e usada para rastreio, nunca validada: origem desconhecida é aceita e processada normalmente.

O que a lista de origens conhecidas muda é só o roteamento de erro ao Sentry — falha de processamento numa origem conhecida vira evento com as tags origem/tipo/request_id; numa origem fora da lista, a falha não é reportada. Ou seja, origem errada não quebra a chamada, mas apaga o alarme: o erro fica invisível.

Origens conhecidas hoje: PPEDAMX · PPEDIDO · PBLDI · PNOTAI · PNOTAD · PCSTPROD · PREQUIS · INT-PIED.

FORMULAS — fórmula (BOM) do kit (fluxo de entrada PIED)

Tabela de entrada (Origem: INT-PIED): a fórmula de um pedido-kit — um objeto por componente do kit. O produtor manda só três campos de input (caixa exata):

Campo Tipo Conteúdo
xPed string(15) código do pedido do kit (casa com o xPed de CONTRATOS/ITENSPED)
CodProd string(6) código do produto componente
Qtde decimal quantidade do componente consumida na fórmula
{ "Origem": "INT-PIED",
  "FORMULAS": [ { "xPed": "260069986", "CodProd": "22162", "Qtde": 10 },
                { "xPed": "260069986", "CodProd": "16442", "Qtde": 2 } ] }

Chave natural (xPed, CodProd). É fluxo de entrada de mão única para o dado: o X-Adm cadastra e devolve só o status (CodRetorno/MsgRetorno) pelo write-back (006 = gravado) — nunca devolve chave, então a tabela não carrega colunas Chave*. Reenvio idempotente preserva o status já gravado. Sem DELETE (pedido pago é imutável).

Manifesto de remessa — a unidade de entrega ao ERP

O ingest não grava só as linhas: ele declara quais precisam chegar juntas ao ERP. Isso nasce na mesma transação do apply, replica pelo PowerSync junto com as próprias linhas, e é o que permite ao coletor perguntar "tenho tudo desta remessa?" em vez de deduzir dependência entre tabelas.

Por que existe. O ERP rejeita filho sem pai, em silêncio e de forma terminal (1XX), e o coletor lê só 000/9XX — a linha rejeitada fica invisível para sempre. O ingest é atômico no Postgres, mas essa atomicidade se perde na fronteira do PowerSync: linhas da mesma transação chegam ao cliente em checkpoints diferentes. Medido em produção, três formatos do mesmo defeito:

pai ausente filho rejeitado mensagem do ERP
propriedades (cliente) contrato → item → fórmulas 120-Cliente CPF … não cadastrado.
contratos (pedido) item, fórmulas 120-Pedido (…) não cadastrado.
estoque (produto) item, fórmulas 120-Produto para o pedido (…) não cadastrado.

As duas tabelas.

tabela o que é
integracao_remessa a unidade: origem, chave_negocio, status, total_itens
integracao_remessa_item as linhas dela: tabela, linha_id, bloqueado, resolvido

Quando nasce. Uma remessa por grupo com dependência (duas ou mais linhas ligadas) — não por push. Linha solta (propriedades avulsa, estoque avulso) não gera remessa e segue coletável como antes: o manifesto acrescenta garantia, nunca remove. chave_negocio é o xPed no grupo de pedido e o cgc_cpf no grupo de cliente. Entra na remessa só o que ainda precisa chegar ao ERP (000, 9XX ou nulo); 006 e 1XX ficam de fora.

Uma remessa VIVA por chave (ABERTA ou TRAVADA) — invariante de banco. As ENTREGUE e RESOLVIDA_MANUAL acumulam como histórico. Um re-push reusa a viva, inclusive travada; é o que faz o resgate fechar.

Os estados, derivados no servidor a partir dos cod_retorno — exceto o último, que nasce de comando:

status significa
ABERTA ainda não entregue, sem rejeição terminal
ENTREGUE todas as linhas aceitas (006) — vira histórico
TRAVADA alguma linha em 1XX; só o re-arme a reabre
RESOLVIDA_MANUAL o operador lançou o pedido à mão no X-Adm e fechou a remessa por comando; terminal

As duas flags por item, e por que existem:

  • bloqueado — um pai desta linha, na mesma remessa, está 1XX. Enviá-la repetiria o dano (uma ausência virou nove linhas terminais no incidente). O consumidor lê a flag; não deduz dependência. É também o que distingue rejeição real de colateral: quem reporta ao operador deve nomear a mensagem do item não bloqueado.
  • resolvido — nada mais a esperar por esta linha: 006, soft-deletada ou órfã. Existe porque, do lado do cliente, linha deletada e linha que ainda não chegou são o mesmo estado observável (a coluna deleted não sincroniza e a linha some do banco local). Sem a flag, a remessa ficaria retida para sempre lá.

total_itens não é cache de COUNT(*). É o que deixa o consumidor detectar lista de itens rasgada: ele conta o que recebeu e compara com o declarado. Sem isso concluiria "tenho tudo" sobre uma lista truncada e liberaria cedo — o mesmo modo de falha do incidente, um nível acima.

Tudo o que é derivado sai de uma função só, chamada de quatro lugares: o apply (PUT), o soft-delete (DELETE), o write-back do PowerSync e o re-arme. Uma fonte só é o que impede o drift. A única exceção é RESOLVIDA_MANUAL, que a função não escreve nem reabre.

Consulta: GET /api/v1/integracao/remessas?origem=&chave=&status= — chave aceita repetição. Nas TRAVADA vêm também os itens em 1XX com codRetorno, msgRetorno e bloqueado. A resposta completa e o fechamento à mão (POST …/remessas/resolver-manual) estão no fato próprio, integracao-remessa.md.

Re-arme: POST /api/v1/xadm/retorno/pedido/reenfileirar?chave= re-arma todos os itens 1XX da remessa viva — qualquer que seja a tabela, inclusive o pai. Sem isso, remessa cujo 1XX é um pai não sairia de TRAVADA por caminho nenhum. xPed segue valendo como alias.

Tabela ESTOQUE

Cada item do array ESTOQUE é um produto. Todos os campos são opcionais no envelope — o que não vier não é escrito.

Campo Tipo Nota
ChaveEst texto identidade do produto. É a chave de join do espelho
codProd texto identidade alternativa, usada onde ChaveEst chega vazio. Aceita CodProd (PascalCase)
NomeProd texto descrição. Aceita nomeProd
EAN13 texto código de barras. Aceita ean13
CodProdAlt texto código alternativo do produto no X-Adm
CodigoAlt texto código do produto na PIED — rastreabilidade de origem externa, gravado em pied_codigo_alt. Não é identidade nem critério de join
Venda decimal preço de venda. Aceita venda
VendaPz decimal preço a prazo. Aceita vendaPz
Saldo decimal saldo em estoque. Aceita saldo

Identidade. O produto é identificado por ChaveEst; onde ele chega vazio (o caso da instância Thoms), a identidade é o codProd. A chave do parceiro (CodigoAlt) nunca é identidade — entra prefixada no espelho (pied_codigo_alt) e serve só para rastrear de onde o dado veio. Promover chave de parceiro a identidade de primeira classe acopla o espelho ao vocabulário de um terceiro.

Write-back. CodRetorno e MsgRetorno não são lidos deste payload: são escritos pelo X-Adm no sentido inverso. Mandá-los aqui não tem efeito.

Caixa dos campos. Onde a tabela diz "aceita", o parser lê as duas grafias; nos demais campos a grafia é exata. Campo com caixa fora do previsto é ignorado em silêncio — o registro é gravado sem ele, e a chamada volta SUCESSO.

O que esta integração usa

Do contrato acima, o fluxo de saída da Thoms manda só a tabela ESTOQUE, e dela seis campos:

Campo Papel nesta integração
codProd identidade do produto — o ChaveEst chega vazio na instância Thoms
nomeProd descrição do produto
ean13 correlação com a WebStorm (decisão 0002)
venda preço à vista
vendaPz preço a prazo (decisão 0001)
saldo saldo disponível para venda

Exemplo do que a Thoms envia:

PUT /api/v1/xadm HTTP/1.1
Host: int.thoms.xadm.biz
Authorization: Bearer <API_BEARER_TOKEN>
Content-Type: application/json
{
  "Origem": "<ORIGEM>",
  "ESTOQUE": [
    { "codProd": "100103", "nomeProd": "FILTRO PARA PISCINA 1.000L/H 127V",
      "ean13": "7891234000001", "vendaPz": 283.13, "venda": 259.99, "saldo": 2 },
    { "codProd": "120145", "nomeProd": "REFIL PARA FILTRO 1.000L/H 127V",
      "ean13": "7891234132002", "vendaPz": 34.45, "venda": 29.99, "saldo": 40 }
  ]
}

Resolvido: o formato do export do X-Adm (incl. Origem) é o da spec oficial em docs/anexos/privado/THOMS2026_413248_Us.pdf. Na Thoms o fluxo vivo é CSV → tradutor (§5.3 abaixo), então este PUT JSON com Origem é o contrato de entrada do integrador (outro repo, fora do escopo deste).

Remoção (soft-delete)

DELETE https://int.thoms.xadm.biz/api/v1/xadm — mesmo Bearer e mesmo formato de corpo. Efeito: marca o produto como removido (deleted); o tradutor então envia saldo = 0 (esgotado) ao parceiro.

4. Site de acompanhamento

O que está sendo enviado ao parceiro é auditável em https://webstorm.thoms.xadm.biz (telas do tradutor, atrás do middleware de auth do Coolify):

  • /webstorm-ecom — trilha de cada POST /sync/erp: quando, cod_prod, ean13, HTTP, resultado (ATUALIZADO/NAO_ENCONTRADO/AMBIGUO/ERRO/FALHA_TRANSPORTE) e sucesso.
  • /webstorm-ecom?atencao=true — só o que precisa de olhar humano (não casado / falha).
  • detalhe (por linha) — payload enviado + resposta do parceiro; botão Reprocessar (reenvia o produto ignorando a idempotência).
  • /admin/monitor — watchdog do heartbeat do X-Adm (decisão 0009): periodicidade, timeout, janela útil, dias vigiados, último CSV recebido, idade e estado (OK / fora-da-janela / ALARMADO). Se o X-Adm parar de enviar dentro da janela além do timeout, cai um alarme ERROR no GlitchTip; a tela é a leitura humana desse estado. Sábado e domingo ficam fora por padrão (MONITOR_DIAS). Silenciar: WEBSTORM_MONITOR_ENABLED=false (§5.2).
  • /health — {status, versao}.

5. Variáveis de ambiente

Nomes de ENV seguem o mapeamento Micronaut (propriedade → MAIÚSCULA_COM_UNDERSCORE). Segredos via secrets do Coolify.

5.1 Servidor do integrador — int.thoms.xadm.biz

Variável Valor / descrição
CLIENTE Thoms
DATASOURCES_DEFAULT_URL JDBC do db_thoms (ex. jdbc:postgresql://db:5432/db_thoms)
DATASOURCES_DEFAULT_USERNAME / DATASOURCES_DEFAULT_PASSWORD credencial do integrador (dono do schema)
API_BEARER_TOKEN segredo exigido no Bearer de PUT/DELETE /api/v1/xadm (o X-Adm usa o mesmo)
SENTRY_DSN DSN GlitchTip (observabilidade)
PORT 8080 (default)
WEBSTORM_WEBHOOK_ENABLED true (só no deploy Thoms) — liga o poke de saída
WEBSTORM_WEBHOOK_URL https://webstorm.thoms.xadm.biz
WEBSTORM_WEBHOOK_TOKEN segredo — Bearer que o integrador manda no poke; tem que valer o WEBSTORM_API_TOKEN do tradutor (é o token para chamar o webstorm). (Convenção nova: nomear WEBSTORM_API_TOKEN também aqui.) Path do poke: migrar o TradutorWebhookClient de /webhook/estoque-mudou → /api/integrador/produto (o tradutor mantém o alias na transição — poke não cai).

5.2 Servidor do tradutor — webstorm.thoms.xadm.biz

Variável Valor / descrição
CLIENTE Thoms
DATASOURCES_DEFAULT_URL JDBC do mesmo db_thoms
DATASOURCES_DEFAULT_USERNAME / DATASOURCES_DEFAULT_PASSWORD credencial do tradutor — read em estoque, write em webstorm_ecom_*
WEBSTORM_API_TOKEN segredo — Bearer ROLE_API para chamar o webstorm (/api/**): o X-Adm no POST /api/csv/processar e o integrador no poke POST /api/integrador/produto (alias /webhook/estoque-mudou). Nomenclatura por destino: <APP>_API_TOKEN = token para chamar <APP>. Consolidou os antigos PRODUTO_TOKEN+WEBHOOK_TOKEN. Compartilhar com o X-Adm; e o integrador manda este valor (WEBSTORM_WEBHOOK_TOKEN lá = este).
PARCEIRO_URL base da API WebStorm (https://api.thoms.com.br) — o app faz POST {url}/sync/erp. Default inócuo http://localhost:65535: não setar não dá erro no boot, dá falha de envio na trilha
PARCEIRO_API_TOKEN Bearer para chamar o parceiro WebStorm (/sync/erp)
INTEGRADOR_URL https://int.thoms.xadm.biz — o adapter CSV faz PUT/DELETE {url}/api/v1/xadm. Default inócuo http://localhost:65534 (não seta não quebra o boot; falha ao processar snapshot)
INTEGRADOR_API_TOKEN Bearer para o adapter chamar o integrador (PUT/DELETE /api/v1/xadm) — o mesmo que o integrador valida no seu inbound (API_BEARER_TOKEN do §5.1)
PRODUTO_PACING_MS pausa (ms) entre lotes ≤100 ao integrador — protege o lock global do ERP. Default 0 (sem pausa); calibrar em prod se o bulk atrapalhar notas/pedidos
SWEEP_INTERVALO cadência do sweep de segurança (default 60s)
WEBSTORM_SWEEP_ENABLED false desliga o sweep periódico (default: ligado). Kill switch — ver §8. @Requires no bean: lido no start, exige redeploy/restart
WEBSTORM_CLEANUP_CRON quando a limpeza do log roda (default 0 0 3 * * * — 03:00)
LOG_RETENCAO_DIAS retenção do log de auditoria (default 90)
MONITOR_PERIODICIDADE watchdog do X-Adm (decisão 0009): min entre envios esperados (default 60)
MONITOR_TIMEOUT_ALARME min de silêncio na janela útil antes de alarmar no GlitchTip (default 240). Deve ser ≥ 2× MONITOR_PERIODICIDADE — senão o app não sobe
MONITOR_INTERVALO cadência do próprio check @Scheduled (default 5m)
MONITOR_ZONA fuso da janela útil (default America/Sao_Paulo). Os horários da janela (07:00–20:00) são literais no application.yml, não env (o : quebra o default de placeholder) — mudar = editar o yaml + redeploy
MONITOR_DIAS dias da semana vigiados, siglas pt-br separadas por vírgula (default SEG,TER,QUA,QUI,SEX; aceita SAB, DOM, sem diferenciar maiúscula/acento). Dia fora da lista não alarma; na segunda a contagem recomeça às 07:00. Sigla inválida ou lista vazia → o app não sobe
WEBSTORM_MONITOR_ENABLED false silencia o watchdog (default: ligado). @Requires no bean — lido no start, exige redeploy/restart
GARAGE_ENDPOINT / GARAGE_BUCKET / GARAGE_REGION Garage (decisão 0010): endpoint S3, bucket dedicado, region do cluster. Injetados pelo /xadm-setup (client-grade em app.json → features.garage)
GARAGE_ACCESS_KEY_ID / GARAGE_SECRET_ACCESS_KEY secret — write key do Garage. Injetados pelo /xadm-setup (só em app com production_url). Sem a SECRET_ACCESS_KEY o ArquivoStore não sobe (@Requires) — feature de arquivo off, resto do app normal
ARQUIVO_RETENCAO_DIAS retenção do upload no Garage (default 30; a linha de auditoria segue LOG_RETENCAO_DIAS=90)
SECURITY_ENABLED micronaut.security.enabled — default true (linhagem 006). Quem "desliga" é o tri-estado da ViewSecurityRule: sem AUTH_* fora de dev/test = fail-closed (telas 503); em dev/test = bypass. Não desligar em prod
AUTH_FIREBASE_PROJECT_ID xadm-6ab81 (reusa o projeto Firebase do onpetro) — login Google das telas admin
AUTH_FIREBASE_API_KEY / AUTH_FIREBASE_AUTH_DOMAIN / AUTH_FIREBASE_APP_ID config web do Firebase xadm-6ab81 (públicos; vão no HTML do /login)
AUTH_SESSION_SECRET segredo HS256 do cookie xadm_session (≥32 chars). Sem ele (e os demais AUTH_*) em prod → telas fail-closed (503)
AUTH_SESSION_TTL_SECONDS TTL do cookie de sessão (default 28800 = 8h)
AUTH_ISSUER claim iss do cookie xadm_session (default webstorm-ecom). Não setar em prod — o default identifica o app. Ao adotar a lib (decisão 0006) o issuer mudou de bi-comercial-xls para webstorm-ecom: as sessões vivas com o issuer antigo caem → 1 re-login forçado no primeiro deploy (TTL 8h; o deploy já reinicia o app)
AUTH_XADM_EMAIL_DOMAIN domínio aceito no login admin (default xadm.com.br)
SENTRY_DSN DSN do GlitchTip — fonte: docs/app.json → features.glitchtip.dsn (projeto thoms-integracao-produto-ecom, bug.xadm.biz/17). É público; o Coolify só injeta o env. DSN vazio/inválido = Sentry inerte, app sobe igual
SENTRY_ENVIRONMENT default production no código
SENTRY_RELEASE versão marcada nos eventos do GlitchTip (opcional)
MICRONAUT_ENVIRONMENTS contendo dev ou test, não inicializa o Sentry — não usar em produção
PORT 8080 (default)

5.3 Cliente que posta ao integrador — X-Adm (Java/Dart)

O componente do X-Adm que envia produto/estoque (implementado em Java ou Dart) precisa apenas de:

Variável Valor / descrição
INTEGRADOR_URL https://int.thoms.xadm.biz (faz PUT {url}/api/v1/xadm)
INTEGRADOR_API_TOKEN o mesmo API_BEARER_TOKEN do integrador (§5.1)

Nota: acima é o contrato (URL + Bearer) do cliente que posta JSON ao integrador — do repo do integrador (nomes literais são dele). Na Thoms esse caminho não é o vivo: ver Estado atual abaixo (o X-Adm posta CSV ao tradutor).

Estado atual (2026-07): o X-Adm não posta JSON direto no integrador — posta o catálogo em CSV (.zip) no tradutor (POST https://webstorm.thoms.xadm.biz/api/csv/processar, Bearer WEBSTORM_API_TOKEN), e o adapter do tradutor é quem faz o PUT/DELETE /api/v1/xadm no integrador (INTEGRADOR_URL + INTEGRADOR_API_TOKEN do §5.2). Este §5.3 é o destino (X-Adm→ integrador direto); quando ele existir, o adapter CSV é removido. Ver Modelo de dados §Ingestão de produtos via CSV.

6. Passos de implantação (Coolify)

  1. Banco. Garantir db_thoms acessível a ambos os apps; criar o usuário do tradutor com SELECT em estoque e ALL nas webstorm_ecom_* (o Flyway do tradutor cria/migra as suas).
  2. Integrador. Deploy do xadm/integrador no subdomínio int.thoms.xadm.biz; setar as ENV do §5.1 (incluindo o trio WEBSTORM_WEBHOOK_*). Confirmar a feature 008 (coluna updated_at + webhook) no build implantado.
  3. Tradutor. Deploy deste repo no subdomínio webstorm.thoms.xadm.biz; setar as ENV do §5.2. O WEBSTORM_WEBHOOK_TOKEN do integrador tem que casar com o API_BEARER_TOKEN do tradutor.
  4. Auth das telas (linhagem 006). O app agora tem auth in-app (micronaut-security): / e /{id} públicos (auditoria de saída), /admin atrás de login Google @xadm.com.br (Firebase xadm-6ab81). Setar os AUTH_* (§5.2) — sem eles em prod, as telas ficam 503 (fail-closed). O middleware de auth do Coolify sai da frente (a auth é do app).
  5. Ingestão + poke (API ROLE_API). Setar WEBSTORM_API_TOKEN (§5.2) e compartilhá-lo com o X-Adm (que POSTa o .zip em /api/csv/processar). O integrador passa a mandar esse mesmo token no poke, migrando o path /webhook/estoque-mudou → /api/integrador/produto (o alias antigo segue vivo na transição — o poke não cai; o sweep cobre qualquer lapso). Setar INTEGRADOR_URL + INTEGRADOR_API_TOKEN (§5.2) para o adapter postar ao integrador.

7. Verificação

  1. GET https://int.thoms.xadm.biz/health e GET https://webstorm.thoms.xadm.biz/health → {status:"UP", versao:"…"}.
  2. PUT /api/v1/xadm com o exemplo do §3.1 → 200 SUCESSO.
  3. Abrir https://webstorm.thoms.xadm.biz/webstorm-ecom → o produto aparece na trilha com o resultado do parceiro (hoje em dry_run). Só entra produto com ean13: sem EAN o Sweep filtra antes de enviar e nada aparece — não é falha de deploy.
  4. Poke. O webhook responde 202 Accepted na hora (dispara o sweep em virtual thread) e 401 application/problem+json com Bearer errado — é como se confere o par WEBSTORM_WEBHOOK_TOKEN ↔ WEBHOOK_TOKEN sem esperar o sweep.
  5. Bump. Repetir o mesmo PUT, mesmo sem mudar valores → nova linha na trilha. É o esperado: estoque.updated_at é bumpada em toda escrita (@DateUpdated, sem dirty-check — decisão 008), e versão nova = envio novo.
  6. Idempotência. Dar um PUT e aguardar > 2 min (≥ 2 ciclos de SWEEP_INTERVALO) sem novo PUT → segue uma linha só para aquele produto. O sweep reler a mesma versão não reenvia. Se aparecer linha nova sem PUT no meio, a idempotência quebrou.

8. Reversão

  • Desligar a integração sem derrubar o X-Adm. O tradutor descobre a mudança por duas vias independentes — o poke e o sweep @Scheduled — e o poke dispara um sweep completo (todos os pendentes, não só o produto pokado). Logo, desligar uma via não para os envios:
Caminho Efeito
Parar o container do tradutor (Coolify) Para tudo. Mais simples e garantido — prefira este
WEBSTORM_WEBHOOK_ENABLED=false (integrador) + WEBSTORM_SWEEP_ENABLED=false (tradutor) Para tudo, mantendo as telas de auditoria no ar. Exige os dois
Só WEBSTORM_WEBHOOK_ENABLED=false ⚠️ Não desliga — o sweep segue enviando a cada SWEEP_INTERVALO
Só WEBSTORM_SWEEP_ENABLED=false ⚠️ Não desliga — cada poke ainda dispara um sweep completo

Em qualquer caso o X-Adm segue postando ao integrador normalmente; a réplica continua atualizada e o que ficou parado sai quando religar (o sweep pega o atraso pela updated_at). - Reverter versão: redeploy da tag anterior (Coolify). As migrations do tradutor são aditivas (webstorm_ecom_*); não tocam a réplica. - Falha de envio ao parceiro: fica gravada na trilha; usar Reprocessar na tela após corrigir a causa (token/URL do parceiro).