Changes

Comparing empty160dc49.

@@ -1,0 +1,82 @@
1+---
2+title: ERP bridge
3+sort: 3
4+tags: [reference]
5+---
6+
7+# ERP bridge
8+
9+Order data flows from ZephyrCart into your ERP (Microsoft Dynamics 365 Business Central or SAP
10+Business One in 90 % of the deployments we see). The recommended shape is a thin worker that
11+subscribes to `order.created` and `order.refunded` webhooks and translates them into ERP API
12+calls.
13+
14+## Reference architecture
15+
16+```
17+ZephyrCart ──── webhook ───▶ Bridge worker ──── ERP API ────▶ Dynamics / SAP
18+ (your code)
19+ │
20+ ▼
21+ Dead-letter queue
22+ (for replays)
23+```
24+
25+## Why a worker, not a direct webhook to the ERP
26+
27+Three reasons:
28+
29+1. ERPs do not authenticate webhooks the same way we sign them. The worker terminates the
30+ signature, translates, and reauthenticates with the ERP's own scheme.
31+2. Field mapping changes more often than the ZephyrCart event schema. Keeping it in your worker
32+ lets you ship mappings without touching either side.
33+3. ERPs go down for maintenance windows. The worker is where you buffer.
34+
35+## Recommended technology
36+
37+- A small Cloud Run / AWS Lambda / Azure Function — request load is exactly
38+ "one execution per order event," which is the cheapest serverless shape there is.
39+- A persistent queue between webhook receipt and ERP push (Cloud Tasks, SQS, Service Bus) so the
40+ ERP outage doesn't lose data.
41+
42+## Idempotency at the ERP
43+
44+Send the ZephyrCart `order.id` as the external reference on the ERP record. If the ERP returns
45+"already exists," consider that a success and ack the webhook.
46+
47+## A worked example
48+
49+A Node.js worker on Google Cloud Run, posting into Dynamics 365 Business Central:
50+
51+```typescript
52+import { verifyZephyrSignature } from "./signing";
53+
54+export async function handle(req: Request): Promise<Response> {
55+ const body = await req.text();
56+ if (!verifyZephyrSignature(body, req.headers.get("x-zephyrcart-signature")!)) {
57+ return new Response("bad signature", { status: 401 });
58+ }
59+ const event = JSON.parse(body);
60+ if (event.type !== "order.created") {
61+ return new Response("ignored", { status: 200 });
62+ }
63+
64+ await dynamicsClient.createSalesOrder({
65+ externalReference: event.data.id, // idempotency anchor
66+ customerEmail: event.data.customer.email,
67+ currency: event.data.currency,
68+ lines: event.data.line_items.map(toErpLine),
69+ });
70+
71+ return new Response("ok", { status: 200 });
72+}
73+```
74+
75+## Failure modes worth handling
76+
77+| Failure | How to recover |
78+|---------------------------------------------|-------------------------------------------------------------|
79+| ERP returns 5xx | Return 5xx from the worker — ZephyrCart retries |
80+| ERP returns 4xx because the SKU is unknown | Log to dead-letter queue, page the catalog team |
81+| Worker times out at 10 seconds | Ack early, push to your own queue, process async |
82+| Signature verification fails | Return 401; investigate clock skew first |