{
 "id": "RhR5Acct00000001",
 "name": "R5 N11 Omise -> ใบกำกับ FlowAccount (จำลอง)",
 "active": false,
 "nodes": [
  {
   "id": "r5n13",
   "name": "Webhook",
   "type": "n8n-nodes-base.webhook",
   "typeVersion": 2,
   "position": [
    0,
    300
   ],
   "parameters": {
    "httpMethod": "POST",
    "path": "r5-omise",
    "responseMode": "responseNode",
    "options": {
     "rawBody": true
    }
   },
   "webhookId": "79fe06ff-5cda-0095-504f-de10fe5f5ec8"
  },
  {
   "id": "r5n14",
   "name": "Verify + decide",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    220,
    300
   ],
   "parameters": {
    "jsCode": "const it = $input.first();\nconst raw = it.binary && it.binary.data ? Buffer.from(it.binary.data.data, \"base64\").toString(\"utf8\") : JSON.stringify(it.json.body);\nconst H = Object.fromEntries(Object.entries(it.json.headers || {}).map(([k, v]) => [k.toLowerCase(), v]));// N11 (conditional): order -> Omise charge (test mode) -> FlowAccount tax invoice, against MOCK endpoints.\n// From the public docs (read 3 Nov 2026): Omise POST /charges, basic auth with the secret key, amount in the\n// smallest unit (satang), webhooks signed with Omise-Signature = hex HMAC-SHA256(base64-decoded secret,\n// timestamp + \".\" + raw body), comma separated during rotation. FlowAccount Open API needs the paid\n// Pro Business package, so the invoice body below is shaped to our mock only and must be checked against\n// the buyer's API reference once they have access.\nfunction chargeBody(order) {\n  const satang = order.items.reduce((s, it) => s + Math.round(it.price * 100) * it.qty, 0);\n  if (!Number.isInteger(satang) || satang < 2000) throw new Error(\"ยอดต่ำกว่า 20 บาท Omise ไม่รับ\");\n  return { amount: satang, currency: \"thb\", source: order.sourceId, description: \"order \" + order.id, metadata: { order_id: order.id } };\n}\n// An empty or undecodable secret would sign with an empty HMAC key that anyone can compute: refuse it.\n// The replay window has no \"off\" value: nowSec left out means the clock is used.\nfunction verifyOmise(rawBody, headers, secretB64, crypto, nowSec, toleranceSec = 300) {\n  if (typeof secretB64 !== \"string\") return false;\n  const key = Buffer.from(secretB64, \"base64\");\n  if (!key.length) return false;\n  if (nowSec == null) nowSec = Math.floor(Date.now() / 1000);\n  const sigs = String(headers[\"omise-signature\"] || \"\").split(\",\").map((s) => s.trim()).filter(Boolean);\n  const ts = String(headers[\"omise-signature-timestamp\"] || \"\");\n  if (!sigs.length || !/^\\d+$/.test(ts)) return false;\n  if (Math.abs(nowSec - Number(ts)) > toleranceSec) return false;\n  const want = crypto.createHmac(\"sha256\", key).update(ts + \".\" + rawBody).digest(\"hex\");\n  return sigs.some((s) => s.length === want.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(want)));\n}\n// event: Omise webhook event; seen: Set of event ids. Returns what to do next.\nfunction onChargeEvent(event, seen) {\n  if (seen.has(event.id)) return { action: \"ignore\", why: \"event ซ้ำ\" };\n  seen.add(event.id);\n  if (event.key !== \"charge.complete\") return { action: \"ignore\", why: event.key };\n  const ch = event.data || {};\n  if (ch.status !== \"successful\") return { action: \"notify\", why: \"ชำระไม่สำเร็จ: \" + ch.status };\n  return { action: \"invoice\", orderId: ch.metadata && ch.metadata.order_id, amount: ch.amount / 100, chargeId: ch.id };\n}\nfunction invoiceBody(order, chargeId) {\n  const sub = order.items.reduce((s, it) => s + Math.round(it.price * 100) * it.qty, 0); // VAT-inclusive prices\n  const base = Math.round(sub * 100 / 107);\n  return { contactName: order.customer.name, contactTaxId: order.customer.taxId || \"\", isVatInclusive: true, reference: order.id + \"/\" + chargeId,\n    items: order.items.map((it) => ({ name: it.name, quantity: it.qty, pricePerUnit: it.price })), subTotal: sub / 100, vatAmount: (sub - base) / 100, grandTotal: sub / 100 };\n}\n\n\nconst ok = verifyOmise(raw, H, $env.OMISE_WEBHOOK_SECRET, require(\"crypto\"), Math.floor(Date.now() / 1000));\nif (!ok) return [{ json: { code: 401, response: { error: \"bad signature\" } } }];\nconst st = $getWorkflowStaticData(\"global\"); st.seen = st.seen || [];\nconst seen = new Set(st.seen); const r = onChargeEvent(JSON.parse(raw), seen); st.seen = [...seen].slice(-500);\nconst order = st.orders && st.orders[r.orderId];\nreturn [{ json: { ...r, code: 200, response: { action: r.action }, invoice: r.action === \"invoice\" ? invoiceBody(order || { id: r.orderId, customer: { name: \"-\" }, items: [{ name: \"ยอดตามใบสั่ง\", qty: 1, price: r.amount }] }, r.chargeId) : null } }];"
   }
  },
  {
   "id": "r5n15",
   "name": "Respond",
   "type": "n8n-nodes-base.respondToWebhook",
   "typeVersion": 1.1,
   "position": [
    440,
    200
   ],
   "parameters": {
    "respondWith": "json",
    "responseBody": "={{ JSON.stringify($json.response) }}",
    "options": {
     "responseCode": "={{ $json.code }}"
    }
   }
  },
  {
   "id": "r5n16",
   "name": "Invoice?",
   "type": "n8n-nodes-base.if",
   "typeVersion": 2,
   "position": [
    440,
    400
   ],
   "parameters": {
    "conditions": {
     "options": {
      "caseSensitive": true,
      "leftValue": "",
      "typeValidation": "loose"
     },
     "conditions": [
      {
       "id": "c1",
       "leftValue": "={{ $json.action }}",
       "rightValue": "invoice",
       "operator": {
        "type": "string",
        "operation": "equals"
       }
      }
     ],
     "combinator": "and"
    },
    "options": {}
   }
  },
  {
   "id": "r5n17",
   "name": "FlowAccount tax invoice (mock)",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    660,
    400
   ],
   "parameters": {
    "method": "POST",
    "url": "={{ $env.BUYER_API_BASE }}/flowaccount/tax-invoices",
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{ JSON.stringify($json.invoice) }}",
    "options": {
     "timeout": 10000
    }
   },
   "onError": "continueErrorOutput",
   "retryOnFail": true,
   "maxTries": 3
  }
 ],
 "connections": {
  "Webhook": {
   "main": [
    [
     {
      "node": "Verify + decide",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Verify + decide": {
   "main": [
    [
     {
      "node": "Respond",
      "type": "main",
      "index": 0
     },
     {
      "node": "Invoice?",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Invoice?": {
   "main": [
    [
     {
      "node": "FlowAccount tax invoice (mock)",
      "type": "main",
      "index": 0
     }
    ]
   ]
  }
 },
 "settings": {
  "executionOrder": "v1",
  "saveDataErrorExecution": "all"
 },
 "pinData": {}
}