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