{
  "name": "Atlas Labs: bridge accepted sales orders to work orders",
  "nodes": [
    {
      "id": "webhook-trigger",
      "name": "Catch office.sales_order.accepted",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [-200, 0],
      "parameters": {
        "httpMethod": "POST",
        "path": "atlas-labs-sales-order-accepted",
        "responseMode": "onReceived",
        "responseData": "{\"ok\":true}",
        "options": {}
      },
      "webhookId": "atlas-labs-sales-order-accepted"
    },
    {
      "id": "verify-signature",
      "name": "Verify Standard Webhooks signature",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [20, 0],
      "parameters": {
        "language": "javaScript",
        "jsCode": "// Standard Webhooks verification (docs/PLAN.md §4.6): webhook-signature is\n// \"v1,<base64 HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{raw body}`>\",\n// keyed by the base64 part of the whsec_... secret this subscription was\n// created with. Put that secret in the ATLAS_LABS_WEBHOOK_SECRET\n// environment variable of the n8n instance -- never hard-code it here.\nconst crypto = require('crypto');\n\nconst item = $input.item;\nconst headers = item.json.headers || {};\nconst id = headers['webhook-id'];\nconst timestamp = headers['webhook-timestamp'];\nconst signatureHeader = headers['webhook-signature'];\nconst secret = $env.ATLAS_LABS_WEBHOOK_SECRET;\n\nif (!id || !timestamp || !signatureHeader) {\n  throw new Error('missing Standard Webhooks headers (webhook-id/webhook-timestamp/webhook-signature)');\n}\nif (!secret || !secret.startsWith('whsec_')) {\n  throw new Error('ATLAS_LABS_WEBHOOK_SECRET is not set to a whsec_... secret');\n}\n\n// The raw body n8n parsed into item.json.body must be re-serialised exactly\n// as it was received for the HMAC to match; n8n's Webhook node gives us the\n// parsed JSON, so this recipe assumes JSON.stringify reproduces the same\n// bytes (true for Atlas Labs payloads, which contain no extra whitespace).\nconst rawBody = JSON.stringify(item.json.body);\nconst key = Buffer.from(secret.slice('whsec_'.length), 'base64');\nconst expectedMac = crypto.createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest('base64');\n\nconst providedSignatures = signatureHeader.split(' ').map((part) => part.split(',')[1]).filter(Boolean);\nconst valid = providedSignatures.some((sig) => {\n  const a = Buffer.from(sig, 'base64');\n  const b = Buffer.from(expectedMac, 'base64');\n  return a.length === b.length && crypto.timingSafeEqual(a, b);\n});\nif (!valid) {\n  throw new Error('webhook-signature did not match — rejecting delivery');\n}\n\nreturn [{ json: item.json.body }];"
      }
    },
    {
      "id": "if-accepted-event",
      "name": "IF office.sales_order.accepted",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [240, 0],
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict" },
          "conditions": [
            {
              "id": "event-type",
              "leftValue": "={{$json.type}}",
              "rightValue": "office.sales_order.accepted",
              "operator": { "type": "string", "operation": "equals" }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "get-sales-orders",
      "name": "GET sales orders",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [460, -80],
      "parameters": {
        "method": "GET",
        "url": "={{$env.ATLAS_LABS_BASE_URL || 'https://automationatlas.io/labs/api/v1'}}/office/sales-orders",
        "sendQuery": true,
        "queryParameters": { "parameters": [{ "name": "limit", "value": "100" }] },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [{ "name": "Authorization", "value": "=Bearer {{$env.ATLAS_LABS_OFFICE_KEY}}" }]
        },
        "options": {}
      }
    },
    {
      "id": "pick-sales-order",
      "name": "Find the accepted order",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [680, -80],
      "parameters": {
        "language": "javaScript",
        "jsCode": "// GET /office/sales-orders/{id} exists and is the simpler choice for a new\n// build. This template predates it and still lists the first page of\n// sales orders and filters client-side (see templates/README.md \"API gaps\").\n// A production integration would page through `cursor` if the order isn't on\n// the first page.\nconst salesOrderId = $('Verify Standard Webhooks signature').first().json.data.sales_order_id;\n// List endpoints wrap their rows as {items: [...], next_cursor}, not a bare array (PLAN §4.1 \"Lists\").\nconst orders = $input.first().json.data.items;\nconst order = (Array.isArray(orders) ? orders : []).find((o) => o.id === salesOrderId);\nif (!order) {\n  throw new Error(`sales order ${salesOrderId} was not found in the first page of GET /office/sales-orders`);\n}\nconst eventId = $('Verify Standard Webhooks signature').first().json.id;\nreturn [{\n  json: {\n    sales_order_id: order.id,\n    quantity: order.quantity,\n    due_date: order.due_sim_day,\n    idempotency_key: eventId,\n  },\n}];"
      }
    },
    {
      "id": "create-work-order",
      "name": "POST /factory/work-orders",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [900, -80],
      "parameters": {
        "method": "POST",
        "url": "={{$env.ATLAS_LABS_BASE_URL || 'https://automationatlas.io/labs/api/v1'}}/factory/work-orders",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            { "name": "Authorization", "value": "=Bearer {{$env.ATLAS_LABS_FACTORY_KEY}}" },
            { "name": "Idempotency-Key", "value": "={{$json.idempotency_key}}" },
            { "name": "Content-Type", "value": "application/json" }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { sales_order_id: $json.sales_order_id, quantity: $json.quantity, due_date: $json.due_date } }}",
        "options": {
          "response": { "response": { "neverError": true, "fullResponse": true } }
        }
      }
    },
    {
      "id": "if-bridged-ok",
      "name": "IF bridged (2xx or already_bridged)",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [1120, -80],
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose" },
          "conditions": [
            {
              "id": "created",
              "leftValue": "={{$json.statusCode}}",
              "rightValue": 201,
              "operator": { "type": "number", "operation": "equals" }
            },
            {
              "id": "already-bridged",
              "leftValue": "={{$json.statusCode === 409 && $json.body && $json.body.error && $json.body.error.code}}",
              "rightValue": "already_bridged",
              "operator": { "type": "string", "operation": "equals" }
            }
          ],
          "combinator": "or"
        }
      }
    },
    {
      "id": "bridge-succeeded",
      "name": "Bridged (success)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [1340, -160],
      "parameters": {}
    },
    {
      "id": "bridge-failed",
      "name": "Bridge failed",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [1340, 0],
      "parameters": {
        "errorMessage": "=POST /factory/work-orders failed: {{$json.statusCode}} {{JSON.stringify($json.body && $json.body.error)}}"
      }
    },
    {
      "id": "ignore-other-events",
      "name": "Ignore (not this event)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [460, 80],
      "parameters": {}
    }
  ],
  "connections": {
    "Catch office.sales_order.accepted": {
      "main": [[{ "node": "Verify Standard Webhooks signature", "type": "main", "index": 0 }]]
    },
    "Verify Standard Webhooks signature": {
      "main": [[{ "node": "IF office.sales_order.accepted", "type": "main", "index": 0 }]]
    },
    "IF office.sales_order.accepted": {
      "main": [
        [{ "node": "GET sales orders", "type": "main", "index": 0 }],
        [{ "node": "Ignore (not this event)", "type": "main", "index": 0 }]
      ]
    },
    "GET sales orders": {
      "main": [[{ "node": "Find the accepted order", "type": "main", "index": 0 }]]
    },
    "Find the accepted order": {
      "main": [[{ "node": "POST /factory/work-orders", "type": "main", "index": 0 }]]
    },
    "POST /factory/work-orders": {
      "main": [[{ "node": "IF bridged (2xx or already_bridged)", "type": "main", "index": 0 }]]
    },
    "IF bridged (2xx or already_bridged)": {
      "main": [
        [{ "node": "Bridged (success)", "type": "main", "index": 0 }],
        [{ "node": "Bridge failed", "type": "main", "index": 0 }]
      ]
    }
  },
  "pinData": {},
  "settings": { "executionOrder": "v1" },
  "meta": {
    "templateCredsSetupCompleted": false,
    "instanceId": "atlas-labs-templates"
  }
}
