Este guia constrói os dois trabalhos mais necessários pelos fornecedores:
- Sincronização noturna: envie produtos e clientes do seu ERP para o TradeCatalog.
- Monitor de pedidos: a cada alguns minutos, puxe novos pedidos para o seu ERP e marque-os como reconhecidos.
Você precisa de uma chave de API de Leitura e Escrita. Veja Autenticação.
Como a sincronização noturna se comportaLink para esta seção#
- Os produtos são correspondidos por SKU e os clientes por código de conta, então executar a sincronização duas vezes é inofensivo.
- Novos produtos ficam disponíveis no seu preço de lista imediatamente.
- Preços de lista alterados não ficam disponíveis. Eles são coletados em alterações de preço em rascunho. Cada manhã, alguém abre Alterações de Preço no app, verifica a pré-visualização e os avisos, escolhe uma data e publica. Os clientes podem ser notificados por e-mail sobre seus novos preços.
brand,category,manufacturerRefedescriptionsão escritos como enviados, então sempre envie o produto completo.- Os
emailediscountBpdos clientes são escritos como enviados também:nullos apaga.
Envie apenas o que mudou
Se seu ERP puder lhe dizer quais produtos mudaram desde a última execução, envie apenas esses. Menos solicitações, e menos produtos para escanear em cada rascunho.
Sincronização noturna (Node.js)Link para esta seção#
Requer Node.js 20 ou mais recente, para o fetch integrado. Substitua as duas funções fromErp com leituras do seu sistema.
// sync.mjs — run nightly: TRADECATALOG_API_KEY=tc_live_… node sync.mjs
const BASE = 'https://tradecatalog.app/api/v1'
const KEY = process.env.TRADECATALOG_API_KEY
const sleep = (seconds) => new Promise((r) => setTimeout(r, seconds * 1000))
async function api(method, path, body, attempt = 0) {
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${KEY}`,
'Content-Type': 'application/json',
},
body: body && JSON.stringify(body),
})
if ((res.status === 429 || res.status >= 500) && attempt < 5) {
await sleep(Number(res.headers.get('Retry-After')) || 2 ** attempt)
return api(method, path, body, attempt + 1)
}
const json = await res.json()
if (!res.ok) {
const detail = json.error.issues
?.map((i) => `${i.path}: ${i.message}`)
.join('; ')
throw new Error(`${method} ${path}: ${json.error.message} ${detail ?? ''}`)
}
return json
}
// Replace these with reads from your ERP. Money in pence, discounts in basis points.
async function productsFromErp() {
return [
{
sku: 'HX-8510',
name: 'Hex bolt M10 x 50 zinc',
listPrice: 1950,
unitLabel: 'Box of 100',
brand: 'Northgate',
category: 'Bolts',
availability: 'in_stock',
},
]
}
async function customersFromErp() {
return [
{
accountCode: 'OAK01',
name: 'Oakfield Joinery',
email: 'accounts@oakfieldjoinery.co.uk',
discountBp: 500,
},
]
}
const batches = (rows, size = 100) =>
Array.from({ length: Math.ceil(rows.length / size) }, (_, i) =>
rows.slice(i * size, i * size + size),
)
const { data: workspace } = await api('GET', '/workspace')
console.log(`Syncing into ${workspace.name} (${workspace.currency})`)
for (const customers of batches(await customersFromErp())) {
const { data } = await api('PUT', '/customers', { customers })
console.log(`Customers: ${data.created} new, ${data.updated} updated`)
}
// Every batch adds its price changes to the same draft.
let priceChangeId
for (const products of batches(await productsFromErp())) {
const { data } = await api('PUT', '/products', { products, priceChangeId })
console.log(`Products: ${data.created} new, ${data.updated} updated`)
priceChangeId = data.priceChangeId ?? priceChangeId
if (data.unknownCustomers.length)
console.warn('Unknown customers in onlyFor:', data.unknownCustomers)
}
if (priceChangeId) {
const { data } = await api('GET', `/price-changes/${priceChangeId}`)
console.log(
`Draft price change: ${data.counts.up} up, ${data.counts.down} down,` +
` ${data.warnings.length} warnings. Publish it in TradeCatalog → Price changes.`,
)
}
Clientes primeiro, então produtos com onlyFor podem encontrá-los.
Execute-o a partir do cron, uma tarefa agendada ou o gerenciador de tarefas do seu ERP:
0 2 * * * cd /opt/tradecatalog && TRADECATALOG_API_KEY=tc_live_… node sync.mjs >> sync.log 2>&1
Monitor de pedidos (Node.js)Link para esta seção#
A cada alguns minutos, busque novos pedidos, entregue cada um ao seu ERP e marque-o como reconhecido. O comprador recebe um e-mail dizendo que seu pedido foi reconhecido.
// orders.mjs — run every 5 minutes. Copy BASE, KEY, sleep and api() from sync.mjs.
async function sendToErp(order) {
// Create the sales order in your system. Use order.reference as its external
// reference, and check it first so a retry can't create it twice.
console.log(
`${order.reference} for ${order.customer.accountCode}: ${order.lines.length} lines`,
)
}
const { data: newOrders } = await api('GET', '/orders?status=new')
for (const summary of newOrders.reverse()) {
const { data: order } = await api('GET', `/orders/${summary.id}`)
await sendToErp(order)
await api('PATCH', `/orders/${order.id}`, { status: 'acknowledged' })
}
Os pedidos mais antigos são tratados primeiro (reverse()), porque a lista é da mais nova para a mais antiga.
Se a etapa do seu ERP for bem-sucedida, mas o PATCH falhar, o pedido ainda será new na próxima vez. Por isso, sendToErp deve verificar se há order.reference antes de criar algo.
Mais tarde, quando um pedido for enviado, marque-o como despachado com os detalhes de rastreamento na observação:
await api('PATCH', `/orders/${orderId}`, {
status: 'dispatched',
note: 'DPD tracking 1554 2210 0931',
})
Monitor de pedidos (Python)Link para esta seção#
O mesmo monitor com requests:
# orders.py: run every 5 minutes. TRADECATALOG_API_KEY=tc_live_… python orders.py
import os
import time
import requests
BASE = "https://tradecatalog.app/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['TRADECATALOG_API_KEY']}"
def api(method, path, body=None, attempt=0):
res = session.request(method, BASE + path, json=body, timeout=30)
if (res.status_code == 429 or res.status_code >= 500) and attempt < 5:
time.sleep(int(res.headers.get("Retry-After", 2**attempt)))
return api(method, path, body, attempt + 1)
data = res.json()
if not res.ok:
raise RuntimeError(f"{method} {path}: {data['error']['message']}")
return data
def send_to_erp(order):
# Create the sales order in your system; skip it if order["reference"] exists.
print(order["reference"], order["customer"]["accountCode"], len(order["lines"]), "lines")
for summary in reversed(api("GET", "/orders?status=new")["data"]):
order = api("GET", f"/orders/{summary['id']}")["data"]
send_to_erp(order)
api("PATCH", f"/orders/{order['id']}", {"status": "acknowledged"})
Dinheiro e códigosLink para esta seção#
listPrice,unitPrice,lineTotaletotalestão em unidades menores (pence para GBP). Divida por 100 para obter libras.- Os preços dos pedidos são como você os definiu: antes do imposto se seu
taxLabeldiz “ex …” (seu ERP adiciona IVA quando fatura), incluindo imposto se diz “inc …”. - Use os códigos de conta de cliente do seu ERP como códigos de conta do TradeCatalog, para que ambos os sistemas correspondam sem uma tabela de correspondência.
Limites a considerarLink para esta seção#
- 100 linhas por
PUT, e 120 solicitações por minuto por chave. Um catálogo de 10.000 produtos é 100 solicitações, cerca de um minuto. GET /orderscobre os 200 pedidos mais recentes correspondentes. Faça a varredura com frequência suficiente para que menos de 200 novos pedidos cheguem entre as execuções.- Publicar alterações de preço, regras de desconto e preços acordados é feito no app.