# Synchronisez avec votre ERP

> Une synchronisation complète des produits et clients chaque nuit, ainsi qu'un poller de commandes qui récupère les nouvelles commandes dans votre propre système.

Ce guide met en œuvre les deux tâches dont les fournisseurs ont le plus besoin :

1. **Synchronisation nocturne** : envoyer les produits et les clients depuis votre logiciel de gestion de production (ERP) vers TradeCatalog.
2. **Poller de commandes** : toutes les quelques minutes, récupérer les nouvelles commandes dans votre ERP et les marquer comme reçues.

Vous avez besoin d'une clé d'API **Lecture et écriture**. Voir [Authentification](/docs/api/authentication).

## Comment se comporte la synchronisation nocturne

- Les produits sont associés par référence (SKU) et les clients par code de compte, donc exécuter la synchronisation deux fois est inoffensif.
- Les nouveaux produits deviennent visibles à leur prix de liste immédiatement.
- Les prix de liste modifiés **ne** deviennent pas visibles. Ils sont stockés dans des changements de prix en brouillon. Chaque matin, quelqu'un ouvre **Changements de prix** dans l'application, vérifie l'aperçu et les avertissements, sélectionne une date et publie. Les clients peuvent recevoir un courriel avec leurs nouveaux prix.
- `brand`, `category`, `manufacturerRef` et `description` sont écrits comme envoyés, donc envoyez toujours le produit complet.
- Les `email` et `discountBp` des clients sont également écrits comme envoyés : `null` les efface.

**Envoyez uniquement ce qui a changé:** Si votre ERP peut vous indiquer quels produits ont changé depuis la dernière exécution, envoyez uniquement ceux-ci. Moins de demandes, et moins de produits à scanner à chaque brouillon.

## Synchronisation nocturne (Node.js)

Nécessite Node.js 20 ou version ultérieure, pour le `fetch` intégré. Remplacez les deux fonctions `fromErp` par des lectures de votre système.

```js
// 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.`,
  )
}
```

Les clients sont traités en premier, donc les produits avec `onlyFor` peuvent les trouver.

Exécutez-le depuis cron, une tâche planifiée ou le gestionnaire de tâches de votre ERP :

```text
0 2 * * *  cd /opt/tradecatalog && TRADECATALOG_API_KEY=tc_live_… node sync.mjs >> sync.log 2>&1
```

## Poller de commandes (Node.js)

Toutes les quelques minutes, récupérez les nouvelles commandes, transmettez-les à votre ERP, puis marquez-les comme reçues. L'acheteur reçoit un courriel indiquant que sa commande a été reçue.

```js
// 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' })
}
```

Les commandes les plus anciennes sont traitées en premier (`reverse()`), car la liste est triée par date de création.

Si l'étape de votre ERP réussit mais que le `PATCH` échoue, la commande est toujours `new` lors de la prochaine exécution. C'est pourquoi `sendToErp` doit vérifier les `order.reference` avant de créer quoi que ce soit.

Plus tard, lorsqu'une commande est expédiée, marquez-la comme expédiée avec les détails de suivi dans la note :

```js
await api('PATCH', `/orders/${orderId}`, {
  status: 'dispatched',
  note: 'DPD tracking 1554 2210 0931',
})
```

## Poller de commandes (Python)

Le même poller avec [requests](https://requests.readthedocs.io) :

```python
# 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"})
```

## Argent et codes

- `listPrice`, `unitPrice`, `lineTotal` et `total` sont en unités mineures (pence pour le GBP). Divisez par 100 pour obtenir les livres.
- Les prix des commandes sont tels que vous les avez définis : avant taxe si votre `taxLabel` indique « ex … » (votre ERP ajoute la TVA lors de la facturation), incluant la taxe si cela indique « inc … ».
- Utilisez les codes de compte clients de votre ERP comme codes de compte TradeCatalog, afin que les deux systèmes correspondent sans table de correspondance.

## Limites à prévoir

- 100 lignes par `PUT`, et 120 requêtes par minute par clé. Un catalogue de 10 000 produits est de 100 requêtes, environ une minute.
- `GET /orders` couvre les 200 commandes les plus récentes correspondantes. Faites le poll souvent suffisamment pour que moins de 200 nouvelles commandes arrivent entre deux exécutions.
- La publication des changements de prix, des règles de remise et des prix convenus se fait dans l'application.
