Changes
Comparing empty → 160dc49.
| @@ -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 | | |