Pular para o conteúdo principal

Integração Wessel Alimentos (ERP Totvz — WEAPI)

A organização Wessel Alimentos (slug wessel) integra com o ERP da Wessel pela WEAPI, uma API REST no Protheus/Totvz. Toda a configuração é declarativa — entidades canônicas + conectores — e vive em apps/api/prisma/seed-data/.

Como a WEAPI funciona​

  • Cada recurso é um par WEAPI###/<endpoint>: WEAPI004/get_cliente, WEAPI005/get_produto, etc.
  • O ERP responde num envelope { "cRetorno": [...] }, mas o túnel de dev (/api/wessel/rest/) já desembrulha e devolve o array cru na raiz. Por isso os conectores não definem itemsPath.
  • Vários endpoints trazem envelope pai→filhos numa só chamada (tabela com itens[], cliente com titulos[]). O de-para resolve isso com flatten — ver ADR-030.
  • Consultas genéricas passam SQL cru em cQuery (WEAPI002/get_dados_tabela), sobre tabelas do Protheus (SX5010, SD2010, SE1010).
Sem paginação na fonte

Nenhum endpoint da WEAPI pagina. get_cliente devolve ~880 clientes e get_tabela_preco ~1.070 tabelas de uma vez — o get_contas_receber chega a estourar 60s. O data source Totvz usa timeoutMs: 180000. Se um conector vier com paginação page/pageSize configurada, ele traz 0 registros: a WEAPI ignora os parâmetros e o itemsPath não casa.

Conectores​

Todos apontam para o data source Totvz (auth LOGIN_TOKEN, header Authorization: Bearer).

ConectorEndpointEntidade canônica
get-clientesWEAPI004/get_cliente (cLoja=01)client
get-clientes-enderecosWEAPI004/get_clienteclientDeliveryAddress
get-vendedoresWEAPI003/get_vendedorseller
get-hierarquiasWEAPI003/get_vendedorhierarchy
get-relacionamentosWEAPI003/get_vendedorrelationship
get-produtosWEAPI005/get_produtoproduct
get-produtos-alcadaWEAPI005/get_produtoproductDiscountRule
get-categoriasWEAPI006/get_categoriacategory
get-cond-pagamentoWEAPI007/get_cond_pagamentopaymentCondition
get-tabela-precoWEAPI008/get_tabela_precopriceTable (itens)
get-tabela-preco-cabecalhoWEAPI008/get_tabela_precopriceTableData
get-exp-bonificacaoWEAPI010/get_exp_bonifbonusExpectation
get-transportadorasWEAPI011/get_transportadoracarrier
get-segmentosWEAPI012/get_segmentosegment
get-perfisWEAPI012/get_segmentoprofile
get-contas-receberWEAPI013/get_contas_receberreceivable
get-status-pedidoWEAPI018/get_status_pedidoorderStatus
get-promocoesWEAPI027/get_promocaopromotion
get-motivos-cancelamentoWEAPI002/get_dados_tabela (SX5010, tabela ZL)orderCancellationOption
get-notas-pedidoWEAPI002/get_dados_tabela (SD2010 ⋈ SF3010)orderInvoice
get-boletos-pedidoWEAPI002/get_dados_tabela (⋈ SE1010)orderTicket
post-pedidoWEAPI017/post_pedido_venda— (OUTBOUND)

Sob demanda (exigem parâmetro, não têm cron): get-status-pedido (cNrPedido), get-notas-pedido e get-boletos-pedido (cPedido).

Três endpoints alimentam mais de uma entidade — o mesmo get_vendedor produz seller, hierarchy e relationship, cada um com seu de-para. É de propósito: o custo é uma chamada por entidade, e em troca cada canônico tem chave natural própria e sincroniza sozinho.

De-para: 1→N, chave composta e filtro​

Ver ADR-030. Três primitivas cobrem o que a WEAPI exige:

