Table of Contents
Integração de Produtos X-Adm → E-commerce (Thoms)¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-06
No cliente Thoms, o estoque e o preço dos produtos são definidos no X-Adm (ERP), mas o e-commerce é operado por um parceiro, a WebStorm (https://www.webstorm.com.br) — hoje, funcionários da Thoms repetem à mão cada mudança de preço e estoque no site. Esta integração automatiza a ponte.
O X-Adm envia produto/estoque para uma nuvem (int.thoms.xadm.biz): um integrador guarda
esses dados numa réplica do X-Adm e dispara um tradutor (webstorm-ecom.thoms.xadm.biz), que
mapeia para a API do parceiro e faz o POST — registrando o envio e a resposta. Uma carga
inicial por planilha semeia a base do e-commerce uma vez.
Este repositório é de documentação apenas: descreve a arquitetura, o modelo de dados, o mapeamento e os contratos. Não inclui implementação.
Projeto
Documentação Completa¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-06
Consolida o projeto da integração que leva preço e estoque dos produtos do X-Adm (ERP do cliente Thoms) ao e-commerce do parceiro, eliminando a digitação manual que os funcionários da Thoms fazem hoje.
1. Contexto e problema¶
No cliente Thoms, o estoque e o preço de cada produto são definidos no X-Adm. O e-commerce é operado por outra empresa, a WebStorm (https://www.webstorm.com.br) — o parceiro. Por isso, os funcionários da Thoms precisam repetir à mão, no site, cada alteração de preço ou estoque feita no X-Adm — trabalho repetitivo e sujeito a erro/atraso.
Esta integração automatiza essa ponte: o X-Adm passa a alimentar o e-commerce com preço e estoque, sem intervenção manual.
Escopo: a arquitetura ponta-a-ponta (§5), o modelo de dados e os contratos (§3), o de-para X-Adm → parceiro (§4) e os contratos públicos (§7).
Fronteira (confirmada com o parceiro): a API atualiza preço e estoque de variantes que já
existem no e-commerce; não cadastra produto novo — um EAN inexistente volta em
nao_encontrados. Cadastro de produto novo está fora desta integração.
Fora de escopo: o código do integrador (outro repo) e do tradutor (construído à parte);
a implementação dos gatilhos dentro do X-Adm; a exportação .docx.
2. Dados de entrada¶
Dois fluxos alimentam a integração:
- Contínuo (produto/estoque): o X-Adm posta, a cada alteração, um JSON no seu modelo ERP de
produto/estoque ao integrador (raiz
estoque[], 6 campos). Regras e schema desse contrato: §3 (canônico no repo do integrador). - Carga inicial (uma vez): o X-Adm gera o
ProdutosSite.csv(todos os produtos), enviado por e-mail ao parceiro para importação em massa — semeia a base do e-commerce. Colunas e formato: §3.
3. Modelo de dados¶
O modelo é o contrato trocado nas pontas e os três bancos (stores) que a nuvem
persiste. É a fonte única, mantida em modelagem.md e embutida aqui:
Modelo de dados¶
O modelo desta integração tem duas naturezas: os bancos que guardam o que passa pela
nuvem (três stores, §Persistência) e os contratos JSON trocados nas pontas — o PUT de saída
que o X-Adm posta ao integrador (§ Réplica no integrador) e o contrato do parceiro
(POST /sync/erp), que é o alvo do tradutor.
Contrato de resposta do parceiro verificado
O contrato de resposta do parceiro (§Contrato do parceiro) foi verificado contra a API
real em 2026-06-25, em modo dry_run (simulação — sem alterar a base do parceiro). Os
exemplos usam dados fictícios.
Persistência — os três stores¶
A nuvem persiste nas duas pontas para auditoria completa:
| Store | Papel | Dono |
|---|---|---|
| Requests de entrada | log de cada request que o X-Adm posta (o que chegou, quando) | Integrador |
| Réplica do X-Adm | estado consolidado de produto/estoque enviado pelo X-Adm (réplica parcial, não o ERP inteiro) | Integrador |
webstorm_ecom_request |
cada POST de saída ao parceiro e a resposta recebida (fecha a auditoria da saída) + o controle de idempotência (versão/estado já enviado) |
Tradutor |
Detalhes de esquema (colunas, índices) estão nas migrations Flyway (webstorm_ecom_*, V1..V7)
e nos read-models/entidades (ex. o record Estoque da réplica, com updated_at/deleted) — aqui
fica o papel de cada store. Os stores vivem no mesmo banco db_thoms, compartilhado pelos dois apps: a réplica
e os requests de entrada são do integrador; as tabelas do tradutor são prefixadas
webstorm_ecom_* e migradas por um Flyway próprio (history dedicada), sem colidir com as
migrations do integrador. O tradutor lê a réplica (read-only) e nunca a migra.
Tabela
mensagem_m2m(auditoria M2M) — do INTEGRADOR, o tradutor só consome. A libxadm-mensageriatraz uma tabelamensagem_m2m(sem o prefixowebstorm_ecom_*— é infra da lib) compartilhada nodb_thoms: o integrador a cria e grava asSAIDAdo outbox; o tradutor grava só o log de ENTRADA do poke (direcao=ENTRADA/status=RECEBIDO) — sem criar a tabela em produção (doisCREATE TABLEno mesmo banco colidiriam). Nos testes do tradutor, uma migração test-only cria a tabela. Deploy coordenado com o integrador. Ver decisão 0008.
Réplica no integrador — a tabela estoque já existente¶
Não criamos réplica. O integrador já é um espelho do X-Adm e já tem a tabela estoque.
Para a Thoms, ela é alimentada pelo PUT JSON de saída do X-Adm (X-Adm → integrador, igual Sul
Plata/OnPetro) — não pelo fluxo de entrada PIED (que é da Maxsul). O X-Adm posta produto/estoque
já consolidado; o tradutor apenas lê essa tabela (read-only) no db_thoms. Modelo de identidade:
decisão 0013 do integrador (nota de 2026-07-15).
Identidade do produto: no X-Adm o estoque identifica por chave_est; na Thoms o chave_est
chega null e a identidade é o cod_prod (versão virtual do chave_est, gravada como recebida
no PUT de saída). A correlação com a WebStorm é por ean13 (decisão 0002). A coluna
pied_codigo_alt (código do produto na PIED, ex-codigo_alt) é transporte do fluxo Maxsul e
vem null na Thoms — não entra no de-para.
Colunas de estoque que o tradutor consome (tipos reais do integrador; CHAR é padded —
comparar/ler com TRIM):
Coluna estoque |
Tipo real | Campo parceiro | Nota |
|---|---|---|---|
cod_prod |
CHAR(11) | codigo_erp |
identidade do produto na Thoms (chave_est null); referência/rastreio, não é a chave de match |
ean13 |
CHAR(13) | ean |
chave de correlação com a WebStorm; não é unique na tabela |
nome_prod |
VARCHAR(168) | nome |
vem do PUT de saída; enviado mas não aplicado pelo parceiro (decisão 0001) |
venda_pz |
NUMERIC(19,5) | preco_cheio |
preço a prazo; 2 casas no request |
venda |
NUMERIC(19,5) | preco_desconto |
preço à vista; 2 casas no request |
saldo |
NUMERIC(19,3) | estoque |
inteiro no parceiro; pode ser negativo |
id (UUID v7) é a PK física; cod_prod é a identidade de negócio na Thoms. pied_codigo_alt
(transporte PIED, null na Thoms), cod_prod_alt CHAR(8), cod_retorno/msg_retorno e chave_est
não entram no de-para WebStorm.
Fora do PUT de saída:
STATUS/AtivoSai,PESO,ATRIBUTOe a janela de promoção não chegam ao integrador. OPESO/ATRIBUTO/promoção ficam fora da réplica (sem coluna, sem consumidor). OSTATUS, porém, passou a ser tratado pelo adapter de ingestão CSV (abaixo):STATUS=0vira soft-delete no integrador (produto sai de estoque no parceiro).Mudança (2026-07): o CSV virou o fluxo contínuo. O
ProdutosSite.csvdeixou de ser só carga inicial por e-mail — o X-Adm passa a POSTá-lo (dentro de um.zip) num endpoint do tradutor, que transcodifica para oPUT/DELETE /api/v1/xadmdo integrador. Ou seja, o CSV agora alimenta a réplica (via o integrador) e daí o fluxo segue normal (sweep → parceiro). Ver §Ingestão de produtos via CSV (adapter).
Migration necessária no integrador — updated_at para o sweep¶
Hoje estoque não tem coluna de mudança (updated_at/versão) — só deleted/deleted_at. Sem
isso, o sweep (D) não acha "linha alterada" de forma barata. Proposta de migration no
integrador (mesma casa/time), aditiva:
ALTER TABLE estoque ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ;
CREATE INDEX IF NOT EXISTS idx_estoque_updated_at ON estoque(updated_at);
-- bumpar updated_at = now() no apply do X-Adm (XadmApplyService)
Com isso: o webhook (A) avisa na hora; o sweep (D) varre estoque WHERE updated_at >última
processada, comparando com o que a webstorm_ecom_request registra. As tabelas do tradutor
(webstorm_ecom_request — auditoria + idempotência) são as únicas que o nosso Flyway cria,
com history própria e prefixo webstorm_ecom_*; estoque é do integrador e nós não a migramos.
Origem dos dados (X-Adm) — ProdutosSite.csv¶
Para a carga inicial (e como referência das colunas do de-para), o X-Adm exporta os produtos
no arquivo ProdutosSite.csv (1 linha por produto, ;-separado, decimal com vírgula,
datas DD/MM/AAAA HH:MM). A definição oficial e o export real (confidenciais) estão em
docs/anexos/privado/ (PDF THOMS2026_413248 + ProdutosSite.csv).
Cabeçalho: CODIGO;NOME;ATRIBUTO;VALOR;ESTOQUE;STATUS;PESO;PROMOCAO;INI PROMOCAO;FIM PROMOCAO;EAN13
| Coluna | Descrição | Usada na integração |
|---|---|---|
CODIGO |
código único do produto no X-Adm | → codigo_erp (referência) |
NOME |
nome do produto | → nome |
ATRIBUTO |
em branco (a pedido da empresa) | não |
VALOR |
preço a prazo | → preco_cheio |
ESTOQUE |
qtde em estoque (considera pedidos pendentes; pode ser negativo) | → estoque |
STATUS |
1 = ativo com saldo; 0 = inativo / estoque zerado / ficará negativo por pedido pendente |
filtro (ver obs) |
PESO |
peso do produto | não |
PROMOCAO |
preço promocional = preço à vista | → preco_desconto |
INI PROMOCAO / FIM PROMOCAO |
janela de validade da promoção (preço à vista) | não (contexto) |
EAN13 |
código de barras (GTIN-13) — chave de correlação | → ean |
Exemplo (dados fictícios; o real é confidencial):
CODIGO;NOME;ATRIBUTO;VALOR;ESTOQUE;STATUS;PESO;PROMOCAO;INI PROMOCAO;FIM PROMOCAO;EAN13
399001;CAMISETA THOMS BASICA P;;159,90000;12,00000;1;,200;143,91000;01/06/2026 00:00;30/06/2026 23:59;7891000100103
Observações do export:
- Volume: ~2.559 produtos; ~1.992 com
STATUS=1(ativos) — número próximo das ~1.900 variantes do e-commerce; oSTATUSindica o que está ativo/com saldo. EAN13pode faltar: há produto sem EAN no export — sem EAN o parceiro não casa o produto (errosem_ean_nem_sku); alguns EANs são internos (prefixo2000000…), não GTIN reais.ATRIBUTOvem sempre vazio.VALOR>PROMOCAO(a prazo > à vista).- Números: vírgula decimal e até 5 casas no X-Adm → no JSON do parceiro viram ponto e 2 casas.
Ingestão de produtos via CSV (adapter)¶
O X-Adm faz POST do ProdutosSite.csv (dentro de um .zip) no endpoint
POST /api/csv/processar do tradutor (multipart, campo arquivo; convenção da casa
/api/<formato>/processar, igual ao onpetro /api/xls/processar). Auth por filtro Bearer
dedicado (webstorm.produto.token) — o app é slim e não liga o micronaut-security global. O
endpoint responde 202 na hora ({loteId, recebidos, status}); o processamento pesado roda
assíncrono, serializado por lock justo (dois snapshots concorrentes vão em série).
O snapshot é completo (todo o catálogo). Para não reenviar tudo a cada envio, o adapter mantém
um diff-store próprio (webstorm_ecom_snapshot: cod_prod → checksum de VendaPz|Saldo|STATUS
da última versão confirmada). O diff de conjunto completo classifica cada produto:
| Situação no snapshot | Ação no integrador |
|---|---|
STATUS=1 novo ou com checksum mudado |
PUT (upsert) /api/v1/xadm |
STATUS=1 inalterado |
skip (não toca o integrador) |
STATUS=0 (era ativo) ou produto sumido do CSV |
DELETE (soft-delete) — sai de estoque no parceiro |
O envio vai em lotes ≤100, sequenciais, com pacing (protege o lock global do integrador). Como o
PUT/DELETE do integrador é tudo-ou-nada, um lote que volta resultado=ERRO é reenviado
item-a-item para isolar o veneno (o resto grava; o veneno vai pro GlitchTip). O diff-store só
avança nos itens que gravaram (resultado=SUCESSO) — garantia at-least-once: o que falha fica
"sujo" e é reenviado no próximo snapshot (auto-cura). EAN13 vazio → linha descartada + alerta
Sentry (nunca deve acontecer).
O adapter não chama o parceiro: ao consolidar a réplica, o integrador emite EstoqueMudouEvent
→ webhook → sweep existente → POST /sync/erp. É uma ponte temporária — quando o X-Adm postar
JSON direto no integrador, o adapter é removido e o resto do fluxo fica igual.
Auditoria da entrada: cada snapshot processado grava uma linha em webstorm_ecom_ingestao
(lote_id, recebidos, enviados_put/delete, ignorados_skip, erros, descartados, created_at)
— "o que veio da planilha e quando". descartados = linhas removidas por dado crítico faltante
(sem EAN13, sem VALOR ou sem ESTOQUE) — não vão ao integrador e geram alerta no Sentry
(produto incompleto). erros = falha de formato (número inválido, CODIGO vazio) — vai pro log. Complementa a webstorm_ecom_request (auditoria da saída, o
que chegou na WebStorm). Persiste mesmo se o integrador estiver fora (o envio é resiliente: transporte
caído → itens ficam "sujos" p/ o próximo snapshot, sem derrubar o processamento).
Telas de auditoria (linhagem 006, micronaut-security in-app): / e /{id} são
públicas (últimos payloads/saída, com valores). A / (Envios) traz filtro por transporte
(Todos · Sucesso · Falha, pelo booleano sucesso — SUCESSO inclui NAO_ENCONTRADO, que é transporte
ok) e paginação (50/página); HTTP nulo (sem resposta = timeout) aparece como —. /admin (+ /admin/{id}) exige login
Google @xadm.com.br e lista os processamentos (webstorm_ecom_ingestao, a entrada) no
padrão da tela do BI-Comercial (# · arquivo · tipo · status · recebido em · duração · registros),
com filtro por Status (derivado: erros→ERRO, descartados→ATENÇÃO, senão SUCESSO) e paginação
(20/página); o breakdown PUT/DELETE/skip/descartados e o lote_id ficam no /admin/{id}. O
reprocesso (POST /admin/produto/{codProd}/reprocessar, CSRF) é só admin. /admin/upload
(ships) sobe um .csv ou .zip e processa síncrono (mesmo pipeline do POST /api/csv/processar)
para inspeção — mostra recebidos/PUT/DELETE/skip + descartados/erros. Um checkbox "forçar reenvio
total" ignora o diff-store e reenvia todos os itens válidos (STATUS=1→PUT, STATUS=0→DELETE),
mesmo inalterados — re-semeia o integrador (perdeu base / diff-store dessincronizado); a idempotência
por versão no integrador absorve os duplicados, e os lotes ≤100 seguem valendo. Em dev (@Requires(env="dev")),
um mock do integrador (@Replaces(IntegradorClient) + PayloadRecorder) grava e exibe os payloads que
sairiam ao PUT/DELETE /api/v1/xadm, sem tocar a rede. O /admin/{id} (detalhe) mostra o detalhe
por-item persistido em webstorm_ecom_ingestao.detalhe (jsonb, V5 / DetalheIngestao): descartados
(motivo), erros de formato, e o que foi enviado e rejeitado pelo integrador — com o payload e
reenvio por item (POST /admin/ingestao/{id}/reenviar/{cod}); descarte não reprocessa item isolado
(dado faltante) → reenvia o snapshot pelo Upload. O detalhe também tem Baixar arquivo (GET
/admin/{id}/arquivo) quando o upload original foi guardado no Garage — a linha carrega
arquivo_ref/arquivo_tipo (V7), retenção de 30 d; ver decisão 0010.
O /api/** é
@Secured("ROLE_API") (Bearer WEBSTORM_API_TOKEN, propriedade app.api-token). Detalhe
operacional: runbook §5.2.
%% caption: Ingestão CSV — adapter no tradutor alimenta a réplica; o sync ao parceiro segue o fluxo existente
flowchart TB
XADM["X-Adm"] -->|POST .zip/CSV| ADP["Adapter (tradutor)<br/>/api/csv/processar"]
ADP -->|diff vs diff-store| ADP
ADP -->|PUT/DELETE /api/v1/xadm| INT["Integrador"]
INT -->|consolida| REP["Réplica estoque"]
INT -->|EstoqueMudouEvent → webhook| SWP["Sweep (tradutor)"]
REP --> SWP
SWP -->|POST /sync/erp| API["Parceiro WebStorm"]
Contrato do parceiro (saída do tradutor)¶
É o que o tradutor produz a partir da réplica e envia ao e-commerce.
%% caption: Fluxo do contrato do parceiro (etapa do tradutor)
flowchart TB
REP["Réplica X-Adm<br/>produto/estoque"] --> REQ["items[] (request)"]
REQ -->|POST /sync/erp| API["API do parceiro"]
API --> MATCH{"EAN existe<br/>na base do parceiro?"}
MATCH -->|sim, 1 SKU| UPD["atualiza preco/estoque<br/>(diff-aware)"]
MATCH -->|nao| NF["nao_encontrados[]"]
MATCH -->|varios SKUs| AMB["ambiguos[]"]
UPD --> RESP["resposta<br/>contadores + itens[]"]
NF --> RESP
AMB --> RESP
RESP --> REC["grava em webstorm_ecom_request<br/>(POST + resposta)"]
Contrato de entrada — request¶
POST https://api.thoms.com.br/sync/erp · Authorization: Bearer <token> em todo request ·
Content-Type: application/json. O corpo aceita 1 produto ou um lote em items[].
Limites: máximo 200 itens por request (acima → HTTP 413); recomendado lotes de 100,
enviados em sequência (não em paralelo). Em tempo real, 1 item por request é o caso ideal.
| Campo | Tipo | Origem (X-Adm) | Obrigatório |
|---|---|---|---|
ean |
string | EAN | sim — chave de match |
codigo_erp |
string | código X-Adm | sim — referência (não é chave de match) |
nome |
string | nome | sim |
preco_cheio |
number | preço a prazo | sim |
preco_desconto |
number | preço a vista | sim |
estoque |
integer | qtde em estoque | sim |
Exemplo (dados fictícios):
{ "items": [
{ "ean": "7891000100103", "codigo_erp": "X-1001", "nome": "Camiseta Thoms Basica P",
"preco_cheio": 159.90, "preco_desconto": 143.91, "estoque": 12 }
]}
Contrato de resposta¶
HTTP 200. Campos de topo:
| Campo | Tipo | Significado |
|---|---|---|
dry_run |
bool | true = simulação (não grava na base do parceiro) |
recebidos |
int | total de itens no lote |
casados |
int | itens localizados na base do parceiro (por EAN) |
atualizados |
int | itens efetivamente atualizados (0 em dry_run) |
nao_encontrados |
string[] | EANs não localizados na base do parceiro |
ambiguos |
string[] | EANs que casaram com mais de um SKU (não esperado — EAN é chave de variante) |
erros |
obj[] | erros por item (ver tabela) |
itens |
obj[] | detalhe dos casados, diff-aware (ver tabela) |
Objeto de itens[] (só dos casados):
| Campo | Tipo | Significado |
|---|---|---|
ean |
string | EAN enviado |
sku |
string | ID interno do produto no parceiro (não existe no X-Adm) |
preco_recebido |
number | preço que enviamos (= preco_cheio) |
estoque_recebido |
int | estoque que enviamos |
preco_atual |
number | preço atual na base do parceiro |
estoque_atual |
int | estoque atual na base do parceiro |
vai_atualizar_preco |
bool | há diferença de preço a aplicar |
vai_atualizar_estoque |
bool | há diferença de estoque a aplicar |
Objeto de erros[]:
| Campo | Tipo | Significado |
|---|---|---|
idx |
int | posição do item no lote (0-based) |
erro |
string | código do erro (ex. sem_ean_nem_sku) |
Exemplo (dados fictícios):
{ "dry_run": true, "recebidos": 2, "casados": 1, "atualizados": 0,
"nao_encontrados": ["7890000000005"], "ambiguos": [], "erros": [],
"itens": [ { "ean": "7891000100103", "sku": "100123",
"preco_recebido": 159.90, "estoque_recebido": 12,
"preco_atual": 149.90, "estoque_atual": 8,
"vai_atualizar_preco": true, "vai_atualizar_estoque": true } ] }
Respostas de erro¶
| Situação | HTTP | Corpo |
|---|---|---|
| Token inválido/ausente | 401 |
{"error":"unauthorized"} |
| JSON malformado | 400 |
{"error":"invalid_json"} |
Lote vazio (items: []) |
400 |
{"error":"empty_payload"} |
| Lote acima de 200 itens | 413 |
payload grande demais |
Observações¶
skué o identificador interno do parceiro, devolvido no match; o X-Adm não o possui — a correlação é sempre por EAN, que no e-commerce é chave de variante (1 EAN = 1 variante; sem ambiguidade — decisão 0002).- A API atualiza apenas preço (cheio) e estoque de variantes existentes. O
nomee opreco_desconto(à vista) são enviados (o contrato aceita), mas não são aplicados hoje: o e-commerce usa opreco_cheioe aplica seu próprio desconto à vista (decisão 0001). POST /sync/erpnão cadastra produto novo — um EAN ainda inexistente no catálogo do parceiro volta emnao_encontrados(não é criado).
4. Mapeamento entrada ↔ dados¶
Como os dados da réplica viram o contrato do parceiro, campo a campo (executado pelo tradutor):
De-para campo a campo entre os dados de produto na réplica do X-Adm (as tabelas que o integrador preenche a partir do que o X-Adm posta) e o JSON do parceiro, executado pelo tradutor. É a fonte única do de-para; a forma dos contratos (request/resposta) está no Modelo de dados.
Contrato do parceiro verificado contra a API real em 2026-06-25 (modo dry-run). A correlação do produto é por EAN (decisão 0002); o mapeamento de preços segue a decisão 0001. As colunas de origem abaixo são as da tabela
estoqueda réplica (estado consolidado do PUT de saída do X-Adm, § Modelo de dados).
Regras gerais¶
- Chave de correlação =
ean. O parceiro localiza o produto pelo EAN; ocodigo_erp(código X-Adm) vai no payload apenas como referência/rastreio — não é usado no match. - Preços:
venda_pz(preço a prazo) →preco_cheio;venda(preço à vista) →preco_desconto. - Valores monetários: o PUT de saída já traz number (ex.
283.13); o request ao parceiro usa ponto e 2 casas. (Na carga inicial por CSV o X-Adm usa vírgula/5 casas — conversão só ali.) STATUS/ ativação (decidido): não vem no contrato contínuo. Produto que zera/inativa é enviado comsaldo = 0(o parceiro mostra esgotado); produto excluído do catálogo não passa pela integração — exclusão é manual no X-Adm e direto na WebStorm.- Chaves: na tabela
estoquea identidade do produto na Thoms écod_prod(chave_estchega null); a correlação com a WebStorm é porean13.ean13éCHAR(13)padded — ler comTRIM. Sem EAN não casa (sem_ean_nem_sku) — tratar antes de enviar. nomeepreco_desconto: enviados sempre (o contrato aceita), mas não aplicados hoje — a API atualiza só preço cheio e estoque; o e-commerce mantém o nome da variante e calcula seu próprio desconto à vista a partir dopreco_cheio(decisão 0001).- Lote:
items[]aceita até 200 itens por request (HTTP 413acima). Enviamos em lotes configuráveis (PARCEIRO_LOTE, default 10; faixa útil 10–100), em sequência (não em paralelo), com pacing entre lotes (PARCEIRO_PACING_MS, default 1s) e circuit breaker: o sweep aborta apósPARCEIRO_MAX_FALHAS(default 3) lotes seguidos falhos e retoma no próximo ciclo. Lote pequeno responde antes doread-timeoutde 20s; se o gargalo do parceiro for nº de requests (proteção/rate-limit) e não tempo por request, subirPARCEIRO_LOTE(menos requests). Tempo real (reprocesso) = 1 item/request; sweep do catálogo (~1.900 variantes) ≈ 190 requests de 10.
De-para — réplica X-Adm → request¶
Origem: colunas da tabela estoque da réplica (estado consolidado do PUT de saída do X-Adm; ver o Modelo
de dados).
Coluna (estoque) |
Tipo real | Campo parceiro | Transformação | Obrigatório |
|---|---|---|---|---|
ean13 |
CHAR(13) | ean |
TRIM, só dígitos (GTIN-13) — chave de match |
sim |
cod_prod |
CHAR(11) | codigo_erp |
TRIM — identidade do produto na Thoms (chave_est null); referência/rastreio, não-match |
sim |
nome_prod |
VARCHAR(168) | nome |
nenhuma | sim |
venda_pz |
NUMERIC(19,5) | preco_cheio |
2 casas | sim |
venda |
NUMERIC(19,5) | preco_desconto |
2 casas | sim |
saldo |
NUMERIC(19,3) | estoque |
inteiro (pode ser negativo) | sim |
O PUT de saída não carrega PESO, ATRIBUTO, promoção nem STATUS — o X-Adm posta
produto/estoque já consolidado. A inatividade chega como saldo = 0 (esgotado); exclusão de
catálogo é manual (X-Adm + WebStorm), fora da integração.
"Obrigatório" (venda_pz/venda/saldo) — o que significa (linhagem 005): na ingestão via
CSV, produto sem VALOR/PROMOCAO/ESTOQUE é descartado pelo adapter (não é enviado ao
integrador) e gera alerta no Sentry (produto incompleto — nunca deveria acontecer); não quebra
o app. Defensivamente, o read-model Estoque tolera null (campos @Nullable): se a réplica
tiver null por outra fonte, o sweep não crasha — envia preco_cheio/preco_desconto null e
estoque 0, em vez de derrubar o lote. Ou seja: "obrigatório" = ausência é anomalia (Sentry), não
falha fatal.
Leitura da resposta — em termos de negócio¶
O parceiro devolve contadores e o detalhe dos casados (forma completa no Modelo de dados); o
tradutor grava a resposta na webstorm_ecom_request:
| Pergunta de negócio | Como ler na resposta |
|---|---|
| Quantos produtos atualizados? | atualizados (em dry_run, ainda 0; use casados para "quantos seriam") |
| Quantos não encontrados? | len(nao_encontrados) — a lista traz os EANs ausentes na base do parceiro |
| Quais não existem no e-commerce? | os de nao_encontrados — a API não cadastra; cadastro de produto novo é fora desta integração |
| Algum EAN ambíguo? | ambiguos — não esperado: o EAN é chave de variante no e-commerce (1 EAN = 1 variante) |
| Algum item rejeitado? | erros[] — {idx, erro} (ex. sem_ean_nem_sku) |
Em produção (fora do dry_run), atualizados reflete o que foi de fato gravado. Hoje o endpoint
responde em dry_run; a virada para produção é do parceiro, ao ativar a integração.
5. Fluxos e processamento (arquitetura)¶
A integração é uma nuvem de 3 hops com persistência auditável nas duas pontas; os dois apps
(integrador e tradutor) compartilham o mesmo banco db_thoms:
%% caption: Arquitetura ponta-a-ponta (3 hops; banco db_thoms compartilhado)
flowchart TB
XADM["X-Adm<br/>(ERP Thoms)"] -->|POST modelo ERP| HUB["Integrador<br/>int.thoms.xadm.biz"]
HUB -->|grava| RE["requests de entrada"]
HUB -->|consolida| REP["réplica produto/estoque<br/>(db_thoms · dono: integrador · +versão)"]
HUB -.->|A: POST 'produto mudou'<br/>Bearer, @Retryable| TR["Tradutor<br/>webstorm-ecom.thoms.xadm.biz"]
REP -->|D: sweep lê versão nova| TR
TR -->|mapeia + POST /sync/erp| API["API do parceiro"]
API -->|resposta| TR
TR -->|grava| RC["webstorm_ecom_request<br/>(db_thoms · dono: tradutor)"]
Dois blocos, donos diferentes, um banco:
- Integrador (
int.thoms.xadm.biz) — recebe o JSON do X-Adm, grava o log de requests de entrada e consolida a réplica (produto/estoque) nodb_thoms. É do projeto integrador (repoxadm/integrador-server, doc em https://docs.xadm.biz/aplicacoes/integrador-server/): app Java/Micronaut multi-tenant (um recurso Coolify por cliente);int.thoms.xadm.bizé o deploy da Thoms. Aqui é referenciado. - Tradutor (
webstorm-ecom.thoms.xadm.biz) — lê a réplica direto nodb_thoms, mapeia para o contrato do parceiro, faz oPOSTe grava awebstorm_ecom_request. É o mini-projeto deste repo: app Micronaut dedicado enxuto (Micronaut Data/JDBC para as tabelas próprias, Views/JTE (decisão 0025) para as telas de auditoria/reprocesso). Serviço próprio também para futuras consultas de preço/produto (ex. bot de WhatsApp). Decisão e alternativas descartadas: decisão 0003.
Transporte integrador → tradutor (§6): como o dado já está na réplica compartilhada, o
integrador só avisa que algo mudou. (A) webhook — POST do integrador numa URL do tradutor
(baixa latência, Bearer + @Retryable); (D) sweep — @Scheduled no tradutor varre a réplica
por versão nova (rede de segurança). Os dois chamam o mesmo código; a idempotência por versão evita
envio duplicado.
Fluxo contínuo (feliz): alteração no X-Adm → POST ao integrador → grava request + consolida
réplica → webhook (A) avisa o tradutor → lê a réplica → mapeia → POST /sync/erp → grava a
resposta na webstorm_ecom_request. Se o webhook se perder, o sweep (D) processa na varredura
seguinte.
Carga inicial: ProdutosSite.csv por e-mail → parceiro importa. Independe do fluxo contínuo.
Bordas: produto sem EAN não casa (sem_ean_nem_sku); EAN inexistente volta em
nao_encontrados (não cadastra); resposta hoje em dry_run; reprocesso da mesma versão é
tratado por idempotência (§6); falha do POST no parceiro é gravada na webstorm_ecom_request e
reprocessada pela tela.
6. Conceitos transversais e configuração¶
- Gatilho = webhook (A) + sweep (D). O dado já está na réplica do
db_thoms(compartilhado); o integrador só avisa. (A) ao consolidar, faz umPOSTnuma URL do tradutor ("produto X mudou", só o id/EAN) — baixa latência. (D) um@Scheduledno tradutor varre a réplica por versão > última enviada — rede de segurança. Padrão Outbox / Polling Publisher: com os dois apps no mesmo banco, o próprio banco é a fila (sem broker, sem sync). - Banco compartilhado
db_thoms. Integrador e tradutor usam o mesmo Postgres. Fronteira: Flyway próprio por app (history dedicada, ex.flyway_schema_history_webstorm_ecom) para as migrations não colidirem; tabelas do tradutor prefixadaswebstorm_ecom_*; a réplica é do integrador — o tradutor a lê, nunca a migra. - Idempotência. Por estado/versão da linha: o tradutor registra na
webstorm_ecom_requesto que já enviou; reprocessar a mesma versão não geraPOSTduplicado. Ordenação não é crítica: a API do parceiro é state-based (seta preço/estoque), last-write-wins — basta at-least-once, garantido pelo sweep. - Entrega confiável. O retry do
POSTao parceiro é@Retryable/@CircuitBreaker(Micronaut); falha persistente fica gravada nawebstorm_ecom_requeste é reprocessada pela tela ou pelo próximo sweep. - Lotes. O tradutor particiona o
POST /sync/erpem lotes de até 100 (limite do endpoint: 200/request). Processamento reativo (webhook/sweep → lê réplica → mapeia →POST→ grava). - Auth. As telas de auditoria/reprocesso ficam atrás do middleware do Coolify. O webhook do integrador é protegido por Bearer app→app (mesma casa).
- Preço à vista × a prazo — decidido. O X-Adm envia sempre os dois preços (a prazo →
preco_cheio, à vista →preco_desconto); como usá-los é decisão da WebStorm. Hoje o site usa o cheio + um desconto à vista padrão (igual para todos) e não consome opreco_descontopor produto — mas o contrato entrega ambos, e a WebStorm pode passar a usar o desconto por produto sem mudança do nosso lado (decisão 0001). - Ativação / exclusão — decidido. Produto que zera ou inativa é enviado com
saldo = 0— o parceiro mostra esgotado (a inatividade é lida pelo saldo, não por um campoSTATUS, que o contrato contínuo não traz). Produto excluído do catálogo não passa pela integração: a exclusão é feita manualmente no X-Adm e direto na WebStorm (a API do parceiro só atualiza preço/estoque, não cadastra nem remove produto). Na réplica, a exclusão no X-Adm marcadeleted→ o tradutor enviasaldo = 0.
Identidade do produto: a identidade na Thoms é
cod_prod(chave_estchega null; a antiga "pendênciacodigo_alt×CodProd" era premissa errada —codigo_alté transporte da PIED,pied_codigo_altno integrador, e não é usado na Thoms). Ver decisão 0013 do integrador.
7. Contratos públicos / API¶
A conclusão da cadeia: o contrato do parceiro (POST /sync/erp), que o tradutor consome. A
forma completa (request, resposta, erros, exemplos) está no Modelo de dados (§3); no site
publicado, também no Swagger interativo (seção API REST do parceiro). A API atualiza
preço/estoque de variantes existentes (match por EAN); não cadastra produto novo; hoje
responde em dry_run (simulação) — a virada para produção é do parceiro.
8. Apêndice — Histórico¶
O histórico de versões (por release) é mantido no CHANGELOG do projeto. As decisões que moldaram a arquitetura, o de-para e a correlação estão registradas em Decisões (0001 preços · 0002 EAN · 0003 arquitetura).
Decisões:
- Mapeamento de preços: a prazo → preco_cheio, à vista → preco_desconto
- Correlação de produto por EAN (codigo_erp é referência)
- Arquitetura: integrador + tradutor, banco compartilhado e transporte por webhook + sweep
- Auth in-app em 3 camadas (micronaut-security) + telas pública/admin
- Adotar a lib xadm-seguranca (motor de auth) — remove as cópias byte-a-byte
- Adotar a lib xadm-comum-web (infra web) — unifica o corpo de erro e a versão/health
- Adotar a lib xadm-mensageria — log de ENTRADA do poke + tela /admin/mensageria
- Watchdog do heartbeat do CSV do X-Adm — alarme GlitchTip na janela útil + tela /admin/monitor
- Guardar o CSV/ZIP da ingestão no Garage + botão de download no detalhe do processamento
- Habilitar GraalVM native-image no tradutor (lado-app: perfil + reflect-config + boot smoke)
- Views de auditoria em JTE (gg.jte) no lugar de Thymeleaf — native-safe
- CI/CD 100% GitHub Actions num pipeline.yml único — adota a central 0027
- Smoke de produção pós-deploy no tradutor — adota a central 0031
- Tela de login servida pela xadm-seguranca — adota a central 0032
Operação
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).
Dev
Guia do código¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-15
Audiência: dev que abriu o repositório e quer passar os olhos e entender como o código do tradutor está organizado, antes de mergulhar classe a classe. Para o porquê das escolhas, veja a arquitetura integrador + tradutor e as demais decisões; para o desenho completo, a Documentação Completa; para rodar e testar, Como rodar.
O retrato em uma frase¶
O tradutor varre a réplica estoque do db_thoms, mapeia cada produto para o contrato da WebStorm e
envia em lotes para POST /sync/erp, gravando cada envio e a resposta para auditoria e reprocesso; ao
lado, recebe o CSV do X-Adm e o repassa ao integrador.
flowchart TB
poke["webhook: POST /api/integrador/produto"] --> sweep["sweep: varre estoque por updated_at"]
agenda["SweepScheduler"] --> sweep
sweep --> mapper["mapper: réplica para item do parceiro"]
mapper --> envio["envio e parceiro: lotes para POST /sync/erp"]
envio --> auditoria["auditoria: webstorm_ecom_request"]
csv["produto: POST /api/csv/processar"] --> integrador["IntegradorClient: PUT e DELETE /api/v1/xadm"]
Como o código é organizado¶
Organização package-by-feature sob br.com.xadm.webstormecom: cada funcionalidade é um pacote de
topo que carrega as próprias camadas, em vez de pastas globais controller//service/.
| Pacote | É | Responsabilidade |
|---|---|---|
sweep |
feature | a varredura da réplica: agenda (SweepScheduler), trava entre instâncias por advisory lock (SweepClusterLock) e uma execução por vez (SingleFlight) |
replica |
feature | leitura da tabela estoque, que é do integrador (só leitura) |
mapper |
feature | o de-para réplica → item do parceiro (ProdutoMapper) |
parceiro |
feature | o cliente HTTP da WebStorm (WebStormClient), o envio em lote e a retentativa em 5xx ou timeout |
envio |
feature | idempotência por versão, classificação da resposta, registro do envio e reprocesso |
auditoria |
feature | a trilha webstorm_ecom_request, a leitura que as telas consomem (AuditoriaService) e a limpeza por retenção (CleanupJob) |
webhook |
feature | o poke do integrador (POST /api/integrador/produto, ROLE_API) |
produto |
feature | a ingestão do CSV do X-Adm (POST /api/csv/processar): extrai o zip, calcula o diff contra o último snapshot e repassa ao integrador; a leitura da trilha de processamentos é do IngestaoService |
arquivo |
feature | a guarda do upload original no Garage (ArquivoStore) |
monitor |
feature | o watchdog do heartbeat do CSV (Monitor) e a tela /admin/monitor |
views |
feature | as telas JTE: a trilha de envios (/) e o admin (/admin), que leem pelo serviço da feature |
security |
base | a regra de acesso das views (ViewSecurityRule, ViewWhitelist) e as respostas de erro de auth |
dev |
base | apoio de desenvolvimento: seed, mock do integrador e gravação de payload |
Os templates JTE ficam em src/main/jte/; o layout padrão X-Adm vem do kit em src/main/jte/kit/.
As migrations do Flyway (src/main/resources/db/migration/) criam só as tabelas webstorm_ecom_*.
As camadas¶
Uma requisição ou um disparo agendado entra por um controller ou scheduler, passa pelo serviço da
feature e chega ao repositório micronaut-data (@JdbcRepository). A tela recebe um view-model, não a
entidade. O que é transversal vem das libs da casa: login e sessão (xadm-seguranca), /health e
GlitchTip (xadm-comum-web), registro das mensagens m2m e a tela /admin/mensageria
(xadm-mensageria) e o S3Client do Garage (xadm-comum-storage).
Travas de arquitetura¶
O ArquiteturaTest, dentro do ./gradlew check, guarda cinco invariantes:
- sem ciclo de dependência entre os pacotes de feature (
RegrasArquitetura.semCiclosEntreFatias); - controller não acessa repositório: a persistência fica atrás do serviço da feature
(
RegrasArquitetura.controllerNaoAcessaRepository); - nada depende de controller — o domínio, o parsing e a infra não conhecem a borda HTTP
(
RegrasArquitetura.nadaDependeDeController); - todo
@Post,@Putou@Patchde controller de view declara@Consumes— sem ele o submit do formulário devolve 415; - o import de classes não pode vir vazio, senão as regras acima passariam sem testar nada.
As guardas estáticas do job gate do pipeline.yml completam a lista (placeholders do Micronaut,
paridade dos Dockerfiles, reflect-config da JTE no native, libs no piso, entre outras).
Por onde começar a ler¶
Application— o entrypoint; inicializa o Sentry antes do contexto, para erro de boot chegar ao GlitchTip.src/main/resources/application.yml— toda a configuração e as envs de cada feature.- Uma feature ponta a ponta:
WebhookController→Sweep→ProdutoMapper→EnvioParceiro→RegistradorEnvio.
Referência completa¶
Este app não publica Javadoc em dev/api/: a referência de classe é o próprio código, e a narrativa
acima é o atalho para achar por onde entrar.
Como rodar, testar e buildar¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-15
Audiência: dev que vai mexer no tradutor na própria máquina. Para entender o código antes, veja o Guia do código; para operar em produção, o runbook de implantação.
Pré-requisitos¶
- JDK 25 — a versão do
docs/app.json→toolchain.java. O Gradle vem do wrapper (./gradlew). - Docker no ar — os testes de integração sobem o Postgres de teste (o singleton da
xadm-comum-teste,postgres:18-alpine) e o S3Mock por Testcontainers. Sem Docker, a suíte de integração falha dizendo isso. - Lib da casa ainda não publicada — publique-a no
~/.m2a partir doxadm-commons(./gradlew publishToMavenLocal). O build lê omavenLocaldepois do registro e só parabr.com.xadm: a versão que já está no registro sempre vem de lá.
Testar¶
./gradlew check
É o mesmo comando do job gate do pipeline.yml: Checkstyle da casa, testes unitários e de
integração. Os @MicronautTest estendem IntegracaoComPostgres, que aponta o datasource para o
Postgres de teste e zera as tabelas do app antes de cada teste. Teste travado reprova: 2 min por
teste, 15 min para a task inteira.
Cobertura consolidada (relatório em build/reports/jacoco/):
./gradlew cleanTest test jacocoTestReport --rerun-tasks
O boot da imagem native tem teste próprio (@Tag("native")), fora do check porque exige a imagem
pré-buildada:
./gradlew dockerBuildNative -PnativeQuick --no-configuration-cache
./gradlew nativeSmoke
Subir o app¶
./gradlew run
Precisa de um Postgres com a tabela estoque do integrador; o Flyway do tradutor cria só as suas
webstorm_ecom_*, com histórico próprio (flyway_schema_history_webstorm_ecom). Variáveis mínimas:
| Variável | Para quê |
|---|---|
DATASOURCES_DEFAULT_URL |
JDBC do db_thoms (ex. jdbc:postgresql://localhost:5432/db_thoms) |
DATASOURCES_DEFAULT_USERNAME / DATASOURCES_DEFAULT_PASSWORD |
credencial: lê estoque, escreve webstorm_ecom_* |
WEBSTORM_API_TOKEN |
Bearer ROLE_API que o integrador (poke) e o X-Adm (CSV) mandam ao tradutor |
PARCEIRO_URL / PARCEIRO_API_TOKEN |
API da WebStorm (POST /sync/erp) |
INTEGRADOR_URL / INTEGRADOR_API_TOKEN |
integrador, destino da ingestão do CSV |
CLIENTE |
Thoms (tag dos erros no GlitchTip) |
Login (AUTH_*), Garage (GARAGE_*), watchdog (MONITOR_*) e o ajuste do lote ao parceiro
(PARCEIRO_LOTE, PARCEIRO_PACING_MS, PARCEIRO_MAX_FALHAS) estão no
runbook de implantação. O /health responde
{status, versao, flavor, commit}.
Buildar¶
./gradlew shadowJar # build/libs/app.jar
docker build -f Dockerfile.native -t webstorm-ecom:native . # a imagem que vai a produção
docker build -t webstorm-ecom:jar . # a imagem JVM, fallback
Produção usa a imagem native buildada pelo job build_native do pipeline.yml na tag vX.Y.Z,
fora do host de produção; o Coolify só puxa.
Documentação¶
mkdocs build --strict
O job docs do pipeline.yml baixa antes do build os hooks (scripts/frontmatter-cabecalho.py e
scripts/visao-tecnica.py) e os fatos importados do integrador (docs/_importado/); para buildar
local, baixe-os com os mesmos comandos do job.
Release¶
Pela /xadm-release: bump SemVer, CHANGELOG e tag. A tag builda o native, deploya pelo
control-plane com o gate e o release-check verdes, e o smoke confere produção.
Público
API de sincronização — POST /sync/erp¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-06-26
Contrato do endpoint do parceiro que recebe preço e estoque dos produtos do X-Adm. Atualiza
variantes existentes (correlação por EAN); não cadastra produto novo. Hoje responde em
dry_run (simulação — não grava).
A referência interativa abaixo (Swagger UI) traz os tipos de envio e retorno, com exemplos.
Como usar via curl¶
Envio (1 produto; dados fictícios — troque <TOKEN> pelo token do parceiro):
curl -X POST https://api.thoms.com.br/sync/erp \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"ean": "7891000100103",
"codigo_erp": "X-1001",
"nome": "Camiseta Thoms Basica P",
"preco_cheio": 159.90,
"preco_desconto": 143.91,
"estoque": 12
}
]
}'
Resposta (exemplo, dry_run):
{
"dry_run": true,
"recebidos": 1,
"casados": 1,
"atualizados": 0,
"nao_encontrados": [],
"ambiguos": [],
"erros": [],
"itens": [
{
"ean": "7891000100103", "sku": "100123",
"preco_recebido": 159.90, "estoque_recebido": 12,
"preco_atual": 149.90, "estoque_atual": 8,
"vai_atualizar_preco": true, "vai_atualizar_estoque": true
}
]
}
Regras rápidas¶
- Auth:
Authorization: Bearer <TOKEN>em todo request. - Lote: 1 a 200 itens por request (acima →
413); recomendado 100 por lote, em sequência. Em tempo real, 1 item por request. - Preços:
preco_cheio= preço a prazo ·preco_desconto= preço à vista (promocional). - Leitura da resposta:
casados/atualizados,nao_encontrados(lista de EANs),erros[]({idx, erro}, ex.sem_ean_nem_sku). dry_run: truehoje = nada é gravado; a virada para produção é do parceiro.
Não exponha o token em páginas, prints ou repositórios públicos.
Decisões
0001 — Mapeamento de preços (a prazo → cheio, à vista → desconto)¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-08-11 · Decidido em: 2026-06-25
Contexto¶
O X-Adm tem dois preços por produto — preço a vista e preço a prazo — e a API do parceiro
espera dois campos — preco_cheio e preco_desconto. A especificação do parceiro não disse
qual preço do X-Adm alimenta qual campo, então era preciso decidir o de-para.
Decisão¶
VALOR(preço a prazo, o maior) →preco_cheioPROMOCAO(preço promocional / à vista) →preco_desconto
Consequências¶
- Confirmado pela definição oficial do export X-Adm (
ProdutosSite.csv/ PDFTHOMS2026_413248): a colunaVALORé o preço a prazo ePROMOCAOé o preço promocional / à vista; nas linhas reaisVALOR > PROMOCAO. - Coerente com a convenção de varejo no Brasil: preço cheio = a prazo; preço com desconto = à vista.
- Coerente com o exemplo da spec do parceiro, em que
preco_descontofica ~10% abaixo depreco_cheio. - Confirmado na verificação contra a API real (2026-06-25): o campo
preco_recebidoda resposta ecoou opreco_cheioenviado — o parceiro trata opreco_cheiocomo preço de referência. - Uso do
preco_desconto— decidido (2026-08-11): o ERP envia sempre os dois preços (a prazo →preco_cheio, à vista →preco_desconto), e como usá-los é decisão da WebStorm. Hoje o e-commerce usa opreco_cheioe aplica um desconto à vista padrão (igual para todos), sem consumir opreco_descontopor produto — mas o contrato entrega ambos, então a WebStorm pode passar a usar o desconto por produto quando quiser, sem mudança do nosso lado. Não há pendência aberta.
0002 — Correlação de produto por EAN¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-06-25 · Decidido em: 2026-06-25
Contexto¶
O payload envia ean e codigo_erp (código X-Adm). Era preciso saber qual deles o parceiro
usa para localizar o produto na base dele — e, portanto, dizer se foi atualizado ou não encontrado.
Decisão¶
A correlação é por ean. O codigo_erp vai no payload apenas como referência/rastreio e
não participa do match.
Consequências¶
- Verificado contra a API real (2026-06-25): um item só com
codigo_erp(semean) retorna o errosem_ean_nem_sku; e a listanao_encontradosdevolve EANs, não códigos X-Adm. - O parceiro também aceita o
skuinterno dele como chave, mas o X-Adm não possui essesku— logo a chave prática é o EAN. - Requisito de dado: todo produto a sincronizar precisa de EAN válido no X-Adm; produto sem EAN não casa (cai em erro / não-encontrado).
- Sem ambiguidade (confirmado com o parceiro): no e-commerce o EAN é chave de variante (cada
variante — cor/tamanho — tem seu próprio EAN), então 1 EAN = 1 variante; o campo
ambiguosda resposta tende a ficar vazio.
0003 — Arquitetura: integrador + tradutor (banco compartilhado, webhook + sweep)¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-08-11 · Decidido em: 2026-07-15
Contexto¶
A integração leva preço e estoque dos produtos do X-Adm ao e-commerce do parceiro WebStorm
(https://www.webstorm.com.br), via POST /sync/erp. Era preciso decidir por onde os dados
trafegam entre o X-Adm e a API do parceiro e quem faz cada parte, com dois requisitos: (a) o X-Adm envia no seu próprio modelo
ERP de produto/estoque; (b) precisamos de auditoria do que entrou e do que saiu (com a
resposta).
Duas restrições moldam a solução:
- Um app dedicado (o tradutor) tem valor além do
POST. Sendo um serviço próprio, ele é a casa natural de futuras ferramentas que consultam preço/produto (ex. um bot de WhatsApp que responde consultas). Por isso mantemos o tradutor como app separado, não uma rotina embutida. - Os dois apps compartilham o mesmo banco
db_thoms. O tradutor lê a réplica direto no Postgres — não precisa de um canal que transporte o dado até ele.
Decisão¶
Nuvem de 3 hops — X-Adm → integrador → parceiro — com dois apps sobre o mesmo banco db_thoms:
X-Adm ──POST (modelo ERP)──▶ Integrador (int.thoms.xadm.biz)
├─ grava request de entrada
├─ consolida a RÉPLICA produto/estoque (db_thoms, com coluna de versão)
└─ (A) POST "produto X mudou" ─────────▶ Tradutor (webstorm-ecom.thoms.xadm.biz)
├─ lê a réplica DIRETO no db_thoms
├─ mapeia → POST /sync/erp (@Retryable)
└─ grava webstorm_ecom_request (envio + resposta)
(D) Tradutor também varre a réplica por @Scheduled: versão > última enviada → processa
Blocos e donos¶
- Integrador (
int.thoms.xadm.biz) — recebe o JSON do X-Adm, grava o log de requests de entrada e consolida a réplica (produto/estoque) nodb_thoms. É do projeto integrador (repoxadm/integrador-server, https://docs.xadm.biz/aplicacoes/integrador-server/; app Java/Micronaut multi-tenant, um deploy por cliente), mesmo time; aqui é referenciado. - Tradutor (
webstorm-ecom.thoms.xadm.biz) — app Micronaut dedicado enxuto: lê a réplica direto nodb_thoms, mapeia para o contrato do parceiro, faz oPOST /sync/erpe gravawebstorm_ecom_request. Micronaut Data/JDBC para as tabelas próprias, Views/JTE (decisão 0025 da casa) para as telas de auditoria/reprocesso. É o mini-projeto deste repo.
Banco compartilhado db_thoms — fronteira de dados¶
Os dois apps usam o mesmo banco. Para não colidirem:
- Flyway próprio por app. Cada app tem sua própria history table e seu próprio conjunto de
migrations (
locations). O tradutor usa uma history dedicada (ex.flyway_schema_history_webstorm_ecom), de modo quemigratede um app não vê nem altera o histórico do outro. Sem isso, um Flyway trata as migrations do outro como "faltando" e quebra. - Prefixo
webstorm_ecom_*. Todas as tabelas do tradutor são prefixadas (webstorm_ecom_request, etc.) — namespace legível no banco compartilhado, zero colisão de nome. - A réplica é do integrador — read-only para o tradutor. O Flyway do tradutor gerencia só as
tabelas
webstorm_ecom_*. As tabelas da réplica pertencem ao integrador (ele as migra); o tradutor as lê, nunca as migra. - A réplica já existe: a tabela
estoque. O integrador é um espelho do X-Adm e já mantémestoque, alimentada para a Thoms pelo PUT JSON de saída do X-Adm (igual Sul Plata/OnPetro) — não pelo fluxo de entrada PIED (Maxsul). Identidade do produto na Thoms =cod_prod(chave_estchega null); correlação WebStorm porean13; colunasnome_prod/venda/venda_pz/saldo. A colunapied_codigo_alt(transporte PIED) vem null na Thoms. A integração WebStorm lê essa tabela (read-only) — não cria tabela nem coluna de produto. Tipos reais: § Modelo de dados; modelo de identidade: decisão 0013 do integrador. updated_atnaestoque(feito). O sweep (D) varre por coluna de mudança; o integrador adicionouupdated_at TIMESTAMPTZ(+deleted) naestoque, bumpado no apply do X-Adm. O read-modelEstoquedo tradutor já lê ambas; o sweep usaupdated_at > watermark.
Transporte integrador → tradutor: webhook (A) + sweep (D)¶
Como o dado já está na réplica compartilhada, o integrador só precisa avisar que algo mudou — e mesmo o aviso não precisa ser confiável, porque o banco é a rede de segurança:
- (A) Webhook — caminho de baixa latência. Ao consolidar, o integrador faz um
POSTnuma URL do tradutor ("produto X mudou", só o id/EAN), protegido por Bearer entre os dois apps e com@Retryable. O tradutor lê a réplica, mapeia, posta e grava. Reusa o padrão de notificação assíncrona (push) que o integrador já opera. - (D) Sweep — rede de segurança. Um
@Scheduledno tradutor varreestoquepor linhas cujaupdated_at> última processada (cruzando comwebstorm_ecom_request) e processa. Pega tudo que o webhook perdeu (tradutor fora do ar,POSTfalho, aviso sumido). É o padrão Outbox / Polling Publisher: com produtor e consumidor no mesmo banco, o próprio banco é a fila.
Convergência: (A) e (D) chamam o mesmo código (ler réplica → mapear → POST → gravar). A
idempotência por versão no webstorm_ecom_request garante que os dois não duplicam envio.
Conceitos transversais¶
- Idempotência por estado/versão da linha: o
webstorm_ecom_requestregistra o que já foi enviado; reprocessar a mesma versão não geraPOSTduplicado. A API do parceiro é state-based (seta preço/estoque), last-write-wins — só precisamos de at-least-once, que o sweep garante. - Ordem = um escritor por vez (advisory lock de cluster). O sweep é single-flight em dois níveis:
lock em memória (
SingleFlight, coalesce dentro de 1 JVM) e advisory lock do Postgres (SweepClusterLock→pg_try_advisory_lock(hashtext('webstorm_ecom_sweep'))) que serializa o corpo do sweep ENTRE instâncias nodb_thomscompartilhado. Envios ao mesmo produto saem seriais, cada um relendo o estado atual da réplica (uma linha por produto), então o parceiro converge para o último valor mesmo com falhas/retries. O payload do parceiro não tem guarda de versão — ele não rejeita escrita velha —, então a ordem depende inteiramente de haver um único escritor por vez; o advisory lock garante isso mesmo com N instâncias idênticas (as demais coalescem ao próximo ciclo/poke). É HA/failover: chave por nome (namespaced, não colide com o integrador no mesmodb_thoms), lock de sessão → se a detentora cai, a sessão morre e o lock solta sozinho, e outra instância assume. Antes o invariante era "rodar em 1 réplica" (só o lock em-memória); o advisory lock removeu essa restrição — escalar horizontal é seguro para o sweep. A guarda de versão por linha continua vindo dopendentes()(updated_at > MAX(enviado com sucesso)), agora livre de corrida entre instâncias. - Retry + circuit breaker (parceiro vivo, mas afoga sob carga). Sintoma observado: reprocesso de
1 item = HTTP 200 na hora, mas o sweep em lote estoura
ReadTimeout100% (o parceiro para de responder sob a rajada — provável proteção/rate-limit disparada pelo "forçar reenvio total"). Então: o client re-tenta só transporte (@Retryable, 2 tentativas — retry demais amplifica), comread-timeoutde 20s (service-idwebstorm-parceiro; não 60s: parceiro que blackholeia só seguraria a conexão mais tempo). Quem realmente protege é o circuit breaker noSweep: após N lotes seguidos falhos (PARCEIRO_MAX_FALHAS, default 3) ele aborta o ciclo; os pendentes não avançam o watermark e retomam no próximo sweep, dando fôlego ao parceiro em vez de queimar o catálogo inteiro em timeouts. Falha persistente fica gravada nowebstorm_ecom_request(auditoria). - Lotes: o
POST /sync/erpé particionado em lotes configuráveis (PARCEIRO_LOTE, default 10; faixa útil 10–100, limite do endpoint 200), com pacing entre lotes (PARCEIRO_PACING_MS, default 1s). Lote pequeno responde bem antes doread-timeout; o pacing evita o burst martelar o parceiro lote atrás de lote. Calibrar com o parceiro: se o teto for nº de requests (rate-limit) e não tempo por request, subir o lote (menos requests) em vez de descer — tudo via env, sem redeploy. dry_rundo parceiro. As respostas 200 vêm comdry_run:true/atualizados:0(modo homologação da WebStorm) — mesmo o sucesso não altera preço/estoque no e-commerce. A virada para produção é do parceiro (WebStorm), não deste lado — sem ação nossa.- Auth: as telas de auditoria ficam atrás do middleware do Coolify. O webhook do integrador é protegido por Bearer app→app (mesma casa).
- Carga inicial (semear a base do parceiro) segue por CSV/e-mail, independente do fluxo contínuo.
Consequências¶
- App dedicado preservado: o tradutor é um serviço próprio (base para o bot de WhatsApp e outras consultas de preço/produto), mas sem as peças pesadas de sync.
- Zero infra de transporte nova: sem broker, sem serviço de sync. O par banco compartilhado + sweep entrega confiabilidade; o webhook só melhora a latência.
- Acesso direto ao dado: o tradutor lê a réplica no
db_thomssem intermediário — o mesmo acesso que a futura ferramenta de consulta vai usar. - Fronteira de dados explícita: Flyway por app + prefixo
webstorm_ecom_*+ réplica read-only mantêm dois donos sobre um banco sem pisar um no outro. - Fronteira do parceiro inalterada: a API atualiza preço/estoque de variantes existentes
(match por EAN, decisão 0002); não cadastra produto novo; hoje responde em
dry_run.
Alternativas descartadas¶
- PowerSync (ponte de sync no tradutor) — desenho anterior desta decisão. Existe para levar o
dado a um consumidor sem acesso ao banco da fonte. Com
db_thomscompartilhado, o tradutor lê a réplica direto — a premissa cai. Sobraria SQLite local espelhando dado do mesmo Postgres + bridge Kotlin (PowerSync não tem SDK Java) + serviço de sync: overhead injustificado. - Broker de fila (RabbitMQ/Kafka/JMS) — daria durabilidade/retry prontos, mas seria um servidor a mais para fornecer a durabilidade que o Postgres compartilhado já dá. Justifica só se o banco deixar de ser compartilhado ou surgir fan-out para muitos consumidores. É uma troca de mensagem simples (um tipo, um produtor, um consumidor) — não pede broker.
LISTEN/NOTIFYdo Postgres — nativo e real-time, mas sem persistência (o notify some se ninguém escuta): precisaria do sweep de catch-up de qualquer forma. Complexidade de LISTEN sobre o mesmo D — pior custo/benefício que A+D.- Webhook sozinho (sem sweep) — fire-and-forget perde mensagem se o tradutor estiver fora do ar. Por isso o webhook é otimização de latência sobre o sweep, nunca a garantia de entrega.
- Rotina embutida no integrador (sem app separado) — mais simples para só o
POST, mas o tradutor separado é requisito (casa das consultas de preço/produto). - Um 2º parceiro de e-commerce não está no escopo — o tradutor implementa o contrato deste parceiro direto; refatora-se se surgir.
Adendo (2026-07) — CSV vira o fluxo de entrada; o tradutor ganha porta de ingestão¶
O X-Adm decidiu não enviar produto item-a-item via JSON: passa a POSTar o catálogo inteiro em
CSV (ProdutosSite.csv, dentro de um .zip). Isto não substitui o transporte
integrador→tradutor (webhook + sweep, acima), que segue intacto — muda de onde a réplica é
alimentada. Um adapter no tradutor (POST /api/csv/processar) recebe o zip, faz diff contra
estado próprio e transcodifica para o PUT/DELETE /api/v1/xadm do integrador (se passando pelo
X-Adm). O integrador consolida a réplica e emite EstoqueMudouEvent — daí o fluxo é o mesmo desta
decisão (sweep → POST /sync/erp).
Consequências: (a) o tradutor deixa de ser só leitor da réplica — ganha uma porta de
ingestão; (b) o adapter é uma ponte temporária (quando o X-Adm postar JSON direto no
integrador, deleta-se o adapter e nada mais muda); (c) STATUS=0 do CSV vira soft-delete no
integrador → o tradutor envia saldo = 0 (é assim que a ativação/STATUS é tratada — decidido).
Detalhe do adapter (diff-store, lotes,
at-least-once, paridade de campos): Modelo de dados § Ingestão de produtos via CSV. Linhagem de
trabalho: .ia/005-ingestao-csv-catalogo-*.
0005 — Auth in-app em 3 camadas + telas de auditoria¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-28 · Decidido em: 2026-07-28
Atualização (08/2026, decisão 0006): o motor de auth descrito aqui deixou de ser cópia local e passou a vir da lib
br.com.xadm:xadm-seguranca(pacotebr.com.xadm.comum.seguranca). O modelo de 3 camadas segue igual; só mudou a origem do código. A policy de rota app-específica (whitelist,/webhook/, views públicas por UUID) vive agora emsecurity/ViewWhitelist(local). Oissuerdo cookie virou config (AUTH_ISSUER).
Contexto¶
O tradutor rodava com micronaut-security desligado: as telas de auditoria ficavam atrás do
middleware do Coolify e os endpoints de máquina usavam filtros Bearer dedicados
(ProdutoAuthFilter, WebhookAuthFilter). A operação precisava de três níveis de acesso:
API de máquina, área admin (reprocesso/processamentos) e uma tela pública para a equipe
Thoms/WebStorm ver a auditoria sem login. Com o Coolify na frente de tudo não dá para abrir um path
anônimo — a tela pública exige auth in-app.
Decisão¶
Portar o padrão de segurança in-app do onpetro (bi-comercial-xls) e ligar o
micronaut-security com tri-estado (ViewSecurityRule): enabled (envs AUTH_* presentes)
exige login; bypass em dev/test; fail-closed (503) em prod sem envs. Três camadas:
| Camada | Rotas | Mecanismo |
|---|---|---|
| API (máquina) | /api/csv/processar, /api/integrador/produto (+ alias /webhook/estoque-mudou) |
@Secured("ROLE_API") + StaticBearerTokenValidator (Bearer API_BEARER_TOKEN) |
| Pública (anônima) | / e /{id} (payloads/saída, valores no detalhe) |
ViewSecurityRule retorna ALLOWED direto (ViewWhitelist.isPublicView) |
| Admin (humano) | /admin, /admin/{id}, /admin/produto/{cod}/reprocessar |
login Google @xadm.com.br (Firebase xadm-6ab81, cookie xadm_session); reprocesso com CSRF |
- Um
API_BEARER_TOKENcompartilhado consolida os dois Bearers de entrada antigos (PRODUTO_TOKENdo X-Adm +WEBHOOK_TOKENdo integrador). - Convenção nova: todo callback integrador→app fica sob
/api/integrador/<recurso>(este poke =/api/integrador/produto). O/webhook/estoque-mudouvira alias temporário até o integrador migrar (o poke não cai no deploy; o sweep é a rede).
Consequências¶
- Telas saem de trás do Coolify (auth é do app); em prod, sem
AUTH_*→ 503 (fail-closed), por isso o deploy tem que setar osAUTH_*. - O integrador tem que mandar o
API_BEARER_TOKEN(não maisWEBHOOK_TOKEN) e migrar o path do poke — coordenação cross-repo, coberta pelo alias. - A tela pública expõe preço/estoque completos (decisão do usuário; dado da Thoms).
- Auditoria em duas trilhas sem join por
lote_id:/(payloads/saída) ×/admin(processamentos/entrada,webstorm_ecom_ingestao) — a saída é async por produto/versão.
Alternativas descartadas¶
- Manter só o Coolify na frente — zero código, mas impede a tela pública anônima (tudo ficaria atrás do guard).
- Híbrido Coolify + rota anônima — dois modelos de auth convivendo + config de proxy fina; frágil.
- Dois tokens de API (emissores separados) — mais higiênico, mas o usuário optou por um token só (simples, igual onpetro).
- Pattern greedy no
intercept-url-map(/*isAnonymous) para o detalhe público — exporia o/admin(o intercept-url-map, ordem -100, ganha daViewSecurityRule). Por isso aViewSecurityRuleretornaALLOWEDdireto para/e/{uuid}.
Linhagem de trabalho: .ia/006-auth-camadas-telas-auditoria-*.
0006 — Adotar a lib xadm-seguranca (motor de auth)¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-11 · Decidido em: 2026-08-04
Contexto¶
O motor de auth das telas (decisão 0005) nasceu portado byte-a-byte
do onpetro/bi-comercial-xls — 9 classes de motor em br.com.xadm.webstormecom.security. A casa
extraiu esse motor para a lib br.com.xadm:xadm-seguranca (pacote br.com.xadm.comum.seguranca;
ADR 0019 no repo central). Como o validador do IdP Firebase compartilhado (xadm-6ab81) é 1-de-N, o
drift entre as cópias vira exposição cross-client — cada app que mantém cópia é uma superfície a
mais. O onpetro já adotou (app-piloto, e2e de paridade verde em prod).
Decisão¶
webstorm-ecom depende da xadm-seguranca (registro Maven Forgejo, org xadm pública →
leitura anônima) — adotada na 0.2.0; a versão corrente é a do build.gradle.kts (fonte única,
hoje 0.7.1) e deleta as 9 cópias do motor. Lift-and-shift: comportamento de auth idêntico ao
pré-extração (Firebase → xadm_session → rota protegida → CSRF), provado por AuthE2EIT.
- A policy de rota fica no app, não na lib —
security/ViewWhitelist(local) guarda o que é app-específico do thoms: a whitelist de views, o alias/webhook/(poke do integrador, linhagem 006) e as views públicas por UUID (isPublicView/UUID_RAIZ). A lib traz só o contrato (AUTH_*_ATTR,SESSION_COOKIE,deveRecusarAnonimo,isHttps) + os beans do motor. Não unificar/webhook//isPublicViewna lib: não existem nos outros apps (policy per-app). - Issuer corrigido — a cópia carregava
iss = "bi-comercial-xls"hardcoded (nome do onpetro). Com a lib o issuer virou config (AUTH_ISSUER, defaultwebstorm-ecom). Corrigido na adoção: oisspassa a identificar o app certo. - Swap atômico — a dependência e a deleção das cópias entram no mesmo commit: com as cópias E
a lib no classpath haveria dois beans do mesmo tipo (
@ConfigurationProperties("auth"),AuthenticationFetcher,SecurityRule) e o DI não-único quebra o startup.
Consequências¶
- Um re-login forçado no primeiro deploy: sessões vivas assinadas com
iss=bi-comercial-xlsfalham a verificação (o segredo de sessão do thoms já é próprio, então a severidade é baixa — defesa em profundidade). TTL de sessão é 8h e o deploy já reinicia o app. VerAUTH_ISSUERna implantação. - Atualização do motor (correções de segurança) passa a vir por bump de versão da lib, não por editar cópia — é o ganho central.
AuthExceptionHandler,ViewRejectionHandler(dependem doProblemDetaillocal, RFC-7807) eApiTokenGuard(guard de ingestão, não é auth-user) ficam locais — só repontam imports do motor.Superado em parte pela decisão 0007: o
ProblemDetaillocal foi depois eliminado e os dois handlers passaram a usar o da libxadm-comum-web.ApiTokenGuardsegue local. Atualização 2026-09-11 — oApiTokenGuardfoi promovido à lib. Axadm-seguranca0.7.2 trouxe oBearerTokenGuard(@Context), que faz o mesmo:app.api-tokendeclarado e vazio recusa o boot fora de dev/test com a segurança ligada, e em dev/test só avisa. A migração da lib cita este repo; oApiTokenGuarde o teste dele saíram no mesmo commit do bump — mantê-los deixaria dois donos da mesma checagem de boot.
Alternativas descartadas¶
- Manter as cópias — zero trabalho, mas mantém o drift cross-client (o motivo da extração).
- Preservar
iss=bi-comercial-xls— zero re-login, mas carrega o bug adiante. Como a adoção já reinicia o app e o impacto é 1 re-login, o momento de corrigir é agora. - Unificar a whitelist/
isPublicViewna lib — reduz duplicação aparente, mas/webhook/e as views por UUID são policy do thoms; forçá-las na lib acopla apps que não as têm.
Linhagem de trabalho: .ia/008-adota-xadm-seguranca-*.
Atualização 2026-08-19 — bump de manutenção 0.3.0 → 0.4.0¶
Acompanhamento do latest do registro (política da casa). Source-compatível: nenhum import ou
assinatura do motor mudou neste app, zero adaptação de código, gate da stack verde. Sem efeito
operacional (nenhum re-login, nenhuma config nova). A ViewSecurityRule local segue por desenho.
0007 — Adotar a lib xadm-comum-web (infra web)¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-11 · Decidido em: 2026-08-05
Contexto¶
O webstorm-ecom estava meio-migrado na infra web. Coexistiam dois corpos de erro: um
ProblemDetail local (record de 5 campos, RFC-7807 completo, usado pelos handlers de auth) e um
Problema pobre (record de 3 campos, montado à mão pelo ProdutoController). Havia ainda cópias de
VersaoInfo (versão em runtime) e SaudeController (/health). A adoção do xadm-seguranca
(decisão 0006) deixou AuthExceptionHandler/ViewRejectionHandler
apontando para o ProblemDetail local, com a re-extração para a lib adiada para esta fase.
A casa extraiu esse núcleo web para a lib br.com.xadm:xadm-comum-web (pacote
br.com.xadm.comum.web; ADR 0019 no repo central). O ProblemDetail da lib é idêntico ao local
(mesmo record, mesmo .of(HttpStatus, String, String), mesmo CONTENT_TYPE) — dois corpos de erro
divergentes são dívida ativa e superfície de drift cross-client.
Decisão¶
webstorm-ecom depende da xadm-comum-web (registro Maven Forgejo, org xadm pública) —
adotada na 0.1.0; a versão corrente é a do build.gradle.kts (fonte única, hoje 0.8.0) e
elimina as cópias locais da infra web:
- Corpo de erro único — deletados
dto/ProblemDetaillocal ecomum/Problema.AuthExceptionHandler/ViewRejectionHandlerrepontam o import para oProblemDetailda lib (lógica idêntica). OProdutoControllerpassa a montar o corpo viaProblemDetail.of(status, status.name(), detail)+ProblemDetail.CONTENT_TYPE— ganha os camposdetail/codeque oProblemanão tinha. Mudança de campo: a mensagem, que ia notitle, passa paradetail; otitlevira a razão do status e entra ocodeestável — é o efeito da unificação (provado porProdutoControllerIT, que asserecode+statusno corpo do 400). - Versão e health da lib — deletados a
VersaoInfo(raiz) e ocomum/SaudeControllerlocais. A versão vem daVersaoInfoda lib (@Singleton implements InfoSource; também exposta em/info); o/healthpassa a ser oHealthControllerda lib (@Secured(IS_ANONYMOUS), mesmo shape{status, versao}). O/healthjá era anônimo nointercept-url-mape noViewWhitelist. - Fallback de versão — quando
version.propertiesfalta, o canônico da lib é"desconhecida"(o local era"dev"). Só observável sem o arquivo gerado; nos testes ele existe no classpath.
Fica local (não migrado nesta fase)¶
SentryInitializer— a versão do thoms tem extras app-específicos (tagcliente, skip porMICRONAUT_ENVIRONMENTS,sendDefaultPii,diagnosticLevel) ausentes na lib. Parametrizar a lib para absorvê-los é follow-up, não este PR. (Resolvido em 2026-08-11 — ver Atualização abaixo.)- Error-flow — o thoms não tem
UnifiedErrorResponseProcessor(handlers distribuídos) nem hierarquia de exceção HTTP. Centralizar o error-flow na lib é refactor maior, fora deste lift.
Consequências¶
- Correções/evoluções do corpo de erro e do health passam a vir por bump de versão da lib, não
por editar cópia — é o ganho central (mesmo racional do
xadm-seguranca). - Nova superfície
/info(management, anônima via/management/**) com a versão — inócua, subproduto doInfoSourceda lib. xadm-comum-webcontinua em0.1.0(ainda pré-1.0.0): esta é a primeira adoção — a API estabiliza quando ela assentar.
Alternativas descartadas¶
- Manter os dois corpos de erro — zero trabalho, mas perpetua a dívida (o motivo da extração) e
deixa o
ProdutoControllerfora do contrato RFC-7807 rico. - Adotar só o
ProblemDetail, manterVersaoInfo/SaudeControllerlocais — meio-passo; as cópias de versão/health seguem drift-prone sem ganho. - Migrar Sentry e error-flow junto — alarga o PR e espalha o risco; ambos ficam como follow-up explícito.
Linhagem de trabalho: .ia/009-adota-xadm-comum-web-*.
Atualização 2026-08-11 — bump das libs + SentryInitializer absorvido¶
Junto da migração da constituição 0.26.6 → 0.28.0, os follow-ups desta decisão fecharam:
xadm-comum-web0.1.0→0.4.0. A0.3.0implementou o seam decode/titledo processor de erro da casa (oProblemDetailganhouinvalidParamse o overloadof(status, code, title, detail); o.of(status, code, detail)de 3 args continua). Este app usa handlers de auth distribuídos, não oUnifiedErrorResponseProcessor— o corpo de erro segue igual, apenas com o campo aditivoinvalidParams(vazio no caminho atual).SentryInitializerdeixa de ser local — passa a vir doxadm-comum-web. A lib absorveu os extras app-específicos (tagcliente, skip porMICRONAUT_ENVIRONMENTS,sendDefaultPii), então a cópia debr.com.xadm.webstormecom.comumfoi deletada (com seu teste) e oApplication.mainchamabr.com.xadm.comum.web.SentryInitializer.inicializar()(assinatura da lib; erainitFromEnvironment()). O comportamento agora é testado na lib.xadm-seguranca0.2.0→0.3.0(bump de acompanhamento; mesmo conjunto de classes, aditivo). AViewSecurityRulelocal permanece por desenho (regra de views específica do app).
Atualização 2026-08-19 — bump de manutenção 0.4.0 → 0.5.0¶
Acompanhamento do latest do registro. Source-compatível: ProblemDetail, VersaoInfo,
HealthController e SentryInitializer seguem com as mesmas assinaturas usadas aqui — zero adaptação,
gate da stack verde. Corpo de erro e /health inalterados.
Atualização 2026-09-11 — 0.9.0 e 0.9.1¶
0.9.0: a env do commit passou a serXADM_COMMIT. O Coolify sobrescreve aSOURCE_COMMITcomHEADem recurso pull-only; o rename entrou nos dois Dockerfiles, nos build-args dopipeline.ymle na decisão 0014.0.9.1: a lib embarca oreflect-config.jsondoSentryAppender(três entradas, com métodos) e o arquivo local saiu (ver 0011). O 404 de negócio, devolvido por uma rota que casou, passa a ir aDEBUG; só o 404 de rota não casada em requisição autenticada vai aERROR. O contrato de wire não muda.
0008 — Adotar a lib xadm-mensageria (log de ENTRADA do poke)¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-11 · Decidido em: 2026-08-06
Contexto¶
O tradutor recebe um poke do integrador ("produto/estoque mudou", corpo vazio) no
WebhookController (POST /api/integrador/produto + alias /webhook/estoque-mudou) e dispara o
Sweep em virtual thread, respondendo 202 na hora. Esse poke era fire-and-forget e não deixava
rastro: não havia registro de quando o integrador chamou nem quantas vezes. Se o parceiro
reclama de estoque desatualizado, não dava para provar "o integrador me pokou às 07:03 e o sweep
rodou" — a evidência não existia.
Esta é a Fase 3 da iniciativa libs+contrato: a casa extraiu o motor de mensageria M2M para a lib
br.com.xadm:xadm-mensageria (pacote br.com.xadm.comum.mensageria). O thoms é a adoção mais
leve das três: é receptor puro nesta direção — só log de ENTRADA + tela, sem outbox de
saída.
Decisão¶
webstorm-ecom depende da xadm-mensageria (registro Maven Forgejo) — adotada na 0.1.0;
a versão corrente é a do build.gradle.kts (fonte única, hoje 0.4.0) e usa só o lado de
entrada:
- Registro do poke — o
WebhookControllerchamaMensageriaService.registrarEntrada(tipo="poke", correlationId, payload, resumo)síncrono no thread do request, antes de disparar o sweep. Grava umaMensagemM2mdirecao=ENTRADA/status=RECEBIDO. Falha do registro propaga (poke pode virar 5xx): o banco é compartilhado e o sweep também depende dele, então um202com o banco caído seria mentira. O202e o disparo do sweep ficam inalterados — o registro é aditivo. correlationId= UUID gerado (a casa não tem MDC de request-id; é o ramo fallback do contrato).payload= metadados saneados do recebimento (rota, instante, origem) — nunca oAuthorization/Bearer. Corpo do poke é vazio.- Tela
/admin/mensageria(read-only) — vem embarcada na lib (@Controller+mensageria.html); o thoms só a habilita. Lista as entradas; como o thoms não produzSAIDA, os botões de reenviar/cancelar (que a view só mostra em linhasSAIDA) não aparecem. - Tabela
mensagem_m2mé do INTEGRADOR — o tradutor NÃO a cria. Nodb_thomscompartilhado a tabela tem um dono só, e é o integrador (remetente M2M com outbox/relay). O tradutor consome (gravaENTRADAviaregistrarEntrada), sem migração de produção. Em teste, sem integrador, uma migração test-only (db/migration-test/V6__mensagem_m2m.sql, ligada só noapplication-test.yml) cria a tabela. Deploy coordenado: o tradutor-mensageria só sobe junto/depois do integrador-mensageria (release sincronizado dos repos) — senão oregistrarEntradagrava numa tabela inexistente. - Config (
application.yml, blocomensageria:) —relay.enabled: false,retencao-dias: 90,admin.email-domain: ${AUTH_XADM_EMAIL_DOMAIN:xadm.com.br}(ver Pontos notáveis).
Pontos notáveis (verificados contra o jar 0.1.0)¶
- Retenção — a lib PODA
RECEBIDO. A lib tem um@ScheduleddiárioLimpezaMensagensque apaga linhas[ENVIADO, RECEBIDO]mais velhas quemensageria.retencao-dias(default 30). ComoRECEBIDOé o status do poke, no default a auditoria sumiria em 30 dias — o oposto do valor da feature. Fixamosretencao-dias: 90(alinhawebstorm.cleanup.retencao-diasdo app). O número é decisão de negócio — confirmar a janela com a Thoms. - Dois
@Scheduled, não um. Desligar o relay (relay.enabled: false, cortado porque não háEnviadorMensagem/SAIDA) não desliga oLimpezaMensagens— este roda diário independentemente, governado porretencao-dias. - Gate: quem manda é a rule da lib. A
MensageriaAdminRuleda lib temgetOrder() = -100e precede aViewSecurityRuledo thoms (0): decide/admin/mensageriasozinha. Exige o atributoemaildo principal (populado peloSessionAuthenticationFetcherdoxadm-seguranca) terminando em@${mensageria.admin.email-domain}— sem esse property a rule tranca todos, inclusive o admin válido. Consequências: anônimo → 401; autenticado fora do domínio → 403; a tela não herda o bypass dev/test das demais/admin(a rule roda antes daViewSecurityRule). - Tabela compartilhada com o integrador.
mensagem_m2m(sem o prefixowebstorm_ecom_*— é infra da lib) é a mesma que o integrador cria/usa nodb_thoms: o tradutor grava as linhasENTRADA, o integrador asSAIDAdo outbox. Por isso o tradutor não a migra em produção — doisCREATE TABLEno mesmo banco colidiriam (o 2º deploy quebraria com "table already exists"). Coordenação de retenção: aLimpezaMensagensda lib (poda diária de[ENVIADO, RECEBIDO]) roda nos dois apps sobre a mesma tabela — alinhar no release sincronizado quem poda (idealmente só o dono/integrador) pra o tradutor não apagar asSAIDAdo integrador. - Bug corrigido no thoms — model imutável. A tela da lib devolve o model como
Map.of(...)imutável. OGlobalViewModel(oViewModelProcessorque injetacurrentUser/csrf/marca em toda view) faziamodel.putdireto →UnsupportedOperationException(500 no render, quebraria a tela em prod). Passou a enriquecer uma cópia mutável e re-injetar viasetModel— tolera model imutável vindo de qualquer controller de lib.
Fica de fora (não é escopo desta adoção)¶
- Outbox de saída / entrega garantida do thoms — o único outbound é o
POST /sync/erpna WebStorm (3º-parte externa, não par M2M da casa); semenfileirar, SPIEnviadorMensagemnem relay ativo. - Ações de reenviar/cancelar — o thoms não é remetente M2M nesta direção; tela read-only.
- Contrato OpenAPI do poke — trabalho distinto e paralelo (receptor publica o contrato).
Consequências¶
- Auditoria do poke existe e é consultável pelo operador
@xadm.com.br— fecha a lacuna de evidência da entrada M2M da casa. - Correções/evoluções do motor de mensageria passam a vir por bump de versão da lib, não por código local — mesmo racional de 0006/0007.
xadm-mensageriaestá em0.1.0(pré-1.0.0): esta é a primeira adoção; a API estabiliza quando assentar.
Alternativas descartadas¶
- Log caseiro do poke (tabela
webstorm_ecom_*própria + tela ad-hoc) — reinventa o que a lib já padroniza entre os adotantes; drift-prone. - Adotar o motor completo com outbox — o thoms não tem par M2M de saída da casa; ligar relay/SPI/
máquina de estado de
SAIDAseria complexidade sem necessidade presente.
Linhagem de trabalho: .ia/010-adota-mensageria-*.
0009 — Watchdog do heartbeat do CSV do X-Adm¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-12 · Decidido em: 2026-08-07
Contexto¶
O X-Adm posta o catálogo (ProdutosSite.csv em .zip) no tradutor (POST /api/csv/processar) em
ciclos regulares (a cada ~60 min) dentro do horário comercial. Se essa rotina parar (falha no
X-Adm, agendador caído, rede), o e-commerce do parceiro passa a servir preço/estoque velhos e
ninguém percebe — não há sinal de "faz X minutos que não chega nada". Faltava um watchdog que
transformasse o silêncio do X-Adm num alarme.
Decisão¶
Um watchdog @Scheduled (Monitor + MonitorScheduler, pacote …webstormecom.monitor) que, dentro
da janela útil, alarma no GlitchTip se ficar mais que timeout-alarme minutos sem receber CSV:
- Heartbeat =
webstorm_ecom_ingestao(o últimocreated_at): a chegada do CSV. Um snapshot que chega com erros ainda conta como "recebido" — o watchdog vigia silêncio do X-Adm, não a qualidade do dado. - Janela útil
[janela-inicio, janela-fim)no fusozona(default 07h–20h America/Sao_Paulo): fora dela o silêncio é esperado → nunca alarma (e desarma um alarme pendente). Na abertura, a referência de idade é a abertura de hoje (não o último de ontem) — assim as 07h não disparam falso alarme. - Edge-triggered: alarma uma vez na transição (uma linha
ERROR→ GlitchTip pela recipe dologback.xml), não spamma; desarma quando chega dado novo; re-alarma se estourar de novo. - Config
webstorm.monitor.*(yaml/env):periodicidade(60),timeout-alarme(240),intervalodo check (5m),janela-inicio/janela-fim(07:00/20:00),zona. Validação no boot (fail-fast, estiloApiTokenGuard):timeout-alarme ≥ 2 × periodicidadee janela não-vazia — senão o app não sobe. - Estado in-memory (
volatile Instant alarmadoDesde): sem tabela nova. Um deploy durante um silêncio na janela re-alarma uma vez (a condição ainda é real) — aceito. - Tela read-only
/admin/monitor(MonitorViewController): periodicidade, timeout, janela+zona, último recebido, idade, e o estado (OK / fora-da-janela / ALARMADO desde). Sob aViewSecurityRuledo app (login@xadm.com.br, com bypass dev/test — é tela própria, não a rule da lib mensageria).
Notas de implementação (armadilhas de stack)¶
SELECT max(created_at)→Optional<Instant>, nuncaInstant. Num agregado nulável, tabela vazia devolve NULL, e o Micronaut Data com retorno não-Optionallança"Query produced no result"em runtime — que aqui seria 500 no watchdog num deploy novo, antes do 1º CSV (bug pego por teste de tabela-vazia).IngestaoRepository.ultimoRecebido()éOptional<Instant>.- Horários no YAML como literais entre aspas (
"07:00"), não${ENV:07:00}: o:do horário confunde o parser de default de placeholder do Micronaut (${K:07:00}quebra no último:e resolve"00"), e07:00sem aspas viraria sexagesimal no YAML. Trade-off: a janela deixou de ser env-configurável (muda-se editando o yaml + redeploy) — aceitável (raramente muda). - Testabilidade: a decisão é uma função pura
Monitor.avaliar(agora, zona, janela, ultimo, alarmadoDesde, timeout)— testada sem relógio/banco; oMonitorScheduler(@Requiresnowebstorm.monitor.enabled) só faz o wiring e é desligado nos ITs (padrão doSweepScheduler).
Fica de fora (follow-up se exigido)¶
- Edição dos valores pela tela / persistência do estado — decisão: yaml + in-memory (a tela é read-only). Tabela editável seria over-engineering pra agora.
- Calendário de dias úteis/feriados — a janela vale todo dia; se o X-Adm não enviar fim de semana/feriado, sábado 07h+timeout alarmaria falso. Precisaria de calendário (não neste escopo). Dias da semana resolvidos na atualização de 2026-09-12 (abaixo); feriados seguem de fora.
- Outro canal de alarme além do GlitchTip (o
SentryAppenderjá existente).
Consequências¶
- O silêncio do X-Adm vira um alarme observável no GlitchTip; o operador confirma o estado em
/admin/monitor. - Novos knobs de deploy
MONITOR_*/WEBSTORM_MONITOR_ENABLED(ver implantação §5.2).
Alternativas descartadas¶
- Alarmar 24/7 (sem janela) — falso alarme toda madrugada, quando o X-Adm legitimamente não envia.
- Config em tabela editável pela tela — runtime sem redeploy, mas migração + form + CSRF + validação no save sem necessidade presente (a janela/timeout raramente mudam).
- Persistir o estado alarmado — evita o re-alarme de 1× no deploy, ao custo de uma tabela para um booleano; o re-alarme é inócuo (a condição é real).
Atualização 2026-09-12 — dias da semana vigiados¶
Sábado 2026-09-12 o watchdog alarmou às 11:02 (242 min sem CSV desde a abertura das 07:00). O alarme
era verdadeiro — o X-Adm costuma enviar no fim de semana (13–14 CSVs em 15/08, 16/08, 29/08, 30/08 e
06/09, medido na webstorm_ecom_ingestao) e naquele sábado não enviou nada —, mas silêncio de fim de
semana não deve acionar ninguém.
- Nova config
webstorm.monitor.dias(envMONITOR_DIAS), siglas pt-br separadas por vírgula, defaultSEG,TER,QUA,QUI,SEX. Dia fora da lista vale como fora da janela: não alarma e desarma um alarme pendente (oemJanelaagora é dia e horário). - Segunda-feira usa a mesma regra da virada do dia: a referência é a abertura de segunda (07:00), não o último CSV da sexta. Uma queda que começa no sábado só alarma segunda ~11:00 (07:00 + 240 min) — aceito: é o custo de não acionar ninguém no fim de semana.
- Fail-fast no boot: lista vazia ou sigla desconhecida (ex.
MON,SEXTA) derruba o app, como a validação detimeout-alarme. Parse sem diferenciar maiúscula nem acento (sáb=SAB). - A tela
/admin/monitormostra os dias vigiados. - Feriados seguem fora de escopo (precisariam de calendário).
Linhagem de trabalho: .ia/011-watchdog-heartbeat-csv-*.
0010 — Guardar o upload da ingestão no Garage¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-15 · Decidido em: 2026-08-10
Contexto¶
O X-Adm posta o catálogo (ProdutosSite.csv em .zip) no tradutor (POST /api/csv/processar), que
extrai, processa e descarta os bytes. Ao investigar um processamento na tela /admin/{id}, o
operador não conseguia baixar o arquivo que foi usado — a evidência não existia. Faltava guardar o
upload e expor um download.
Decisão¶
Guardar o upload original (os bytes como vieram — .zip ou .csv) no Garage (object storage
S3-compatível da casa) e servir um botão de download:
ArquivoStore(pacote…webstormecom.arquivo) — cliente AWS SDK v2s3, path-style,endpointOverride.@Requires(property="garage.secret-access-key", pattern=".+"): o bean só sobe com a write key presente — sem Garage configurado, a captura é no-op e o botão some (dev/test sobem sem Garage). Usa oUrlConnectionHttpClient(não onetty-niodefault) pra não colidir com o Netty do Micronaut.- Captura na ingestão (
ProdutoService) — após gravar oIngestaoSnapshot, fazput("ingestao/<id>", bytes, tipo)e gravaarquivo_ref/arquivo_tipona linha (migração V7). Vale nas duas portas (/api/csv/processarasync e/admin/uploadsíncrono). Falha doputnão derruba a ingestão (o processamento é o efeito principal). - Download
GET /admin/{id}/arquivo(AdminViewController, sob aViewSecurityRule) — stream proxied do Garage comContent-Disposition; botão Baixar arquivo no detalhe, só quando há ref. - Retenção do arquivo = 30 dias (
webstorm.arquivo.retencao-dias), menor que a da linha (90d): oCleanupJobapaga o objeto e zeraarquivo_refaos 30d; a linha de auditoria fica os 90d, só perde o download.
Notas de implementação (armadilhas de stack — não há recipe da casa)¶
- Não existia lib de storage na época. Garage é S3 puro; o app falava AWS SDK v2 path-style
direto. Era net-new (a página
engenharia/java-micronautnão tinha recipe de storage — §6). (Superado em 2026-08-11: a casa extraiuxadm-comum-storage, adotada — ver Atualização abaixo.) - Envs reais são
GARAGE_*, nãoS3_*. A docinfraestrutura/garagedizS3_*, mas o broker injetaGARAGE_ENDPOINT/GARAGE_BUCKET/GARAGE_REGION/GARAGE_ACCESS_KEY_ID/GARAGE_SECRET_ACCESS_KEY(§6). Mapeados paragarage.*noapplication.ymlvia placeholder${GARAGE_*}— evita a ambiguidade env→property do Micronaut (nome com múltiplos_). Aregionvem do env, então o app usa o valor do cluster — que precisa sergarage. O broker chegou a injetargarega(typo) e o valor se propagou aapp.json/Coolify; não é inofensivo: o Garage valida a region no escopo SigV4 e exigegarage— comgaregatoda chamada S3 volta400 AuthorizationHeaderMalformed. Corrigido na fonte client-grade (probe LIST/PUT/GET/DELETE 200/204 comgarage). - Teste com
adobe s3mock, não MinIO. A casa migrou de MinIO pro Garage — o duplo de teste é ocom.adobe.testing:s3mock-testcontainers(mock S3 feito pra teste), que exercita o protocolo S3 real (path-style, AWS SDK) sem reintroduzir MinIO.
Fica de fora (follow-up se exigido)¶
- Presigned URLs — o download é proxied pela app (auth in-app), sem presign.
- Editar a retenção pela tela / guardar o CSV extraído — guarda-se o upload original; retenção é config.
- ~~Lib
xadm-arquivos— não existe; se a casa extrair uma, migrar então.~~ Feito (2026-08-11):xadm-comum-storageadotada — ver Atualização.
Consequências¶
- O upload fica auditável e baixável por 30 dias na tela de detalhe.
- Provisionamento via
/xadm-setup(Garage): cria bucket + write key, injeta asGARAGE_*no Coolify (ver implantação §5.2). Oapp.jsonguarda o client-grade (features.garage), nunca o segredo.
Alternativas descartadas¶
byteano Postgres — entregaria o mesmo botão sem infra (e caía na retenção da linha), mas o usuário optou pelo jeito da casa (object storage). Registrado como a opção "simples" preterida.- MinIO como backend — a casa já migrou dele pro Garage; não reintroduzir (nem em teste).
- Config do bucket em tabela editável — over-engineering; endpoint/bucket/region vêm de env.
Linhagem de trabalho: .ia/012-guardar-arquivo-ingestao-garage-*.
Atualização 2026-08-11 — adota xadm-comum-storage (elimina a construção do client na mão)¶
Auditoria de aderência às libs da casa: a construção do S3Client que o ArquivoStore fazia na mão
era o mesmo código da S3ClientFactory da casa. Adotada a xadm-comum-storage:
ArquivoStoreagora injeta oS3Clientbean da lib. AS3ClientFactory(@Factory) o constrói a partir doS3Config(@ConfigurationProperties("s3")): endpoint + path-style + creds estáticas. O wrapper local ficou só comput/get/delete(a interfaceArquivoXlsStorageda lib é datada/XLS, não casa com o.zip/.csv+contentTypedaqui). O bean da lib é lazy → o gate continua no consumidor (@RequiresnoArquivoStore).- Config permanece
garage.*(o storage É o Garage — nome semântico). A lib binda o prefixo fixos3.*; oGarageS3ConfigFactorymonta oS3Configda lib a partir degarage.*e@Replaceso bindings3.*default. Envs de deployGARAGE_*e@Requires garage.secret-access-keyinalterados. netty-nio-client(transitivo daxadm-comum-storageviaawssdk:s3) excluído — mantém ourl-connection-clientcomo HTTP client sync e evita o 2º Netty colidindo com o do Micronaut.- Teste: segue com adobe s3mock (props
s3.*). Decisão explícita: não trocado peloGarageTestResourcedaxadm-comum-teste(que sobe um Garage realdxflrs/garage— mais pesado); o s3mock foi escolha deliberada e continua servindo. Idem Postgres: mantido o Micronaut Test Resources (framework, idiomático), não oPostgresTestResourceda lib — nenhum dos dois é replicação de código da casa.
Na mesma leva (auditoria de replicação): o SHA-256 na mão do diff/ingestão (ProdutoDiff,
ProdutoService) passou a ChecksumSha256.hex do xadm-comum-util (output idêntico — hex
minúsculo, sem drift de checksum persistido); e foi adicionado um teste de arquitetura
(ArquiteturaTest) com a RegrasArquitetura.semCiclosEntreFatias do xadm-comum-teste (app
confirmado livre de ciclos entre fatias).
Atualização 2026-08-19 — bump de manutenção xadm-comum-storage e xadm-comum-teste 0.1.0 → 0.2.0¶
Acompanhamento do latest do registro. Source-compatível: o S3Config (montado pelo
GarageS3ConfigFactory via @Replaces, namespace garage.*) e a RegrasArquitetura do teste seguem
com as mesmas assinaturas — zero adaptação, gate da stack verde. As decisões explícitas de manter o
adobe s3mock e o Micronaut Test Resources (em vez do GarageTestResource/PostgresTestResource
da lib) permanecem.
Atualização 2026-09-11 — a ponte GarageS3ConfigFactory saiu¶
A premissa dela deixou de valer na xadm-comum-storage 0.2.0: o S3Config da lib passou a bindar
garage.* direto (@ConfigurationProperties("garage"), com criar-bucket default false). O
@Replaces local só traduzia o nome de duas chaves (access-key-id/secret-access-key →
access-key/secret-key) — tradução que agora mora no placeholder do application.yml. As envs de
deploy não mudaram (GARAGE_ACCESS_KEY_ID/GARAGE_SECRET_ACCESS_KEY); o @Requires do
ArquivoStore e os testes de integração passaram a usar garage.secret-key/garage.access-key. O
s3mock segue como fixture de teste (decisão explícita acima, inalterada).
Correção (2026-09-15): o Postgres de teste é o singleton da
xadm-comum-teste(IntegracaoComPostgres), a norma da casa para o teste de integração Micronaut, e o Micronaut Test Resources saiu do build; o Testcontainers chega pela lib e pelo BOM do Micronaut, sem declaração no app. A credencial do Garage usa os nomes do broker: o@RequiresdoArquivoStoree os testes de integração leemgarage.secret-access-key/garage.access-key-id, que axadm-comum-storage0.3.0 liga direto das envs, sem ponte noapplication.yml. O s3mock segue como fixture de S3.
0011 — Habilitar GraalVM native-image no tradutor¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-11 · Decidido em: 2026-08-20
Contexto¶
A casa adotou native-image como default dos servers Micronaut elegíveis (decisão 0022 do
central): RSS ~3× menor e arranque quase instantâneo. O webstorm-ecom deployava só como imagem
JVM (eclipse-temurin, java -jar app.jar). Esta decisão habilita o native no lado-app —
o que o repositório precisa para o build native ficar verde e a imagem bootar. Padrão provado:
piloto bi-transporte-xls e central-backend (ambos native-verde na casa).
Falha de native é só em runtime (reflection/resource/serde sem metadata AOT) — "compila verde" não prova nada; o boot da imagem native é que prova.
Decisão¶
Habilitação pura, sem tocar domínio (tradutor / ingestão / mensageria). Quatro peças:
- Perfil no
build.gradle.kts—configure<GraalVMExtension>(o accessorgraalvmNative {}não é gerado no Kotlin DSL porque o pluginorg.graalvm.buildtools.nativevem transitivo doio.micronaut.application):-PnativeQuick→-Ob(loop dev/e2e, build ~40-60% mais rápido); sem a flag (prod) →-Os(imagem menor); sempre--gc=serial(menor footprint; default CE explícito). CE 25 não tem--pgo/--emit build-report/--gc=G1(Oracle-only). - Um reflect-config —
io.sentry.logback.SentryAppender, emsrc/main/resources/META-INF/native-image/br.com.xadm/webstorm-ecom/reflect-config.json(nome como string, não.class— evita o javac arrastarorg.jetbrains.annotations.Nullableausente). Ologback.xmldeclara esse appender; o Joran o instancia por reflexão → sem o registro o boot native quebra noLoggerFactory. É o único metadata manual necessário (igual ao piloto e ao central-backend); AWS SDK v2, Flyway e serde são cobertos por reachability-metadata + features do Micronaut. TODO-casa: mover a hint paraxadm-comum-web(dona doSentryInitializer) e propagar — some daqui quando a lib absorver.Atualização 2026-09-11 — cumprido. A
xadm-comum-web0.9.1 embarca a hint no próprio jar, com três entradas (SentryAppendereSentryOptionscom construtor eallPublicMethods, eLevel.valueOf); o arquivo local saiu no bump. A cópia daqui registrava só a classe: no native o logback não achava os setters e ignorava o<options>, e os eventos chegavam ao GlitchTip sem oenvironment. - Build via
dockerBuildNativedo plugin — que gera o Dockerfile native internamente. Não se escreve Dockerfile native à mão e não se adota o templatedockerfile-java-native(opcional): o piloto não usa, e divergir do fluxo provado só adiciona risco. O Dockerfile JVM atual fica intacto (prod segue JVM até o rollout, que é de operação). As tasks native do plugin não são configuration-cache-safe → invocar com--no-configuration-cache. UsardockerBuildNative, nãonativeCompile(o native é Linux;nativeCompilenum host Windows/Mac geraria binário da plataforma errada) — pré-requisito: daemon Docker. - Liveness =
/healthplano da lib — nada a implementar. O management health fica desligado (endpoints.health.enabled: false, como hoje) e o/health{status,versao}doHealthControllerdaxadm-comum-webserve de liveness. Registrado como não-decisão para não ser reaberto: reabilitar o management health recriaria a rota/healthdupla (controller incondicional da lib + management) →400em toda request (Norma 2, constituição 0.31.2).
Verificação (gate lado-app)¶
./gradlew checkverde (a config native não altera o build/testes JVM — regressão zero).dockerBuildNative -PnativeQuick --no-configuration-cachecompila a imagem native.- Boot smoke (Testcontainers
GenericContainerda imagem native + Postgres com o DDL emprestadoestoquededb/migration-test): afirma boot + Flyway(webstorm_ecom_*) +GET /health200. É boot-level, segregado do./gradlew check(exige a imagem native pré-buildada).
"Native build verde aqui" ≠ "app native funciona". O boot smoke prova o grafo de DI + logback/Sentry + serde no boot — não exercita os caminhos native-arriscados de runtime do tradutor: a query data-jdbc
EstoqueRepository.pendentes()(réplica) e o PUT real S3→Garage da ingestão. Essa prova é do e2e native, que vive em outro repo (etc/tests/native/e2e-thoms-webstorm-ecom, derivado doe2e-central-backend). O CI (ci.yml) permanece JVM; o gate native é out-of-band, coerente com o estágio protótipo native da casa.
Notas¶
awssdkreal = 2.44.7 (axadm-comum-storage:0.2.0eleva o BOM2.28.16declarado nobuild.gradle.kts) — se o e2e achar gap de reflexão no S3, procurar metadata da 2.44.x.
Alternativas descartadas¶
dockerfile-java-native(template da toolchain) — o piloto não usa; adotá-lo diverge do fluxo provado sem ganho.@Livenessexplícito + management health ligado — mais código e recria a rota/healthdupla (400). O/healthda lib já é o liveness.- Provar S3/data-jdbc no boot smoke local — exigiria Garage + poke no compose deste repo; a spec
manteve o e2e ponta-a-ponta no
etc/tests(uma fonte, não duas).
Linhagem de trabalho: .ia/013-native-image-*.
0012 — Views em JTE no lugar de Thymeleaf¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-08-26 · Decidido em: 2026-08-26
Contexto¶
As telas server-render do tradutor (login, monitor/auditoria de requests, admin) rodavam em
Thymeleaf (micronaut-views-thymeleaf). Sob GraalVM native-image (decisão
0011) o Thymeleaf/OGNL resolve o modelo por reflexão — cada
tipo alcançado por template precisaria de @ReflectiveAccess e a falha só aparece em runtime
(500), tela por tela: whack-a-mole de reflexão. A casa fixou o alvo na ADR central
0025 — Views server-render com JTE.
Decisão¶
Adotar JTE (gg.jte) como motor das views server-render, substituindo o Thymeleaf, seguindo a central 0025. Template compilado, type-check no build, zero reflexão de modelo em runtime.
Consequências¶
- Plugin
gg.jte.gradle3.2.4 casado ao runtimemicronaut-views-jte; templates emsrc/main/jte/**(login.jte,kit/,admin/,webstorm-ecom-requests/),generateJteantes docompileJava. generate()mode — os.javagerados são compilados junto do app (native-safe; sem.binpor reflexão em runtime).contentType = Html.reflect-config.jsonde reachability-metadata dos templates gerados emsrc/main/resources/META-INF/native-image/jte-views/.- Os globais de view (
appNome/clienteXadm/authEnabled/usuário) vêm doXadmViewModelda libxadm-seguranca(decisão 0006). - Fontes geradas ficam em
build/generated-sources/jte, fora desrc/main/java→ naturalmente fora do escopo de cobertura.
Fora de escopo: o motivo e as alternativas da migração (vivem na central 0025).
0013 — CI/CD 100% GitHub Actions (pipeline.yml único)¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-08-31 · Decidido em: 2026-08-31
Contexto¶
A CI do tradutor rodava em três workflows Forgejo — ci.yml (gate ./gradlew check),
docs.yml (publish MkDocs) e release.yml (SemVer ↔ CHANGELOG ↔ tag) — com o gate no container
ci-java:25 da fábrica de imagens de CI. A casa migrou o CI de toda a frota para GitHub
Actions num pipeline único, aposentou a fábrica (matriz-baseline.json não gateia mais nada) e
passou a resolver a toolchain pelas próprias actions. O alvo está fixado na ADR central
0027 — CI/CD em GitHub Actions
(emenda a 0003 e à central 0026 de deploy control-plane).
Decisão¶
Adotar a central 0027: um único .github/workflows/pipeline.yml substitui os três
workflows Forgejo, seguindo a variante Java native-canary (o app tem build.targets: [jar,
native]). Migração feita em 2026-08-31 (commit 1ab3c2e); o runner Forgejo saiu com
has_actions=false.
Consequências¶
- Jobs à la carte por
docs/app.jsonbuild.targets:gate(todo push/PR) ·docs(master + tag, slugthoms-webstorm) ·build_jar/build_native+deploy(só na tag, via control-planePOST /api/ci/deployda central [0026]). - Toolchain pelas actions (
setup-java/setup-gradle) a partir detoolchain.javadoapp.json— semcontainer: ci-java:<v>da fábrica (aposentada na constituição 1.1.3). Testcontainers sobem no docker nativo do runner. - Gate carrega as guardas estáticas da stack (placeholder aninhado, serde-api, POI-em-native,
paridade Dockerfile/Dockerfile.native, reaper de PID1 no ENTRYPOINT, persistência
micronaut-data) além do
./gradlew check. - Aviso de falha = notificação nativa do GitHub (e-mail);
DOCS_S3_*é secret por repo. - Os antigos
.forgejo/workflows/{ci,docs,release}.ymlforam removidos do repo.
Fora de escopo: o motivo e as alternativas da migração de CI (vivem na central 0027); o contrato de deploy control-plane (central 0026).
0014 — Smoke de produção pós-deploy (adota a central 0031)¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-08 · Decidido em: 2026-09-08
Contexto¶
O deploy do tradutor sai pelo control-plane (POST /api/ci/deploy, central
0026, adotada localmente na
0013) e esse POST é assíncrono: ele retorna sucesso e o
pipeline fica verde antes de o container novo servir. O /health que o Coolify observa é
liveness — o processo subiu e o Flyway passou —, não "as telas respondem". Entre os dois cabe a
classe de falha que a frota já entregou com o app healthy: tela em 500 e segredo ausente que só
apareceu como 401 no cliente.
Aqui a superfície é concreta: o tradutor serve views JTE atrás de sessão (/, /admin,
/admin/monitor) e roda native (0011), onde o AOT pode podar
uma rota registrada — 404 no binário e 200 no jar, sem erro de build. Até agora nada no pipeline
afirmava produção depois do deploy.
A casa fixou o alvo na ADR central 0031 — Smoke de produção pós-deploy (constituição §9; receita em smoke-producao).
Decisão¶
Adotar a central 0031 neste repo, sem variação local. O que isso significa aqui:
- Job
smokenopipeline.yml, depois dodeploy, rodandoscripts/smoke.pyda toolchain. Reprovou em qualquer camada → reverte para a tag imutável<imagem>:<sha anterior>-<target>, confirma a reversão e fica vermelho. - Manifesto em
docs/app.json→smoke: basehttps://webstorm.thoms.xadm.biz, projetothoms-integracao-produto-ecomno GlitchTip e as rotas críticas —/healthe/logincomopublic;/,/admine/admin/monitorcomosession. Sem rotam2m: os dois endpoints Bearer do tradutor (POST /api/csv/processar,POST /api/integrador/produto) são POST, e o smoke só fazGET— declarar um deles daria 405, não cobertura. A mesma lista serve o e2e native (afirma ≠404 no binário, antes do deploy). - Seam de sessão do
xadm-seguranca0.7.0 (POST /smoke/session, headerX-Smoke-Token), ligado porsmoke.token← envSMOKE_TOKEN. Nasce desligado: sem a env o@Requires(pattern=".+")reprova e/smoke/**nem existe (404). Sem ele as rotassessiondegradariam para asserção negativa (302/401), que é o que o login Google interativo permite. - Identidade de build cabeada:
XADM_COMMIT=${{ github.sha }}como build-arg nos dois jobs de build eARG/ENVtarde nos dois Dockerfiles (declarado no topo, invalidaria o cache e onativeCompilede ~13 min rodaria em todo build). O mesmo valor é ocommitdo/health, areleaseno Sentry e o sufixo da tag imutável.
Consequências¶
- Dois segredos novos por ambiente.
SMOKE_TOKENprecisa existir igual nos dois lados — secret no GitHub (jobsmoke) e env no recurso do Coolify (o app) —;GT_TOKEN(read-only do GlitchTip) habilita a camada 3. Faltando oGT_TOKEN, a camada é pulada; faltando oSMOKE_TOKENde um dos lados, as rotassessionreprovam. basescobre uma URL. O app tem dois alvos de deploy (jarenative) e o manifesto declara aproduction_url. Se os dois flavors servirem hostnames distintos, o smoke só afirma o que está declarado — a segunda base tem de entrar na lista, senão metade do deploy fica sem asserção.- A reversão é por imagem, não por instância. O
POSTdo control-plane não tem eixo de instância: uma reversão reverte todas as bases. Divergência de commit entre bases é tratada como drift e trava o rollback, por desenho da central. - O smoke não prova regressão visual nem que o container velho não respondeu durante a troca (ponto cego declarado na central).
Alternativas descartadas¶
- Seguir só com o
/healthdo Coolify. É liveness: foi exatamente o que deixou tela em 500 com apphealthypassar por deploy bem-sucedido. - Esperar o e2e native cobrir. Ele roda antes do deploy, contra o binário, e afirma só ≠404 — não vê o que produção responde depois que o container troca.
- Reusar credencial de gente no seam. Daria mais permissão do que o smoke precisa e apagaria a
fronteira entre quem testou e quem opera; o principal
smokeé próprio, e só-leitura por verbo.
0015 — Tela de login servida pela lib (adota a central 0032)¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-08 · Decidido em: 2026-09-08
Contexto¶
O tradutor tinha src/main/jte/login.jte própria — uma das cinco cópias da frota. As cinco
carregavam o mesmo mecanismo de auth (SDK do Firebase 10.13.2, signInWithPopup,
POST /login/callback com CSRF double-submit) e divergiam só na moldura: bloco de marca em três
delas, título em quatro formatos. Duplicado justamente o que carrega risco; divergente o que é
cosmético. Um bug de auth custava cinco PRs.
Este repo mediu o custo na prática: a cópia daqui viera do bi-comercial-xls sem o CSS que a
acompanhava — .xadm-login-logo e .xadm-login-tagline estavam definidas no app.css de lá e não
existiam aqui, compensadas por style= inline no <img>. A tela "parecia ok"; o markup viajou, o
contexto não.
A casa fixou o alvo na ADR central
0032 — Tela de login servida pela lib
(constituição 1.4.0), entregue na xadm-seguranca 0.7.1.
Decisão¶
Adotar a central 0032, sem variação local. Adoção é migração no mesmo PR:
- Apagada a
src/main/jte/login.jte. A view agora vem do jar da lib, sob namespace (xadm/login.jte→…precompiled.xadm.JteloginGenerated); oXadmLoginControllerrenderiza@View("xadm/login"). O namespace não é detalhe: na raiz, a view da lib geraria a mesma FQCN que a de um app, uma sombreando a outra sem erro nem aviso. - Cosméticos viram property (
LoginViewSettings):xadm.views.login.marca: trueetagline: "Um ERP Completo para sua empresa"— a moldura que este app já usava, agora declarada em vez de escrita em markup. O título é derivado doxadm.views.app-nome("Login — Webstorm"), que é o que mata os quatro formatos por construção. - Apagado o
ViewGlobalslocal: a0.7.1faz oXadmViewModelinjetarappVersao, com as duas armadilhas (ler a cada render, nunca emstatic final; tolerar model imutável) resolvidas na lib. - Bootstrap deixa de vir do CDN. O kit 1.4.0 vendoriza
bootstrap.min.css+bootstrap.bundle.min.js(5.3.8, conferidos contra o hash SRI publicado pelo projeto) emtemplates/public/**; re-derivamos o kit e os dois arquivos. O SDK do Firebase segue nogstatic, de propósito — vendoriza-se apresentação, não o SDK do provedor de identidade.
Consequências¶
/js/**precisou virar anônimo. O layout passou a linkar/js/bootstrap.bundle.min.js, e as regras de rota deste app — que são dele, não do kit — liberavam/css/**e/images/**mas não/js/**: o bundle respondia 401, inclusive na tela de login, onde por definição não há sessão. Sintoma mudo: página estilizada, JS morto, nada no log. Corrigido nas duas fontes (intercept-url-mapdoapplication.ymleViewWhitelist) e travado porEstaticosDoKitSaoAnonimosTest, que lê olayout.jtereal e cobra cada estático linkado nas duas.- Acoplamento novo entre repos, sem gate. O markup vem do jar da lib; o CSS que o estiliza
(
.xadm-login-logo,.xadm-login-tagline) vem docustom-theme.css, template rastreado do central. Dois versionamentos, nada os amarra: tema velho + lib nova renderiza a marca sem estilo, e só o olho pega. Sintoma nomeado pela lib: marca sem estilo = tema desatualizado, re-derive o kit. - A tela não usa o
kit/layout.jte(emenda da 0032):@templateresolve em tempo de geração, então a view empacotada exigiria o kit dentro do build da lib. Ela traz o próprio documento — logo, nome e versão. Custo declarado lá: ~10 linhas de header duplicadas, que podem divergir visualmente do header logado com o tempo. - Some o botão "Entrar" redundante no header da tela de login: sem sessão não há chip nem menu, e a view autocontida não os renderiza.
Alternativas descartadas¶
- Manter a view local — era o estado que produziu cinco cópias e o CSS órfão descrito acima.
- Vendorizar o Bootstrap só aqui, editando o
kit/layout.jte: o layout é template verbatim, e a edição local seria revertida no próximo re-derive, em silêncio. O caminho certo foi levar ao central, que vendorizou para a frota.
Glossário do projeto¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-06
Vocabulário próprio desta integração. Termos gerais da plataforma estão no glossário da plataforma X-Adm.
- X-Adm
- ERP fornecido pela X-Adm ao cliente Thoms, onde ocorrem o cadastro e a precificação dos produtos. É a origem dos dados desta integração.
- Parceiro (WebStorm)
- WebStorm (https://www.webstorm.com.br) — empresa que desenvolve e opera o
e-commerce da Thoms e expõe a API REST (
POST /sync/erp) que recebe os dados dos produtos. É o destino dos dados; dá nome ao app deste repo (webstorm-ecom.thoms.xadm.biz) e ao prefixo das tabelas (webstorm_ecom_*). - Integrador (hub)
- Nuvem em
int.thoms.xadm.bizque recebe os dados de produto/estoque que o X-Adm posta (modelo ERP), grava o log de requests de entrada e consolida a réplica. É do projeto integrador (repoxadm/integrador-server, doc em https://docs.xadm.biz/aplicacoes/integrador-server/): app Java/Micronaut multi-tenant (um deploy Coolify por cliente;int.thoms.xadm.bizé o da Thoms). Aqui é referenciado. - Réplica (do X-Adm)
- Banco, mantido pelo integrador, com o estado consolidado de produto/estoque que o X-Adm envia. É réplica parcial (só o que a integração usa), não uma cópia do ERP inteiro.
- Tradutor
- App Micronaut dedicado enxuto em
webstorm-ecom.thoms.xadm.bizque lê a réplica direto nodb_thoms, mapeia para o contrato do parceiro, faz oPOSTe grava awebstorm_ecom_request. Acionado por webhook + sweep (ver termos). É o mini-projeto deste repo e a casa de futuras consultas de preço/produto (decisão 0003). - Requests de entrada
- Store, no integrador, com o log de cada request que o X-Adm posta (auditoria da ponta de entrada).
webstorm_ecom_request- Tabela do tradutor que grava cada
POSTde saída ao parceiro e a resposta recebida, mais o controle de idempotência (versão/estado já enviado) — auditoria da ponta de saída. Prefixowebstorm_ecom_*= tabela do tradutor no banco compartilhado (verdb_thoms). db_thoms- O único banco Postgres compartilhado pelo integrador e pelo tradutor. Fronteira:
cada app tem Flyway próprio (history dedicada) para as migrations não colidirem;
as tabelas do tradutor são prefixadas
webstorm_ecom_*; a réplica é do integrador (o tradutor a lê, nunca a migra). - Webhook (A) / sweep (D)
- Como o integrador aciona o tradutor. Webhook (A): o integrador faz um
POSTnuma URL do tradutor ("produto X mudou") ao consolidar — baixa latência, Bearer +@Retryable. Sweep (D): um@Scheduledno tradutor varre a réplica por versão > última enviada — rede de segurança (padrão Outbox/Polling Publisher: o próprio banco é a fila). Os dois chamam o mesmo código; a idempotência evita duplicidade. Substituem o PowerSync do desenho anterior (decisão 0003). - Carga inicial
- Exportação de todos os produtos do X-Adm em
ProdutosSite.csv, enviada por e-mail ao parceiro para importação em massa. Semeia a base do e-commerce uma vez; independe do fluxo contínuo. - Preço à vista
- Preço do produto para pagamento à vista (com desconto). Mapeia para
preco_descontono contrato do parceiro (decisão 0001). - Preço a prazo
- Preço do produto para pagamento a prazo (preço cheio). Mapeia para
preco_cheiono contrato do parceiro (decisão 0001). - EAN
- Código de barras global do produto (GTIN/EAN-13). Chave natural usada pelo parceiro para localizar o produto na base dele (decisão 0002).
- Código X-Adm
- Identificador interno do produto no X-Adm (
CODIGOno CSV;codigo_erpno contrato do parceiro). - ProdutosSite.csv
- Arquivo CSV que o X-Adm gera com os produtos (11 colunas:
CODIGO;NOME;ATRIBUTO;VALOR;ESTOQUE;STATUS;PESO;PROMOCAO;INI PROMOCAO;FIM PROMOCAO;EAN13). Origem da carga inicial e referência das colunas do de-para (ver modelo de dados). - STATUS (X-Adm)
- Coluna do
ProdutosSite.csv:1= produto ativo com saldo em estoque;0= inativo, zerado ou que ficará negativo por pedido pendente.