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 colunadeletednã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 emdocs/anexos/privado/THOMS2026_413248_Us.pdf. Na Thoms o fluxo vivo é CSV → tradutor (§5.3 abaixo), então estePUTJSON comOrigemé 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 cadaPOST /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 alarmeERRORno 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, BearerWEBSTORM_API_TOKEN), e o adapter do tradutor é quem faz oPUT/DELETE /api/v1/xadmno integrador (INTEGRADOR_URL+INTEGRADOR_API_TOKENdo §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)¶
- Banco. Garantir
db_thomsacessível a ambos os apps; criar o usuário do tradutor comSELECTemestoqueeALLnaswebstorm_ecom_*(o Flyway do tradutor cria/migra as suas). - Integrador. Deploy do
xadm/integradorno subdomínioint.thoms.xadm.biz; setar as ENV do §5.1 (incluindo o trioWEBSTORM_WEBHOOK_*). Confirmar a feature 008 (colunaupdated_at+ webhook) no build implantado. - Tradutor. Deploy deste repo no subdomínio
webstorm.thoms.xadm.biz; setar as ENV do §5.2. OWEBSTORM_WEBHOOK_TOKENdo integrador tem que casar com oAPI_BEARER_TOKENdo tradutor. - Auth das telas (linhagem 006). O app agora tem auth in-app (micronaut-security):
/e/{id}públicos (auditoria de saída),/adminatrás de login Google @xadm.com.br (Firebasexadm-6ab81). Setar osAUTH_*(§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). - Ingestão + poke (API
ROLE_API). SetarWEBSTORM_API_TOKEN(§5.2) e compartilhá-lo com o X-Adm (quePOSTa o.zipem/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). SetarINTEGRADOR_URL+INTEGRADOR_API_TOKEN(§5.2) para o adapter postar ao integrador.
7. Verificação¶
GET https://int.thoms.xadm.biz/healtheGET https://webstorm.thoms.xadm.biz/health→{status:"UP", versao:"…"}.PUT /api/v1/xadmcom o exemplo do §3.1 →200 SUCESSO.- 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 comean13: sem EAN oSweepfiltra antes de enviar e nada aparece — não é falha de deploy. - Poke. O webhook responde
202 Acceptedna hora (dispara o sweep em virtual thread) e401application/problem+jsoncom Bearer errado — é como se confere o parWEBSTORM_WEBHOOK_TOKEN↔WEBHOOK_TOKENsem esperar o sweep. - 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. - Idempotência. Dar um
PUTe aguardar > 2 min (≥ 2 ciclos deSWEEP_INTERVALO) sem novoPUT→ segue uma linha só para aquele produto. O sweep reler a mesma versão não reenvia. Se aparecer linha nova semPUTno 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).