{
// 1) explode itens[] -> uma linha por (tabela, produto); o pai fica em _parent
"flatten": { "path": "itens", "parentKey": "_parent" },

// 2) descarta linhas que não são registro desta entidade
"filter": [{ "path": "gestor", "op": "notEmpty" }],

"rules": [
// 3) chave natural composta de pai + filho
{ "targetField": "externalCode", "template": "{{_parent.cod_tabela}}*{{produto}}", "required": true },
{ "targetField": "tableName", "source": "_parent.desc_tabela", "transforms": ["trim"] },
{ "targetField": "price", "source": "preco_produto", "type": "number" }
]
}
O de-para é projeção, não lógica

lockedPrice = !altera_preco e allowsDecimalQuantity = (unid_med == "KG") não são regras de mapeamento. O canônico guarda o dado como o ERP entrega (allowsPriceChange, unit); a derivação é do pipeline, onde a regra pode ser versionada, testada e auditada.

Pegadinhas reais desta fonte​

  • "cnpj " tem um espaço no fim. É assim que o ERP devolve a chave. A regra usa "source": "cnpj ".
  • nome é a razão social; nome_fantasia é o nome fantasia — fácil de inverter.
  • altera_preco é invertido em relação a "preço travado".
  • Vendedor sem gestor (topo da árvore) não vira relationship — daí o filter.
  • parc_titulo vem vazio em muitos títulos, então a chave sai como 000473324-2-. É o mesmo comportamento do integrador legado; mantido por paridade.

Inserir um pedido no ERP​

Quem chama o post-pedido não conhece o formato da WEAPI. Manda o canônico (params.order) e o de-para de saída do conector (requestMapping, ADR-031) monta o corpo — inclusive expandindo order.items em itens_pedido[]:

// entra: canônico
{ "order": {
"externalCode": "S260713193", // ⚠ máx. 10 chars — o ERP trunca o pedido_SFA
"operation": "I", "orderType": "N",
"clientExternalCode": "017541", "storeCode": "01",
"priceTableCode": "004", "paymentConditionExternalCode": "118",
"saleTypeExternalCode": "01", "sellerExternalCode": "3009",
"carrierWithdrawal": "2", "dedicatedLoad": "2", "paidSample": "N",
"items": [
{ "productCode": "010236.0180181", "quantity": 1, "unitPrice": 334.8, "total": 334.8 },
{ "productCode": "199143.0270121", "quantity": 1, "unitPrice": 197.89, "total": 197.89 }
]
} }

O requestMapping do conector traduz isso para cCliente/cTabela/cTpOperacao/itens_pedido[]:

"requestMapping": {
"rules": [
{ "targetPath": "cOpc", "source": "order.operation", "transforms": ["default"], "defaultValue": "I" },
{ "targetPath": "cCliente", "source": "order.clientExternalCode", "type": "string" },
{ "targetPath": "cTabela", "source": "order.priceTableCode", "type": "string" }
// …
],
"expand": [{ // ← 1→N na saída: o inverso do `flatten`
"targetPath": "itens_pedido",
"source": "order.items",
"itemKey": "item",
"rules": [
{ "targetPath": "cProduto", "source": "item.productCode", "type": "string" },
{ "targetPath": "nQuantidade", "source": "item.quantity", "type": "number" },
{ "targetPath": "nPrecoUnit", "source": "item.unitPrice", "type": "number" },
{ "targetPath": "cPedCliente", "source": "order.clientOrderCode" } // o pai continua visível
]
}]
}

Validado ao vivo: o pedido 218234 entrou com duas linhas e o ERP devolveu total: 532.69 — a soma exata das duas, ou seja, a expansão chegou lá.

Resposta: { pedido, pedido_SFA, operacao, mensagem, icms_st, total } — pedido é o número no ERP.

Campos obrigatórios que o Protheus rejeita se vierem vazios​

Descobertos um a um, cada um com um 409 (validado ao vivo até o pedido 218233 entrar):

CampoSe vier errado/vazioValor válido
cTpOperacaoWEFATR36 — "Tipo de produto 'PA' inválido""01" (tipo de venda)
cPedClienteC6_PEDCLI — "Apenas números podem ser preenchidos"só dígitos (ou vazio)
cAmostraPagaMT410TOK — "Favor informar se pedido é amostra""S" / "N"
cTranspRetiraZZ_TRANSPR — "Transportadoras que não estão cadastradas…""1" / "2" — não aceita vazio
cCargaDedicadaidem"1" / "2"
pedido_SFA é truncado em 10 caracteres

Mandamos cPedidoSFA: "SFA260713192644" e o ERP devolveu pedido_SFA: "SFA2607131". O campo tem limite de 10 chars. O identificador do pedido no SFA precisa caber em 10 caracteres, senão o get-status-pedido não consegue casar o retorno com o pedido de origem.

Preço em centavos no legado

O canônico do SFA legado (Mongo) guarda price/total em centavos (117000 = R$ 1.170,00) e o erp-create-order divide por 100 ao enviar. A priceTable do SFA novo guarda em reais — não há conversão, mas isso precisa ficar explícito quando o canônico de pedido for desenhado.

Scripts prontos (montam o corpo a partir de dados canônicos já sincronizados):

# monta e MOSTRA o payload, sem enviar nada
powershell -File scripts\test-pedido-wessel.ps1

# insere de verdade no ERP de dev
powershell -File scripts\test-pedido-wessel.ps1 -Apply

# acompanha o pedido criado (status + NF + boleto)
powershell -File scripts\status-pedido-wessel.ps1 -Pedido 000123 -Documentos

Operação​

# aplica entidades canônicas + conectores + agendamentos (idempotente)
powershell -File scripts\apply-wessel-weapi.ps1

# valida o de-para de cada conector contra o ERP, sem gravar
powershell -File scripts\dryrun-wessel-weapi.ps1

# sincroniza tudo, na ordem de dependência
powershell -File scripts\sync-wessel-weapi.ps1

Os agendamentos são criados desligados — ligue-os em Integrações → Execuções quando a carga inicial estiver conferida.

Quem consome: o wessel-backend​

O backend legado da Wessel (wessel-backend, Express + Mongo) deixou de chamar o ERP direto e passou a puxar o canônico daqui — GET /integrations/entities/{key}/records, autenticado por x-api-key (ADR-034). O de-para que antes era um *.mapper.integrator.ts em código virou a configuração dos conectores desta página.

17 integradores do lado dele consomem as entidades acima. Duas regras importam para quem mexer nos conectores:

  • Só carga cheia migrou. Chamada com filtro do ERP (cVendedor, cPedido, cCodigo) continua indo ao Sankhya, porque a listagem canônica não sabe interpretar esses parâmetros — devolveria a entidade inteira parecendo um resultado plausível. Por isso orderInvoice, orderTicket e priceTableData não são consumidos de cá: são consultas por pedido/tabela.
  • Renomear campo de entidade é mudança de contrato. Do lado do Wessel existe um mapFromSfa por integrador, que traduz estes nomes para o canônico interno dele. Renomear um campo aqui não quebra a compilação lá — o valor simplesmente chega undefined em runtime. Trate wessel-entities.json como interface pública e avise o consumidor.
  • Modele a fonte, não o consumidor. A tentação é batizar os campos com os nomes que o Wessel usa (companyName, cnpj). Não faça: a entidade é compartilhada entre projetos, e amarrá-la ao vocabulário de um backend específico atrapalha a padronização. Quem reconcilia é o mapFromSfa de cada consumidor.

Se o Wessel não achar registros de uma entidade, ele cai no caminho legado e loga [sfa] ... usando o caminho legado — então um conector quebrado aqui aparece lá como "SFA desligado", não como erro. Ver docs/sfa-integration.md no repositório do wessel-backend.

Fora do escopo​

  • Trade marketing (*-trade do integrador legado): alimentados por planilha Excel, não pela WEAPI. Não têm conector.
  • Metas e indicadores (goal, *-monitoring-indicators): vêm do GoodData, não do ERP.
  • Imagens de produto: FTP + sharp, não é REST.
  • Código morto do legado (branch, brand, channel, company, network, payment-method, sub-category, sale-type, devolution, credit-limit-config): o get() desses serviços devolve array vazio desde a migração do Sankhya — não há fonte para integrar.