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 definemitemsPath. - Vários endpoints trazem envelope pai→filhos numa só chamada (tabela com
itens[], cliente comtitulos[]). O de-para resolve isso comflatten— ver ADR-030. - Consultas genéricas passam SQL cru em
cQuery(WEAPI002/get_dados_tabela), sobre tabelas do Protheus (SX5010,SD2010,SE1010).
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).
| Conector | Endpoint | Entidade canônica |
|---|---|---|
get-clientes | WEAPI004/get_cliente (cLoja=01) | client |
get-clientes-enderecos | WEAPI004/get_cliente | clientDeliveryAddress |
get-vendedores | WEAPI003/get_vendedor | seller |
get-hierarquias | WEAPI003/get_vendedor | hierarchy |
get-relacionamentos | WEAPI003/get_vendedor | relationship |
get-produtos | WEAPI005/get_produto | product |
get-produtos-alcada | WEAPI005/get_produto | productDiscountRule |
get-categorias | WEAPI006/get_categoria | category |
get-cond-pagamento | WEAPI007/get_cond_pagamento | paymentCondition |
get-tabela-preco | WEAPI008/get_tabela_preco | priceTable (itens) |
get-tabela-preco-cabecalho | WEAPI008/get_tabela_preco | priceTableData |
get-exp-bonificacao | WEAPI010/get_exp_bonif | bonusExpectation |
get-transportadoras | WEAPI011/get_transportadora | carrier |
get-segmentos | WEAPI012/get_segmento | segment |
get-perfis | WEAPI012/get_segmento | profile |
get-contas-receber | WEAPI013/get_contas_receber | receivable |
get-status-pedido | WEAPI018/get_status_pedido | orderStatus |
get-promocoes | WEAPI027/get_promocao | promotion |
get-motivos-cancelamento | WEAPI002/get_dados_tabela (SX5010, tabela ZL) | orderCancellationOption |
get-notas-pedido | WEAPI002/get_dados_tabela (SD2010 ⋈ SF3010) | orderInvoice |
get-boletos-pedido | WEAPI002/get_dados_tabela (⋈ SE1010) | orderTicket |
post-pedido | WEAPI017/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" }
]
}
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í ofilter. parc_titulovem vazio em muitos títulos, então a chave sai como000473324-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):
| Campo | Se vier errado/vazio | Valor válido |
|---|---|---|
cTpOperacao | WEFATR36 — "Tipo de produto 'PA' inválido" | "01" (tipo de venda) |
cPedCliente | C6_PEDCLI — "Apenas números podem ser preenchidos" | só dígitos (ou vazio) |
cAmostraPaga | MT410TOK — "Favor informar se pedido é amostra" | "S" / "N" |
cTranspRetira | ZZ_TRANSPR — "Transportadoras que não estão cadastradas…" | "1" / "2" — não aceita vazio |
cCargaDedicada | idem | "1" / "2" |
pedido_SFA é truncado em 10 caracteresMandamos 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.
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 issoorderInvoice,orderTicketepriceTableDatanã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
mapFromSfapor 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 chegaundefinedem runtime. Tratewessel-entities.jsoncomo 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 é omapFromSfade 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 (
*-tradedo 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): oget()desses serviços devolve array vazio desde a migração do Sankhya — não há fonte para integrar.