{
  "name": "Order alert — 1 AI A Day (Day 15)",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "new-order",
        "options": {}
      },
      "id": "c998f8bc-9803-42fd-a3d7-9ae3227c32db",
      "name": "New order (webhook)",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        0
      ],
      "webhookId": "e05f7b8e-222b-4d42-8ce5-95d509f9ea87"
    },
    {
      "parameters": {
        "jsCode": "// ---- Map your platform's payload to these names ----------------------\n// Trigger one real order, open the webhook node's output, and change the\n// right-hand side of each line to the field names you actually receive.\nconst FIELD = {\n  orderId:  'order_id',\n  status:   'status',\n  items:    'items',          // an array of line items\n  sku:      'sku',            // inside each item\n  name:     'name',           // inside each item\n  variant:  'variant',        // inside each item\n  qty:      'qty',            // inside each item\n  state:    'ship_state',\n  payment:  'payment',        // 'COD' or anything else = prepaid\n  repeat:   'repeat_buyer',   // true / false\n};\n\n// Orders your packer should never see.\nconst SKIP_STATUSES = ['CANCELLED', 'UNPAID'];\n// ------------------------------------------------------------------------\n\nconst get = (obj, path) =>\n  path.split('.').reduce((o, k) => (o == null ? undefined : o[k]), obj);\n\nconst out = [];\nfor (const item of $input.all()) {\n  const o = item.json.body ?? item.json;   // webhook wraps the payload in .body\n  const status = String(get(o, FIELD.status) ?? '').toUpperCase();\n  const items = get(o, FIELD.items) ?? [];\n  if (SKIP_STATUSES.includes(status) || !items.length) continue;\n\n  const lines = items.map(i => {\n    const v = get(i, FIELD.variant);\n    return `${get(i, FIELD.qty) ?? 1} x ${get(i, FIELD.sku) ?? get(i, FIELD.name)}` +\n           (v ? ` — ${v}` : '');\n  });\n\n  const cod = String(get(o, FIELD.payment) ?? '').toUpperCase().includes('COD');\n  const message = [\n    `NEW ORDER #${get(o, FIELD.orderId) ?? '?'}`,\n    ...lines,\n    `Ship to: ${get(o, FIELD.state) ?? '?'}`,\n    `Payment: ${cod ? 'COD' : 'Prepaid'}`,\n    get(o, FIELD.repeat) ? 'Repeat buyer' : null,\n  ].filter(Boolean).join('\\n');\n\n  out.push({ json: { message } });\n}\n\n// Remember when we last saw an order, for the heartbeat.\n// (Only persists when the workflow is active — not on test runs.)\nif (out.length) {\n  $getWorkflowStaticData('global').lastOrderAt = new Date().toISOString();\n}\nreturn out;\n"
      },
      "id": "cdb2ac85-5566-4ab7-b084-ca5ea75759ec",
      "name": "Filter & format",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://REPLACE-WITH-YOUR-MESSAGING-ENDPOINT/send",
        "sendBody": true,
        "specifyBody": "keypair",
        "bodyParameters": {
          "parameters": [
            {
              "name": "to",
              "value": "REPLACE-WITH-PACKING-GROUP-OR-NUMBER"
            },
            {
              "name": "text",
              "value": "={{ $json.message }}"
            }
          ]
        },
        "options": {}
      },
      "id": "2f383365-e8f7-404f-b8c0-cd8ea8c0ef5a",
      "name": "Send to packers",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        480,
        0
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://REPLACE-WITH-YOUR-MESSAGING-ENDPOINT/send",
        "sendBody": true,
        "specifyBody": "keypair",
        "bodyParameters": {
          "parameters": [
            {
              "name": "to",
              "value": "REPLACE-WITH-YOUR-OWN-NUMBER"
            },
            {
              "name": "text",
              "value": "=Order alert failed to send. Check n8n. Order text was:\n{{ $json.message }}"
            }
          ]
        },
        "options": {}
      },
      "id": "b4056250-f76e-496b-bb10-2c82e31a5048",
      "name": "Alert me: send failed",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        720,
        120
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 1
            }
          ]
        }
      },
      "id": "b0cc87ed-005d-4a26-ae53-41288deb9c68",
      "name": "Every hour",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        320
      ]
    },
    {
      "parameters": {
        "jsCode": "// ---- Your shop's rhythm -------------------------------------------------\nconst QUIET_HOURS = 6;        // alert if no order for this long...\nconst OPEN_FROM = 9;          // ...between these hours (local time)\nconst OPEN_TO = 22;\nconst TZ_OFFSET_HOURS = 8;    // Malaysia is UTC+8\n// ------------------------------------------------------------------------\n\nconst s = $getWorkflowStaticData('global');\nconst now = new Date();\nconst hour = (now.getUTCHours() + TZ_OFFSET_HOURS) % 24;\nif (hour < OPEN_FROM || hour >= OPEN_TO) return [];\n\nconst hoursSince = iso => (now - new Date(iso)) / 36e5;\n\n// Don't repeat the same alert every hour.\nif (s.lastQuietAlert && hoursSince(s.lastQuietAlert) < QUIET_HOURS) return [];\n\nlet message = null;\nif (!s.lastOrderAt) {\n  message = 'Order alerts: no order recorded since this workflow was activated. ' +\n            'If that is not expected, check the trigger.';\n} else if (hoursSince(s.lastOrderAt) >= QUIET_HOURS) {\n  message = `Order alerts have been quiet for ${Math.floor(hoursSince(s.lastOrderAt))}h. ` +\n            'Either sales stopped or the trigger did — check which.';\n}\nif (!message) return [];\n\ns.lastQuietAlert = now.toISOString();\nreturn [{ json: { message } }];\n"
      },
      "id": "6fff7c5a-0b76-4c42-bfd5-0a607e72527d",
      "name": "Heartbeat check",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        320
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://REPLACE-WITH-YOUR-MESSAGING-ENDPOINT/send",
        "sendBody": true,
        "specifyBody": "keypair",
        "bodyParameters": {
          "parameters": [
            {
              "name": "to",
              "value": "REPLACE-WITH-YOUR-OWN-NUMBER"
            },
            {
              "name": "text",
              "value": "={{ $json.message }}"
            }
          ]
        },
        "options": {}
      },
      "id": "7fc9eff6-cc7a-47d6-9ccf-eb6e4366c436",
      "name": "Alert me: gone quiet",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        480,
        320
      ]
    }
  ],
  "connections": {
    "New order (webhook)": {
      "main": [
        [
          {
            "node": "Filter & format",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter & format": {
      "main": [
        [
          {
            "node": "Send to packers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send to packers": {
      "main": [
        [],
        [
          {
            "node": "Alert me: send failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every hour": {
      "main": [
        [
          {
            "node": "Heartbeat check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Heartbeat check": {
      "main": [
        [
          {
            "node": "Alert me: gone quiet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {
    "New order (webhook)": [
      {
        "json": {
          "headers": {},
          "params": {},
          "query": {},
          "body": {
            "order_id": "[ORDER]",
            "status": "READY_TO_SHIP",
            "items": [
              {
                "sku": "BAG-15",
                "name": "Laptop bag",
                "variant": "15-inch / Navy",
                "qty": 2
              },
              {
                "sku": "SLV-13",
                "name": "Laptop sleeve",
                "variant": "13-inch / Black",
                "qty": 1
              }
            ],
            "ship_state": "Selangor",
            "payment": "COD",
            "repeat_buyer": true
          }
        }
      }
    ]
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false,
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}