Pular para conteúdo

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 lib xadm-mensageria traz uma tabela mensagem_m2m (sem o prefixo webstorm_ecom_* — é infra da lib) compartilhada no db_thoms: o integrador a cria e grava as SAIDA do outbox; o tradutor grava só o log de ENTRADA do poke (direcao=ENTRADA/status=RECEBIDO) — sem criar a tabela em produção (dois CREATE TABLE no 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, ATRIBUTO e a janela de promoção não chegam ao integrador. O PESO/ATRIBUTO/promoção ficam fora da réplica (sem coluna, sem consumidor). O STATUS, porém, passou a ser tratado pelo adapter de ingestão CSV (abaixo): STATUS=0 vira soft-delete no integrador (produto sai de estoque no parceiro).

Mudança (2026-07): o CSV virou o fluxo contínuo. O ProdutosSite.csv deixou 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 o PUT/DELETE /api/v1/xadm do 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; o STATUS indica o que está ativo/com saldo.
  • EAN13 pode faltar: há produto sem EAN no export — sem EAN o parceiro não casa o produto (erro sem_ean_nem_sku); alguns EANs são internos (prefixo 2000000…), não GTIN reais.
  • ATRIBUTO vem 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 nome e o preco_desconto (à vista) são enviados (o contrato aceita), mas não são aplicados hoje: o e-commerce usa o preco_cheio e aplica seu próprio desconto à vista (decisão 0001).
  • POST /sync/erp não cadastra produto novo — um EAN ainda inexistente no catálogo do parceiro volta em nao_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 estoque da 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; o codigo_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 com saldo = 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 estoque a identidade do produto na Thoms é cod_prod (chave_est chega null); a correlação com a WebStorm é por ean13. ean13 é CHAR(13) padded — ler com TRIM. Sem EAN não casa (sem_ean_nem_sku) — tratar antes de enviar.
  • nome e preco_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 do preco_cheio (decisão 0001).
  • Lote: items[] aceita até 200 itens por request (HTTP 413 acima). 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ós PARCEIRO_MAX_FALHAS (default 3) lotes seguidos falhos e retoma no próximo ciclo. Lote pequeno responde antes do read-timeout de 20s; se o gargalo do parceiro for nº de requests (proteção/rate-limit) e não tempo por request, subir PARCEIRO_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) no db_thoms. É do projeto integrador (repo xadm/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 no db_thoms, mapeia para o contrato do parceiro, faz o POST e grava a webstorm_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 um POST numa URL do tradutor ("produto X mudou", só o id/EAN) — baixa latência. (D) um @Scheduled no 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 prefixadas webstorm_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_request o que já enviou; reprocessar a mesma versão não gera POST duplicado. 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 POST ao parceiro é @Retryable/@CircuitBreaker (Micronaut); falha persistente fica gravada na webstorm_ecom_request e é reprocessada pela tela ou pelo próximo sweep.
  • Lotes. O tradutor particiona o POST /sync/erp em 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 o preco_desconto por 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 campo STATUS, 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 marca deleted → o tradutor envia saldo = 0.

Identidade do produto: a identidade na Thoms é cod_prod (chave_est chega null; a antiga "pendência codigo_alt × CodProd" era premissa errada — codigo_alt é transporte da PIED, pied_codigo_alt no 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: