8
n8n 中文网amn8n.com

基于 OCR、Claude AI、Slack 和 Notion DB 的自动化发票付款追踪

高级

这是一个AI Summarization, Multimodal AI领域的自动化工作流,包含 92 个节点。主要使用 If, Code, Wait, Slack, Notion 等节点。 基于 OCR、Claude AI、Slack 和 Notion DB 的自动化发票付款追踪

前置要求
  • Slack Bot Token 或 Webhook URL
  • Notion API Key
  • 可能需要目标 API 的认证凭证
  • Anthropic API Key
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "meta": {
    "instanceId": "c8523726fb538ba356528013ec3b00a89ffd0cddf2d31a7bcce94063a36b7fec",
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "909b0322-55e4-4c3d-a5a9-9392d5bbf745",
      "name": "格式检查",
      "type": "n8n-nodes-base.code",
      "position": [
        -5808,
        1744
      ],
      "parameters": {
        "jsCode": "const files = $('Slack Trigger').first().json.files || [];\nconst results = [];\n\nfor (const file of files) {\n  const mimetype = file.mimetype || \"\";\n  const fileType = mimetype.split('/').pop().toLowerCase();\n  const fileName = file.name || file.title || \"unknown\";\n\n  const isImage = ['jpg', 'jpeg', 'png', 'webp'].includes(fileType);\n  const isDocument = ['pdf', 'doc', 'docx', 'txt', 'rtf', 'eml'].includes(fileType);\n  const isEmail = fileType === 'eml' || fileName.endsWith('.eml');\n\n  results.push({\n    json: {\n      ...file, // keep Slack file data like url_private_download\n      fileType,\n      mimetype,\n      isImage,\n      isDocument,\n      isEmail,\n    }\n  });\n}\n\nreturn results;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "ca8559e4-b29b-4a1a-b113-e70bba47f23a",
      "name": "检查格式",
      "type": "n8n-nodes-base.switch",
      "position": [
        -5584,
        1744
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Image",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "f5d6e124-c68e-42bb-a9bb-c97d54b09143",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.isImage }}",
                    "rightValue": "true"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Document",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "87759194-16d1-4593-8c9b-016aa7be4e03",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.isDocument }}",
                    "rightValue": "true"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "allMatchingOutputs": true
        }
      },
      "typeVersion": 3.2
    },
    {
      "id": "6543e986-8860-457d-a8de-bb052718e9c6",
      "name": "获取文档二进制文件",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -5360,
        1840
      ],
      "parameters": {
        "url": "={{ $json.url_private_download }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        },
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "slackApi"
      },
      "credentials": {
        "slackApi": {
          "id": "muT22qcO4YUdHAGU",
          "name": "Slack account"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "af88d22a-5a2d-4471-9ac9-b1cb9531bd35",
      "name": "OCR Space 解析1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -5136,
        1840
      ],
      "parameters": {
        "url": "https://api.ocr.space/parse/image",
        "method": "POST",
        "options": {
          "timeout": 100000
        },
        "sendBody": true,
        "contentType": "multipart-form-data",
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "filetype",
              "value": "PDF"
            },
            {
              "name": "file",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "data"
            },
            {
              "name": "scale",
              "value": "true"
            },
            {
              "name": "OCREngine",
              "value": "2"
            }
          ]
        },
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "id": "dkJjl1msBNIeIT5u",
          "name": "OCR Space"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2,
      "waitBetweenTries": 5000
    },
    {
      "id": "ecb68486-32af-41e3-a9a7-86c763205061",
      "name": "代码",
      "type": "n8n-nodes-base.code",
      "position": [
        -4688,
        1936
      ],
      "parameters": {
        "jsCode": "// $input.all() gives you the 3 separate OCR result items\nconst inputItems = $input.all();\n\nreturn inputItems.map((item, index) => {\n  const parsedResults = item.json.ParsedResults || [];\n\n  // Join all ParsedText per result into one string\n  const mergedText = parsedResults\n    .map(p => p.ParsedText || \"\")\n    .join(\"\\n\\n\")\n    .trim();\n\n  return {\n    json: {\n      index: index + 1,\n      mergedParsedText: mergedText\n    }\n  };\n});\n"
      },
      "typeVersion": 2
    },
    {
      "id": "acb3e33c-59de-4cf3-a008-a47af9673abb",
      "name": "检查解析错误3",
      "type": "n8n-nodes-base.if",
      "position": [
        -4912,
        1840
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "ddbbbfbb-e05b-4ca9-b607-65ad8ac748bb",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $('OCR Space Parse1').item.json.IsErroredOnProcessing }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "96e61695-6dfd-4cf4-8064-aea9d80284f0",
      "name": "基础LLM链",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        -4240,
        1888
      ],
      "parameters": {
        "text": "=Extract the invoice fields from this OCR text and populate the EXACT schema.\n\nTARGET SCHEMA:\n{\n  \"invoice_no\": \"\",\n  \"vendor\": \"\",\n  \"issue_date\": \"YYYY-MM-DD\",\n  \"due_date\": \"YYYY-MM-DD\",\n  \"currency\": \"\",\n  \"subtotal\": 0,\n  \"tax_total\": 0,\n  \"discount_total\": 0,\n  \"discount_percent\": 0,\n  \"amount_total\": 0,\n\n  \"amount_due\": 0,          // if shown (a.k.a. Balance Due); else 0\n  \"paid_amount\": 0,         // if shown or implied; else 0\n  \"paid_date\": \"\",          // if shown; else \"\"\n  \"receipt_no\": \"\",         // if shown; else \"\"\n  \"payment_method\": \"\",     // if shown; else \"\"\n\n  \"destination_account\": \"\",\n  \"payment_ref\": \"\",\n  \"doc_says_paid\": false,\n  \"status\": \"\",\n  \"notes\": \"\",\n  \"source_file_id\": \"\",\n  \"source_file_name\": \"\",\n  \"source_file_url\": \"\",\n  \"ingestion_batch\": \"\",\n  \"line_items\": [],\n  \"attachments\": []\n}\n\nOCR TEXT:\n{{ $json.OCRResult }}\n",
        "batching": {},
        "messages": {
          "messageValues": [
            {
              "message": "=You are an extraction engine for US invoices. Output EXACTLY the JSON fields of the target schema—no extra keys, no comments, no markdown. Do not invent values.\n\nGeneral rules\n- Locale: US. Normalize dates to YYYY-MM-DD.\n- Numbers: plain decimals (no symbols/commas).\n- currency: infer from symbol/labels when present (“$” ⇒ “USD”), else \"\".\n- If a field is unavailable: use \"\" for strings, 0 for numbers, false for booleans, [] for arrays.\n\nDiscount fields\n- If the doc shows a percent (e.g., “Discount 49%”), set discount_percent to the decimal (0.49) and discount_total to 0 unless a currency discount is also printed.\n- If the doc shows a currency discount (e.g., “Discount $60.27”), set discount_total to that amount; discount_percent is 0 unless the percent is explicitly printed too.\n- If both forms are printed, fill both.\n\nTotals\n- amount_total: prefer the document’s explicit “Amount due/Total/Total amount/Grand total”.\n- If not printed but subtotal, discount_* and tax_total are present, compute:\n  amount_total = subtotal − (discount_total OR subtotal × discount_percent) + tax_total.\n- Otherwise leave amount_total = 0.\n\nPartial payments\n- If the doc shows Amount due / Balance due:\n  • Set amount_due to that value.\n  • If amount_total is known and amount_due < amount_total, set paid_amount = amount_total − amount_due (≥ 0).\n- If the doc shows Amount paid / Payment history:\n  • Sum payments and set paid_amount.\n  • If amount_due is not shown but amount_total is known, set amount_due = max(amount_total − paid_amount, 0).\n- If a paid date, receipt number, or payment method is explicitly shown, set paid_date, receipt_no, payment_method; otherwise leave them empty.\n- Clamp impossible values (no negatives; when amount_total is known, neither paid_amount nor amount_due may exceed it).\n\nDocument-based status\n- If amount_due == 0 AND the document states PAID (or shows “Amount due $0.00”): status = \"Paid (Unverified)\".\n- Else if paid_amount > 0 AND amount_due > 0: status = \"Partially Paid\".\n- Else: status = \"Unpaid\".\n- Set doc_says_paid = true only if the document itself explicitly says “PAID” or shows “Amount due $0.00”; otherwise false.\n\nLine items\n- line_items: include when clearly listed, as\n  [{\"description\": string, \"qty\": number, \"unit_price\": number, \"amount\": number}], else [].\n\nAttachments\n- attachments: [] unless explicit links are present.\n\nReturn ONLY the JSON object matching the target schema.\n"
            }
          ]
        },
        "promptType": "define"
      },
      "retryOnFail": false,
      "typeVersion": 1.7
    },
    {
      "id": "0aaa9bdf-05b2-4dc1-ad6d-4de8926ef9f1",
      "name": "Anthropic 聊天模型4",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        -4240,
        2048
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-3-5-haiku-20241022",
          "cachedResultName": "Claude Haiku 3.5"
        },
        "options": {}
      },
      "credentials": {
        "anthropicApi": {
          "id": "UBlNqbwvx1EotIn7",
          "name": "Anthropic account"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "54187b60-dabd-42a7-b5c9-d46199dbed73",
      "name": "清理 AI 响应",
      "type": "n8n-nodes-base.code",
      "position": [
        -3888,
        1888
      ],
      "parameters": {
        "jsCode": "// n8n Code node — Run Once for All Items\n// Repair + Enforce schema + attach Slack file info\n// Adds discount_percent support + derives discount_amount & stable amount_total\n// + Partial payment normalization (amount_due, paid_amount, paid_date, receipt_no, payment_method)\n\nfunction stripCodeFences(s) {\n  return String(s || \"\")\n    .replace(/```(?:json)?/gi, \"\")\n    .replace(/```/g, \"\")\n    .trim();\n}\n\nfunction findTopLevelJson(s) {\n  const str = String(s || \"\");\n  let start = -1, depth = 0;\n  for (let i = 0; i < str.length; i++) {\n    const ch = str[i];\n    if (start === -1) {\n      if (ch === \"{\" || ch === \"[\") { start = i; depth = 1; }\n    } else {\n      if (ch === \"{\" || ch === \"[\") depth++;\n      if (ch === \"}\" || ch === \"]\") depth--;\n      if (depth === 0) return str.slice(start, i + 1);\n    }\n  }\n  return null;\n}\n\nfunction safeParseAny(x) {\n  if (Array.isArray(x)) return x;\n  if (x && typeof x === \"object\" && (x.invoice_no || x.vendor || x.line_items)) return x;\n\n  let txt = \"\";\n  if (typeof x === \"string\") txt = x;\n  else if (typeof x?.text === \"string\") txt = x.text;\n  else if (typeof x?.output === \"string\") txt = x.output;\n  else if (typeof x?.response === \"string\") txt = x.response;\n  else if (typeof x?.content === \"string\") txt = x.content;\n  else if (typeof x?.message === \"string\") txt = x.message;\n  else txt = JSON.stringify(x ?? \"\");\n\n  txt = stripCodeFences(txt);\n\n  try { return JSON.parse(txt); } catch {}\n  const block = findTopLevelJson(txt);\n  if (!block) throw new Error(\"No JSON found in LLM output\");\n  return JSON.parse(block);\n}\n\n// coercers\nfunction num(v) {\n  if (v === \"\" || v == null || (typeof v === \"number\" && !isFinite(v))) return 0;\n  if (typeof v === \"number\") return v;\n  let s = String(v).trim();\n  if (!s) return 0;\n  // keep digits , . -\n  s = s.replace(/[^\\d,.\\-]/g, \"\");\n  if (s.includes(\",\") && s.includes(\".\")) s = s.replace(/,/g, \"\");\n  else if (s.includes(\",\") && !s.includes(\".\")) {\n    if ((s.match(/,/g) || []).length === 1 && /\\d,\\d{1,2}$/.test(s)) s = s.replace(\",\", \".\");\n    else s = s.replace(/,/g, \"\");\n  }\n  const n = Number(s);\n  return isFinite(n) ? n : 0;\n}\nconst str = (v) => (v == null ? \"\" : String(v).trim());\nconst bool = (v) => v === true;\nconst two = (n) => Math.round((Number(n) || 0) * 100) / 100;\n\n// parse percent from \"49%\", \"49\", 0.49, etc. → 0..1\nfunction pct(v) {\n  if (v == null || v === \"\") return 0;\n  if (typeof v === \"string\" && v.includes(\"%\")) return Math.min(Math.max(num(v) / 100, 0), 1);\n  let n = num(v);\n  if (n > 1) n = n / 100;\n  if (n < 0) n = 0;\n  if (n > 1) n = 1;\n  return n;\n}\n\nfunction normalizeOne(obj, fileUrl) {\n  const line_items = Array.isArray(obj?.line_items)\n    ? obj.line_items.map(x => ({\n        description: str(x?.description),\n        qty: num(x?.qty),\n        unit_price: num(x?.unit_price),\n        amount: num(x?.amount)\n      }))\n    : [];\n\n  // base fields from LLM\n  const base = {\n    invoice_no: str(obj?.invoice_no),\n    vendor: str(obj?.vendor),\n    issue_date: str(obj?.issue_date),\n    due_date: str(obj?.due_date),\n    currency: str(obj?.currency),\n    subtotal: num(obj?.subtotal),\n    tax_total: num(obj?.tax_total),\n\n    // discounts (raw)\n    discount_total: num(obj?.discount_total),      // fixed amount if present\n    discount_percent: pct(obj?.discount_percent),  // normalized 0..1\n\n    amount_total: num(obj?.amount_total),\n\n    // payment/receipt extras (raw; normalized below)\n    amount_due: num(obj?.amount_due),\n    paid_amount: num(obj?.paid_amount),\n    paid_date: str(obj?.paid_date),\n    receipt_no: str(obj?.receipt_no),\n    payment_method: str(obj?.payment_method),\n\n    destination_account: str(obj?.destination_account),\n    payment_ref: str(obj?.payment_ref),\n    doc_says_paid: bool(obj?.doc_says_paid),\n    status: str(obj?.status),\n    notes: str(obj?.notes),\n    source_file_id: str(obj?.source_file_id),\n    source_file_name: str(obj?.source_file_name),\n    source_file_url: str(obj?.source_file_url),\n    ingestion_batch: str(obj?.ingestion_batch),\n    line_items,\n    attachments: Array.isArray(obj?.attachments) ? obj.attachments.map(str) : []\n  };\n\n  // ---- discount math ----\n  const discount_amount =\n    base.discount_total > 0\n      ? two(base.discount_total)\n      : two(base.subtotal * (base.discount_percent || 0));\n\n  // compute fallback amount_total\n  const calc_total = two(base.subtotal - discount_amount + base.tax_total);\n\n  // choose stable amount_total for pipeline (prefer LLM if sane; else calc)\n  const provided = base.amount_total;\n  const useProvided =\n    provided > 0 && Math.abs(provided - calc_total) <= Math.max(0.01, calc_total * 0.01);\n  base.amount_total = useProvided ? two(provided) : calc_total;\n\n  // Notion percent wants 0..1\n  base.discount_percent_for_notion = +(base.discount_percent || 0).toFixed(4);\n  base.discount_amount = discount_amount; // handy for dedup & debug\n  base._amount_total_source = useProvided ? \"provided\" : \"calculated\";\n\n  // ---- partial / payment normalization ----\n  const at = Number(base.amount_total || 0);\n  let due  = Number(base.amount_due || 0);\n  let paid = Number(base.paid_amount || 0);\n\n  // Derive missing pieces when amount_total is known\n  if (at > 0) {\n    if (paid > 0 && due <= 0) due = Math.max(0, two(at - paid));\n    if (due > 0 && paid <= 0) paid = Math.max(0, two(at - due));\n    // If doc says PAID and due is zero but paid missing, assume fully paid\n    if (base.doc_says_paid && due === 0 && paid <= 0) paid = at;\n    // Clamp impossible values\n    if (paid < 0) paid = 0;\n    if (due < 0) due = 0;\n    if (paid > at) paid = at;\n    if (due > at) due = at;\n    if (paid + due > at) due = Math.max(0, two(at - paid));\n  } else {\n    // amount_total unknown — still clamp non-negatives\n    if (paid < 0) paid = 0;\n    if (due  < 0) due  = 0;\n  }\n\n  base.paid_amount = two(paid);\n  base.amount_due  = two(due);\n\n  // Receipt hints payload for later routing/Notion Receipt DB\n  base.receipt_hints = {\n    paid_amount: base.paid_amount,\n    paid_date: base.paid_date,\n    receipt_no: base.receipt_no,\n    payment_method: base.payment_method\n  };\n\n  // Nudge status if inconsistent with amounts\n  if (at > 0) {\n    if (base.amount_due === 0 && (base.doc_says_paid || base.paid_amount >= at - 0.01)) {\n      base.status = \"Paid (Unverified)\";\n    } else if (base.paid_amount > 0 && base.amount_due > 0) {\n      base.status = \"Partially Paid\";\n    } else if (base.paid_amount === 0 && base.amount_due >= at - 0.01) {\n      base.status = \"Unpaid\";\n    }\n  }\n\n  // ---- Attach Slack file URL (if available) ----\n  if (fileUrl) {\n    let fileName = \"\", fileId = \"\";\n    try {\n      const u = new URL(fileUrl);\n      const seg = u.pathname.split(\"/\").filter(Boolean);\n      fileName = decodeURIComponent(seg[seg.length - 1] || \"\");\n      const m = u.pathname.match(/-(F[A-Z0-9]+)\\//i);\n      if (m) fileId = m[1];\n    } catch {\n      const m1 = fileUrl.match(/\\/download\\/([^?\\/#]+)($|\\?)/i);\n      if (m1) fileName = decodeURIComponent(m1[1]);\n      const m2 = fileUrl.match(/-(F[A-Z0-9]+)\\//i);\n      if (m2) fileId = m2[1];\n    }\n\n    base.source_file_url = fileUrl;\n    if (!base.source_file_name) base.source_file_name = fileName;\n    if (!base.source_file_id) base.source_file_id = fileId;\n\n    const atSet = new Set(base.attachments.map(String));\n    atSet.add(fileUrl);\n    base.attachments = Array.from(atSet);\n  }\n\n  // Guard status values\n  const allowedStatus = new Set([\n    \"Unpaid\",\"Paid (Unverified)\",\"Paid (Verified)\",\n    \"Partially Paid\",\"Overdue\",\"Void/Cancelled\",\"Duplicate\"\n  ]);\n  if (!allowedStatus.has(base.status)) base.status = \"Unpaid\";\n\n  // Basic date sanity\n  const dateOk = (s) => /^(\\d{4})-(\\d{2})-(\\d{2})$/.test(s);\n  if (base.issue_date && !dateOk(base.issue_date)) base.issue_date = \"\";\n  if (base.due_date && !dateOk(base.due_date)) base.due_date = \"\";\n\n  return base;\n}\n\n// --- Collect Slack file URLs from either node (PDF or Image lanes) ---\nfunction collectSlackUrls(nodeName) {\n  try {\n    return $(nodeName).all()\n      .map(it =>\n        String(\n          it.json?.url_private_download ||\n          it.json?.file?.url_private_download || // some Slack nodes nest under file\n          \"\"\n        )\n      )\n      .filter(Boolean);\n  } catch {\n    return [];\n  }\n}\n\nconst urlsDoc = collectSlackUrls('Take Binary Files for Document'); // PDF lane\nconst urlsImg = collectSlackUrls('Take Binary Files');              // Image lane\n\n// Pick a URL for item index i; prefer same index, then first available\nfunction pickFileUrl(i) {\n  return urlsDoc[i] || urlsImg[i] || urlsDoc[0] || urlsImg[0] || \"\";\n}\n\n// --- Process ALL incoming items; each may yield one or many outputs ---\nconst outputs = [];\n\nfor (const [idx, item] of $input.all().entries()) {\n  let parsed = safeParseAny(item.json);\n\n  if (parsed && !Array.isArray(parsed) && typeof parsed === \"object\") {\n    const maybeArr = parsed.data || parsed.items || parsed.result || parsed.invoices;\n    if (Array.isArray(maybeArr)) parsed = maybeArr;\n  }\n  if (!Array.isArray(parsed)) parsed = [parsed];\n\n  const fileUrlForThisItem = pickFileUrl(idx);\n\n  for (const obj of parsed) {\n    const out = normalizeOne(obj, fileUrlForThisItem);\n    outputs.push({ json: out });\n  }\n}\n\nreturn outputs;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "0e729384-88a1-4ee3-9bd1-ccc218462dbc",
      "name": "内部检查重复发票",
      "type": "n8n-nodes-base.code",
      "position": [
        -3664,
        1888
      ],
      "parameters": {
        "jsCode": "// Input: many cleaned invoices (each item.json is like your sample)\n// Output: clusters of duplicates; each cluster picks one \"keep\" and lists \"drop\"\n\nconst input = $input.all().map(i => i.json);\n\n// --- Helpers ---\nconst canonVendor = (v) => {\n  if (!v) return \"\";\n  return String(v)\n    .toLowerCase()\n    .replace(/[.,'’\"]/g, \" \")\n    .replace(/\\b(pt|tbk|cv|inc|ltd|llc|llp|plc|corp|co|gmbh|sarl|bv)\\b/g, \"\")\n    .replace(/\\s+/g, \" \")\n    .trim();\n};\nconst canonInv = (s) => {\n  if (!s) return \"\";\n  return String(s)\n    .toUpperCase()\n    .replace(/\\s+/g, \"\")\n    .replace(/[–—]/g, \"-\")\n    .replace(/[^A-Z0-9\\/\\-\\.]/g, \"\")\n    .replace(/-+/g, \"-\")\n    .trim();\n};\nconst round2 = (n) => Math.round(Number(n || 0) * 100) / 100;\n\n// --- NEW: scoring helpers so PAID wins ---\nfunction parseYmd(s) {\n  // \"YYYY-MM-DD\" -> epoch ms; invalid -> 0\n  const m = String(s || \"\").match(/^(\\d{4})-(\\d{2})-(\\d{2})/);\n  if (!m) return 0;\n  return Date.UTC(+m[1], +m[2] - 1, +m[3]);\n}\n\nfunction statusRank(status, docPaid) {\n  const s = String(status || \"\").toLowerCase();\n  if (docPaid === true || /paid\\s*\\(verified\\)/i.test(status)) return 4;\n  if (/paid/i.test(s)) return 3;              // Paid (Unverified)\n  if (/partially/i.test(s)) return 2;         // Partially Paid\n  if (/overdue/i.test(s)) return 1;           // Overdue still beats Unpaid\n  return 0;                                   // Unpaid / others\n}\n\nfunction richness(o) {\n  let r = 0;\n  if (Array.isArray(o.attachments) && o.attachments.length) r += 2;\n  if (o.source_file_id) r += 1;\n  if (o.notes) r += 0.5;\n  if (Array.isArray(o.line_items)) r += Math.min(2, o.line_items.length * 0.1);\n  return r;\n}\n\n// choose one representative to keep\nfunction chooseMaster(arr) {\n  let best = null, bestScore = -Infinity, bestWhy = \"\";\n  for (const o of arr) {\n    const paidRank = statusRank(o.status, o.doc_says_paid); // 0..4\n    const newestDate = Math.max(parseYmd(o.due_date), parseYmd(o.issue_date));\n    const rich = richness(o);\n\n    // Weights: paidness >>> recency > richness\n    const score =\n      paidRank * 100 +               // ensure any \"Paid\" beats \"Unpaid\"\n      (newestDate / 8.64e7) * 0.1 +  // days since epoch * 0.1\n      rich;                          // small tie-break\n\n    const why = `paidRank=${paidRank} doc_says_paid=${!!o.doc_says_paid} status=${o.status || \"\"} newest=${newestDate ? new Date(newestDate).toISOString().slice(0,10) : \"-\"} richness=${rich}`;\n\n    if (score > bestScore) {\n      best = o; bestScore = score; bestWhy = why;\n    }\n  }\n  best._keep_reason_debug = bestWhy;\n  best._keep_score_debug = bestScore;\n  return best || arr[0];\n}\n\n// --- Build clusters ---\nconst clustersMap = new Map(); // key -> { reason, list: [] }\n\nfor (const o of input) {\n  const vendor_canon = canonVendor(o.vendor);\n  const invoice_no_canon = canonInv(o.invoice_no);\n  const currency = (o.currency || \"\").toUpperCase().trim();\n  const amt = round2(o.amount_total);\n  const due = (o.due_date || o.issue_date || \"\").slice(0, 10);\n\n  let key, reason;\n  if (invoice_no_canon) {\n    key = `INV#${vendor_canon}|${invoice_no_canon}`;\n    reason = \"SAME_VENDOR + INVOICE_NO\";\n  } else {\n    key = `FALLBACK#${vendor_canon}|${currency}|${amt}|${due}`;\n    reason = \"NO_INVOICE_NO → SAME_VENDOR + CURRENCY + AMOUNT + DATE\";\n  }\n\n  if (!clustersMap.has(key)) clustersMap.set(key, { reason, list: [] });\n  clustersMap.get(key).list.push({\n    ...o,\n    _vendor_canon: vendor_canon,\n    _invoice_no_canon: invoice_no_canon,\n    _fallback_key: !invoice_no_canon\n  });\n}\n\n// --- Emit only duplicate clusters (>1) ---\nconst results = [];\nfor (const [key, { reason, list }] of clustersMap.entries()) {\n  if (list.length <= 1) continue;\n\n  const amountSet = Array.from(new Set(list.map(x => round2(x.amount_total))));\n  const currencySet = Array.from(new Set(list.map(x => (x.currency || \"\").toUpperCase().trim())));\n\n  // annotate if amounts differ\n  let reasonDetailed = reason;\n  if (reason.startsWith(\"SAME_VENDOR\") && amountSet.length > 1) {\n    reasonDetailed += \" (amount differs across copies)\";\n  }\n\n  // --- NEW: count paid vs unpaid; prefer paid in keep\n  const paidCount = list.filter(x => x.doc_says_paid || /^paid/i.test(String(x.status || \"\"))).length;\n  if (paidCount > 0) reasonDetailed += \" | keep prefers PAID copy\";\n\n  const keep = chooseMaster(list);\n  const drop = list.filter(x => x !== keep);\n\n  results.push({\n    json: {\n      isDuplicate: true,\n      cluster_key: key,\n      reason: reasonDetailed,\n      count: list.length,\n      amounts: amountSet,\n      currencies: currencySet,\n      paid_count: paidCount,                 // debug/metrics\n      keep,                                  // should now be one of C/D (paid)\n      drop,\n      entries: list\n    }\n  });\n}\n\n// No duplicates? Emit a simple flag.\nif (results.length === 0) {\n  return [{ json: { isDuplicate: false, info: \"No duplicate invoices found.\" } }];\n}\n\nreturn results;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "dbcea768-1818-4a07-932c-3161e06342b5",
      "name": "Slack 触发器",
      "type": "n8n-nodes-base.slackTrigger",
      "position": [
        -6032,
        1744
      ],
      "webhookId": "d13d3a70-faed-40f9-99fd-00a219c3af7c",
      "parameters": {
        "options": {},
        "trigger": [
          "message"
        ],
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C09701BLY2Z",
          "cachedResultName": "expenses"
        }
      },
      "credentials": {
        "slackApi": {
          "id": "muT22qcO4YUdHAGU",
          "name": "Slack account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "0202318a-016b-4130-930c-bd092f18c492",
      "name": "与原始数据合并",
      "type": "n8n-nodes-base.code",
      "position": [
        -3440,
        1888
      ],
      "parameters": {
        "jsCode": "// n8n Code node — Merge originals with dedup clusters and tag each original\n// Upstream nodes:\n//   - \"Cleans AI Response\"  -> contains ORIGINAL parsed items (A, B, C)\n//   - Input to this node    -> contains cluster objects from \"Internal Check Duplicate Invoice\"\n// Output: one item per original, with dedup_* fields added\n\n// ---- helpers ----\nfunction canonVendor(v) {\n  return String(v || '')\n    .toLowerCase()\n    .replace(/[^a-z0-9\\s]/g, ' ')     // strip punctuation (ASCII-safe)\n    .replace(/\\b(pt|tbk|inc|ltd|llc)\\b/g, '')\n    .replace(/\\s+/g, ' ')\n    .trim();\n}\nfunction canonInvoiceNo(inv) {\n  return String(inv || '')\n    .toUpperCase()\n    .replace(/\\s+/g, '')\n    .replace(/[^A-Z0-9\\/\\-.]/g, '');\n}\nfunction clusterKeyFromItem(it) {\n  const vendor = canonVendor(it.vendor);\n  const inv = canonInvoiceNo(it.invoice_no);\n  if (inv) return `INV#${vendor}|${inv}`;\n  const currency = String(it.currency || '').toUpperCase();\n  const amt = Number(it.amount_total || it.subtotal || 0) || 0;\n  const date = String(it.due_date || it.issue_date || '');\n  return `FALLBACK#${vendor}|${currency}|${amt}|${date}`;\n}\n// Stable-ish signature to match items across steps\nfunction sig(o) {\n  return [\n    o.source_file_id || '',\n    o.source_file_url || '',\n    String(o.vendor || '').trim(),\n    String(o.invoice_no || '').trim(),\n    String(o.issue_date || '').trim(),\n    String(o.due_date || '').trim(),\n    String(o.currency || '').trim().toUpperCase(),\n    String(Number(o.amount_total ?? o.subtotal ?? 0) || 0)\n  ].join('||');\n}\n\n// ---- 1) load originals (A, B, C) ----\nconst originals = $('Cleans AI Response').all().map(i => i.json);\n\n// ---- 2) load and normalize clusters from THIS node input ----\nconst rawIn = $input.all().map(i => i.json);\nconst clusters = [];\nfor (const x of rawIn) {\n  if (Array.isArray(x)) {\n    for (const c of x) if (c && c.cluster_key) clusters.push(c);\n  } else if (x && x.cluster_key) {\n    clusters.push(x);\n  }\n}\n// If your duplicate checker sometimes emits {clusters:[...]}, support that too\nfor (const x of rawIn) {\n  if (x && Array.isArray(x.clusters)) {\n    for (const c of x.clusters) if (c && c.cluster_key) clusters.push(c);\n  }\n}\n\n// ---- 3) index clusters (by cluster_key) ----\nconst clusterIndex = new Map();  // cluster_key -> { meta, keepSig, dropSigSet, keepUsed }\nfor (const c of clusters) {\n  const meta = {\n    cluster_key: c.cluster_key,\n    reason: c.reason || '',\n    count: Number(c.count || (c.entries ? c.entries.length : 1)) || 1,\n    amounts: c.amounts || [],\n    currencies: c.currencies || []\n  };\n  const keepSig = c.keep ? sig(c.keep) : null;\n  const dropSigSet = new Set((c.drop || []).map(sig));\n  clusterIndex.set(c.cluster_key, { meta, keepSig, dropSigSet, keepUsed: false });\n}\n\n// Try to find matching cluster for an original item\nfunction findClusterForOriginal(item) {\n  const s = sig(item);\n  for (const [ck, rec] of clusterIndex) {\n    if ((rec.keepSig && rec.keepSig === s) || rec.dropSigSet.has(s)) return [ck, rec];\n  }\n  const ck = clusterKeyFromItem(item);\n  return [ck, clusterIndex.get(ck) || null];\n}\n\n// ---- 4) merge labels onto originals ----\nconst out = [];\n\nfor (const item of originals) {\n  const [ck, rec] = findClusterForOriginal(item);\n\n  if (!rec) {\n    out.push({\n      json: {\n        ...item,\n        dedup_cluster_key: ck || '',\n        dedup_role: 'unique',\n        dedup_count: 1,\n        dedup_reason: 'no_cluster_match'\n      }\n    });\n    continue;\n  }\n\n  const { meta, keepSig, dropSigSet } = rec;\n  const s = sig(item);\n  let role;\n\n  if (meta.count <= 1) {\n    role = 'unique';\n  } else if (keepSig && s === keepSig && !rec.keepUsed) {\n    role = 'keep';           // first original that matches the keepSig\n    rec.keepUsed = true;     // ensure only one \"keep\" per cluster\n  } else {\n    role = 'drop';           // everything else in that cluster is a duplicate\n  }\n\n  out.push({\n    json: {\n      ...item,\n      dedup_cluster_key: meta.cluster_key,\n      dedup_role: role,                 // \"keep\" | \"drop\" | \"unique\"\n      dedup_count: meta.count,\n      dedup_reason: meta.reason,\n      dedup_amounts: meta.amounts,\n      dedup_currencies: meta.currencies\n    }\n  });\n}\n\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "123fc9c7-f6d3-4be5-b02a-d6c65faa5bc9",
      "name": "将数据值合并到一个键中",
      "type": "n8n-nodes-base.code",
      "position": [
        -4464,
        1888
      ],
      "parameters": {
        "jsCode": "// Code node — Run once for all items\n// Preceded by a Merge (Wait for all) so ALL items from both paths arrive here.\n\nconst out = [];\n\nfunction get(v) {\n  return typeof v === 'string' ? v.trim() : '';\n}\n\nfor (const it of $input.all()) {\n  const j = it.json || {};\n\n  // Your two possible sources per item\n  const fromPdf   = get(j.mergedParsedText);                 // Error3 (PDF path)\n  const fromImage = get(j?.ParsedResults?.[0]?.ParsedText);  // Error2 (image path)\n\n  const OCRResult = fromPdf || fromImage || '';\n  const OCRSource = fromPdf\n    ? 'Error3.mergedParsedText'\n    : fromImage\n    ? 'Error2.ParsedResults[0].ParsedText'\n    : 'none';\n\n  // Keep any file metadata you already have on the item\n  out.push({\n    json: {\n      ...j,\n      OCRResult,\n      OCRSource\n    }\n  });\n}\n\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "88aae252-885b-4433-af13-88fea42ad24e",
      "name": "决定数据去向",
      "type": "n8n-nodes-base.if",
      "position": [
        -3216,
        1888
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "9cb0bdab-37f9-4536-8b75-de7e3bdd4a76",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.dedup_role }}",
              "rightValue": "drop"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "c3b08864-40e6-4c71-b0ad-f46231351a92",
      "name": "创建数据库查询",
      "type": "n8n-nodes-base.code",
      "position": [
        -2992,
        1312
      ],
      "parameters": {
        "jsCode": "// Run ONCE FOR ALL ITEMS\nconst out = [];\n\nfunction windowDates(anchor) {\n  if (!anchor) return null;\n  const d = new Date(anchor + \"T00:00:00Z\");\n  if (isNaN(d)) return null;\n  const s = new Date(d); s.setUTCDate(s.getUTCDate() - 30);\n  const e = new Date(d); e.setUTCDate(e.getUTCDate() + 30);\n  return [s.toISOString().slice(0,10), e.toISOString().slice(0,10)];\n}\n\nfor (const { json: j } of $input.all()) {\n  const haveInvoiceNo = !!j?.invoice_no;\n\n  // Anchor date:\n  // - If we know the invoice number, anchor to invoice-level dates\n  // - Otherwise, paid_date is acceptable for finding the invoice by heuristics\n  const anchor = haveInvoiceNo\n    ? (j?.issue_date || j?.due_date || \"\")\n    : (j?.receipt_hints?.paid_date || j?.issue_date || j?.due_date || \"\");\n  const dates = windowDates(anchor);\n\n  const and = [];\n\n  if (haveInvoiceNo) {\n    // Prefer canon properties if your DB has them; else fall back to title field\n    if (j.invoice_no_canon) {\n      and.push({ property: \"Invoice No (Canon)\", rich_text: { equals: String(j.invoice_no_canon) } });\n      if (j.vendor_canon) {\n        and.push({ property: \"Vendor (Canon)\", rich_text: { equals: String(j.vendor_canon) } });\n      }\n    } else {\n      and.push({ property: \"Invoice No\", title: { equals: String(j.invoice_no) } });\n    }\n    if (j.currency) {\n      and.push({ property: \"Currency\", select: { equals: String(j.currency) } });\n    }\n\n    // IMPORTANT: no Amount Total filter when Invoice No is present\n    // (If you want a belt-and-suspenders check, you can add a WIDE band on amount_total instead,\n    // but never use the receipt paid amount.)\n\n  } else {\n    // Fallback: vendor + currency + invoice total band + date\n    if (j.vendor_canon) {\n      and.push({ property: \"Vendor (Canon)\", rich_text: { equals: String(j.vendor_canon) } });\n    } else if (j.vendor) {\n      and.push({ property: \"Vendor\", rich_text: { equals: String(j.vendor) } });\n    }\n    if (j.currency) {\n      and.push({ property: \"Currency\", select: { equals: String(j.currency) } });\n    }\n    const total = Number(j?.amount_total);\n    if (!Number.isNaN(total) && total > 0) {\n      const eps = Math.max(5, total * 0.03); // ± $5 or 3%\n      and.push(\n        { property: \"Amount Total\", number: { greater_than_or_equal_to: +(total - eps).toFixed(2) } },\n        { property: \"Amount Total\", number: { less_than_or_equal_to:   +(total + eps).toFixed(2) } },\n      );\n    }\n  }\n\n  if (dates) {\n    const [start, end] = dates;\n    and.push({\n      or: [\n        { property: \"Issue Date\", date: { on_or_after: start, on_or_before: end } },\n        { property: \"Due Date\",   date: { on_or_after: start, on_or_before: end } },\n      ]\n    });\n  }\n\n  const queryBody = { filter: { and }, page_size: haveInvoiceNo ? 10 : 5 };\n  out.push({ json: { ...j, queryBody } });\n}\n\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "de2b4a0f-eaba-4d8c-ac76-220f14725f10",
      "name": "检查数据库发票",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -2768,
        1312
      ],
      "parameters": {
        "url": "https://api.notion.com/v1/databases/24df7557957380739611da8371404254/query",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.queryBody }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Notion-Version",
              "value": "2022-06-28"
            }
          ]
        },
        "nodeCredentialType": "notionApi"
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "067db3c6-fc83-46a2-8ad4-0b8cec9ab83e",
      "name": "合并发票项目",
      "type": "n8n-nodes-base.code",
      "position": [
        -2544,
        1312
      ],
      "parameters": {
        "jsCode": "// After \"Check DB Invoice\" (HTTP). Mode: Run once for all items. Single output.\n\n// Originals aligned by index (from your query-prep node)\nconst originals = $('Create Query for DB').all().map(i => i.json);\n\n// Toggle if you want partial flows to auto-route (true) or go to manual_review (false)\nconst ENABLE_PARTIAL = true;\n\n// ---------- Notion helpers ----------\nconst getTitle  = (p, name) => (p?.properties?.[name]?.title || [])\n  .map(t => t.plain_text || '').join('');\nconst getRich   = (p, name) => (p?.properties?.[name]?.rich_text || [])\n  .map(t => t.plain_text || '').join('');\nconst getSelect = (p, name) => p?.properties?.[name]?.select?.name || '';\nconst getNumber = (p, name) => (\n  typeof p?.properties?.[name]?.number === 'number'\n    ? p.properties[name].number\n    : null\n);\nconst getDate   = (p, name) => p?.properties?.[name]?.date?.start || '';\n\nconst isPaidName = (s) => /^paid/i.test(String(s || ''));\n\n// quick equality for “no-change/dup” check\nfunction coreSame(src, db) {\n  const eq = (x,y)=> String(x||'') === String(y||'');\n  const numEq=(x,y)=> {\n    const nx=Number(x??NaN), ny=Number(y??NaN);\n    if (Number.isNaN(nx) && Number.isNaN(ny)) return true;\n    return Math.abs(nx - ny) < 0.001;\n  };\n  return (\n    eq(src.vendor, db.vendor) &&\n    eq(src.invoice_no, db.invoice_no) &&\n    eq(src.currency, db.currency) &&\n    numEq(src.amount_total, db.amount_total) &&\n    eq(src.issue_date, db.issue_date) &&\n    eq(src.due_date, db.due_date)\n  );\n}\n\nconst out = [];\nconst tolAbs = 0.01;   // absolute amount tolerance\nconst tolPct = 0.01;   // 1% relative tolerance\n\n$input.all().forEach((it, idx) => {\n  const src   = originals[idx] || {};\n  const http  = it.json || {};\n  const pages = Array.isArray(http.results) ? http.results : [];\n  const manyMatches = pages.length > 1;\n  const page  = pages[0] || null;\n\n  // ---------- DB snapshot ----------\n  const db = page ? {\n    page_id:     page.id,\n    invoice_no:  getTitle(page, 'Invoice No'),\n    vendor:      getRich(page,  'Vendor'),\n    currency:    getSelect(page,'Currency'),\n    amount_total:getNumber(page,'Amount Total'),\n    issue_date:  getDate(page,  'Issue Date'),\n    due_date:    getDate(page,  'Due Date'),\n    status:      getSelect(page,'Status'),\n  } : null;\n  const dbPaid = db ? isPaidName(db.status) : false;\n\n  // ---------- mismatches ----------\n  const vendorMismatch   = !!(db && db.vendor && src.vendor && db.vendor !== src.vendor);\n  const currencyMismatch = !!(db && db.currency && src.currency && db.currency !== src.currency);\n\n  const inAmt = Number(src.amount_total ?? NaN);\n  const dbAmt = Number(db?.amount_total ?? NaN);\n  const amountMismatch =\n    db && Number.isFinite(inAmt) && Number.isFinite(dbAmt) &&\n    (Math.abs(inAmt - dbAmt) > tolAbs) &&\n    (Math.abs(inAmt - dbAmt) / Math.max(1, dbAmt) > tolPct);\n\n  const fallbackAmbiguous = !src.invoice_no && manyMatches;\n\n  // ---------- derive payment signals from AMOUNTS (ignore labels) ----------\n  const statusStr = String(src.status || '');\n  const totalIn   = Number(src.amount_total ?? NaN);\n  const paidAmtIn = Number(\n    (src?.receipt_hints?.paid_amount ?? src.paid_amount) ?? NaN\n  );\n  const dueIn     = Number(src.amount_due ?? NaN);\n\n  // some payment if: positive paid OR “partial” in status OR doc_says_paid flag\n  let hasSomePayment =\n    (Number.isFinite(paidAmtIn) && paidAmtIn > tolAbs) ||\n    /partial/i.test(statusStr) ||\n    src.doc_says_paid === true;\n\n  // full payment primarily by amounts; labels only if amounts missing\n  let isFullPayment =\n    (Number.isFinite(dueIn)   && dueIn <= tolAbs) ||\n    (Number.isFinite(totalIn) && Number.isFinite(paidAmtIn) && (paidAmtIn >= totalIn - tolAbs));\n\n  // if due is positive, it cannot be full—even if the doc says “PAID”\n  if (Number.isFinite(dueIn) && dueIn > tolAbs) isFullPayment = false;\n\n  // keep compatibility names used later in logic\n  const incomingPaid    = hasSomePayment;\n  const partialDetected = hasSomePayment && !isFullPayment;\n\n  // ---------- decide route ----------\n  let next_action = 'archive';\n  let reason = '';\n\n  if (!page) {\n    // No DB page found → choose create_* route\n    if (hasSomePayment) {\n      if (isFullPayment) {\n        next_action = 'create_paid';\n        reason = 'no_db_match_paid';\n      } else {\n        next_action = ENABLE_PARTIAL ? 'create_partial' : 'manual_review';\n        reason = ENABLE_PARTIAL ? 'no_db_match_partial_payment' : 'partial_needs_review';\n      }\n    } else {\n      next_action = 'create_unpaid';\n      reason = 'no_db_match_unpaid';\n    }\n  } else {\n    // Have a DB page\n    if (manyMatches || vendorMismatch || currencyMismatch || fallbackAmbiguous) {\n      next_action = 'manual_review';\n      reason = manyMatches\n        ? 'multiple_db_matches'\n        : vendorMismatch\n          ? 'vendor_mismatch'\n          : currencyMismatch\n            ? 'currency_mismatch'\n            : 'fallback_ambiguous';\n    } else if (hasSomePayment) {\n      // There is some payment in the incoming doc\n      if (isFullPayment) {\n        // mark paid if DB not yet paid; else archive duplicate payment\n        if (!dbPaid) {\n          next_action = 'update_mark_paid';\n          reason = 'mark_paid';\n        } else {\n          next_action = 'archive';\n          reason = 'duplicate_payment_already_paid';\n        }\n      } else {\n        // partial payment path\n        if (ENABLE_PARTIAL) {\n          next_action = 'update_partial';\n          reason = 'partial_payment_detected';\n        } else {\n          next_action = 'manual_review';\n          reason = 'partial_needs_review';\n        }\n      }\n    } else if (amountMismatch) {\n      next_action = 'manual_review';\n      reason = 'amount_mismatch';\n    } else {\n      // no payment transition\n      const same = coreSame(src, db);\n      next_action = 'archive';\n      reason = same ? 'duplicate_no_change' : 'no_payment_transition';\n    }\n  }\n\n  out.push({\n    json: {\n      ...src,\n      db_found:  !!page,\n      db_page_id: db?.page_id || '',\n      db_status:  db?.status   || '',\n      next_action,\n      reason,\n      _checks: {\n        manyMatches,\n        vendorMismatch,\n        currencyMismatch,\n        amountMismatch,\n        fallbackAmbiguous,\n        hasSomePayment,\n        isFullPayment,\n        dbPaid\n      }\n    }\n  });\n});\n\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "1e7efdbb-f58c-4cfa-9a74-22cda80b58c7",
      "name": "发送至源文件发票1",
      "type": "n8n-nodes-base.notion",
      "position": [
        -1872,
        736
      ],
      "parameters": {
        "title": "={{ $json.source_file_id }}",
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "238f7557-9573-8062-b50d-fe9db34ec410",
          "cachedResultUrl": "https://www.notion.so/238f755795738062b50dfe9db34ec410",
          "cachedResultName": "Source File"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "File ID|title",
              "title": "={{ $json.source_file_id }}"
            },
            {
              "key": "Filename|rich_text",
              "textContent": "={{ $json.source_file_name }}"
            },
            {
              "key": "File URL|url",
              "urlValue": "={{ $json.source_file_url }}"
            },
            {
              "key": "Summary|rich_text",
              "text": {
                "text": [
                  {
                    "text": "={{ $json.line_items_rich_text[0].text.content }}",
                    "annotationUi": {}
                  }
                ]
              },
              "richText": true
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "3b8e7c65-8d40-4636-bfb0-8091365e584e",
      "name": "发送前准备行项目",
      "type": "n8n-nodes-base.code",
      "position": [
        -2320,
        1312
      ],
      "parameters": {
        "jsCode": "// n8n Code node — prepare line items + compute ONE field for Notion (\"Paid Amount\")\nconst out = [];\n\nfunction two(n){ return Math.round((Number(n)||0)*100)/100; }\nfunction chunk(str, size=1800){ const a=[]; for(let i=0;i<str.length;i+=size)a.push(str.slice(i,i+size)); return a; }\nfunction num(x){ if(x===null||x===undefined||x===\"\") return NaN; const n=Number(String(x).replace(/[^0-9.-]/g,\"\")); return isNaN(n)?NaN:n; }\nfunction clamp(n,min,max){ return Math.max(min, Math.min(max, n)); }\nfunction approxEq(a,b,eps=0.01){ return Math.abs(a-b)<=eps; }\n\nfor (const item of $input.all()) {\n  const j = item.json || {};\n  const ccy = (j.currency || '').toUpperCase();\n  const items = Array.isArray(j.line_items) ? j.line_items : [];\n\n  // Build line-items text (unchanged)\n  const lines = items.map(li => {\n    const desc=(li?.description||'').trim();\n    const qty = two(li?.qty);\n    const up  = two(li?.unit_price);\n    const amt = two(li?.amount || qty*up);\n    const tail = ccy ? ` ${ccy}` : '';\n    return `${qty} × ${desc} @ ${up}${tail} = ${amt}${tail}`;\n  });\n  const text = lines.join('\\n') || '';\n  const rich = chunk(text).map(s => ({ type:'text', text:{ content:s }}));\n\n  // Minimal compute for Paid Amount\n  const total = num(j.amount_total);\n  const due   = num(j.amount_due);\n  const delta = num(j?.receipt_hints?.paid_amount ?? j.paid_amount);       // new receipt (e.g., 200)\n  const prev  = num(j.db_paid_amount_prev ?? j.invoice?.paid_amount ?? 0); // previous paid if you pass it\n\n  // Prefer set-to from doc (Total - Due); fallback to prev + delta\n  let newPaid = Number.isFinite(total) && Number.isFinite(due)\n    ? two(clamp(total - due, 0, total))\n    : (Number.isFinite(delta) ? two(clamp(prev + delta, 0, Number.isFinite(total)? total : Infinity)) : NaN);\n\n  // If still NaN, don't emit the field to avoid bad writes\n  const outJson = {\n    ...j,\n    line_items_text: text,\n    line_items_rich_text: rich,\n  };\n  if (Number.isFinite(newPaid)) outJson.notion_paid_amount = newPaid;\n\n  // (Optional tiny extra: status, only if you want it — safe to ignore)\n  if (Number.isFinite(total) && Number.isFinite(newPaid)) {\n    outJson.notion_status =\n      approxEq(newPaid, 0)      ? \"Unpaid\" :\n      approxEq(newPaid, total)  ? \"Paid (Verified)\" :\n                                  \"Partially Paid\";\n  }\n\n  out.push({ json: outJson });\n}\n\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "fa73399a-c133-48b3-87fe-922961f4d448",
      "name": "决定去向",
      "type": "n8n-nodes-base.switch",
      "position": [
        -2096,
        1232
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Create Unpaid",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "18f4cc4b-ad0f-4782-a59d-a66dbffc6475",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.next_action }}",
                    "rightValue": "create_unpaid"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Create Paid",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "d8bb0418-0798-428a-b379-8cf8accf5ea9",
                    "operator": {
                      "name": "filter.operator.equals",
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.next_action }}",
                    "rightValue": "create_paid"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Update to Paid",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "4009916c-000e-435d-ada3-6a7d1fbc595c",
                    "operator": {
                      "name": "filter.operator.equals",
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.next_action }}",
                    "rightValue": "update_mark_paid"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Archive",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "172dad2a-7a92-493d-977d-56c568d5c360",
                    "operator": {
                      "name": "filter.operator.equals",
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.next_action }}",
                    "rightValue": "archive"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "New Create Partial",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "a14d6d5c-6eac-49e8-8380-f85cf565a542",
                    "operator": {
                      "name": "filter.operator.equals",
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.next_action }}",
                    "rightValue": "create_partial"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Update Partial Payment",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "b9d0f364-e7a4-48b8-94fc-6053b51f6e14",
                    "operator": {
                      "name": "filter.operator.equals",
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.next_action }}",
                    "rightValue": "update_partial"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Manual Review",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "4d8986d1-3896-42bf-9d6f-9adb48ac3987",
                    "operator": {
                      "name": "filter.operator.equals",
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.next_action }}",
                    "rightValue": "manual_review"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.2
    },
    {
      "id": "f90f09ea-6016-429b-915f-1565bcff1d97",
      "name": "创建新未付发票",
      "type": "n8n-nodes-base.notion",
      "position": [
        -1648,
        736
      ],
      "parameters": {
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "24df7557-9573-8073-9611-da8371404254",
          "cachedResultUrl": "https://www.notion.so/24df7557957380739611da8371404254",
          "cachedResultName": "Clearflow Invoice (Ledger)"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Attachments|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.attachments[0] }}"
            },
            {
              "key": "Currency|select",
              "selectValue": "={{ $('Decide Fate').item.json.currency }}"
            },
            {
              "key": "Destination Account|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.destination_account }}"
            },
            {
              "key": "Discount Percent|number",
              "numberValue": "={{ $('Decide Fate').item.json.discount_percent_for_notion }}"
            },
            {
              "key": "Due Date|date",
              "date": "={{ $('Decide Fate').item.json.due_date }}"
            },
            {
              "key": "Invoice No|title",
              "title": "={{ $('Decide Fate').item.json.invoice_no }}"
            },
            {
              "key": "Issue Date|date",
              "date": "={{ $('Decide Fate').item.json.issue_date }}"
            },
            {
              "key": "Line Items|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.line_items_rich_text[0].text.content }}"
            },
            {
              "key": "Notes|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.notes }}"
            },
            {
              "key": "Paid Amount|number",
              "numberValue": "={{ $('Decide Fate').item.json.paid_amount }}"
            },
            {
              "key": "Payment Ref|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.payment_ref }}"
            },
            {
              "key": "Recipient Account|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.destination_account }}"
            },
            {
              "key": "Status|select",
              "selectValue": "={{ $('Decide Fate').item.json.status }}"
            },
            {
              "key": "Subtotal|number",
              "numberValue": "={{ $('Decide Fate').item.json.subtotal }}"
            },
            {
              "key": "Tax Total|number",
              "numberValue": "={{ $('Decide Fate').item.json.tax_total }}"
            },
            {
              "key": "Vendor|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.vendor }}"
            },
            {
              "key": "=Source File|relation",
              "relationValue": [
                "={{ $json.id }}"
              ]
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "8f25494a-6b7c-45e5-99fc-7ab5e2e0d085",
      "name": "将收据添加到现金流",
      "type": "n8n-nodes-base.notion",
      "position": [
        -1872,
        1312
      ],
      "parameters": {
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "238f7557-9573-80a2-96c8-e638a56411d2",
          "cachedResultUrl": "https://www.notion.so/238f7557957380a296c8e638a56411d2",
          "cachedResultName": "Cashflow (Ledger)"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Cashflow ID|title",
              "title": "={{ $json.receipt_no || 'cf_' + Date.now() + '_' + Math.floor(Math.random() * 1000) }}"
            },
            {
              "key": "Amount (Net)|number",
              "numberValue": "={{ $json.paid_amount }}"
            },
            {
              "key": "Currency|select",
              "selectValue": "={{ $json.currency }}"
            },
            {
              "key": "Fee|number",
              "numberValue": "={{ $json.tax_total }}"
            },
            {
              "key": "Gross Amount|number",
              "numberValue": "={{ $json.paid_amount }}"
            },
            {
              "key": "Notes|rich_text",
              "textContent": "={{ $json.notes }}"
            },
            {
              "key": "Timestamp|date",
              "date": "={{ $json.paid_date }}"
            },
            {
              "key": "Timestamp (ISO)|rich_text",
              "textContent": "={{ $json.paid_date }}"
            },
            {
              "key": "Type|select",
              "selectValue": "=Expense"
            },
            {
              "key": "Vendor / Recipient|rich_text",
              "textContent": "={{ $json.vendor }}"
            },
            {
              "key": "Vendor Ref ID|rich_text",
              "textContent": "={{ $json.invoice_no }}"
            },
            {
              "key": "Source File|relation",
              "relationValue": [
                "={{ $json.db_page_id }}"
              ]
            },
            {
              "key": "Recipient Account|rich_text",
              "textContent": "={{ $json.vendor }}"
            },
            {
              "key": "Source Account|rich_text",
              "textContent": "={{ $json.payment_method }}"
            },
            {
              "key": "Notes|rich_text",
              "textContent": "={{ $json.line_items_rich_text[0].text.content }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "35db92f5-faf6-4ecd-a148-7bbf09bf620b",
      "name": "更新发票为全额支付",
      "type": "n8n-nodes-base.notion",
      "position": [
        -1648,
        1312
      ],
      "parameters": {
        "pageId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Prepare Line Items before send').item.json.db_page_id }}"
        },
        "options": {},
        "resource": "databasePage",
        "operation": "update",
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Receipt|relation",
              "relationValue": [
                "={{ $json.id }}"
              ]
            },
            {
              "key": "Paid Amount|number",
              "numberValue": "={{ $('Prepare Line Items before send').item.json.paid_amount }}"
            },
            {
              "key": "Status|select",
              "selectValue": "Paid"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "5487835c-5655-476b-bbad-f368653150d1",
      "name": "发送至源文件发票2",
      "type": "n8n-nodes-base.notion",
      "position": [
        -1872,
        928
      ],
      "parameters": {
        "title": "={{ $json.source_file_id }}",
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "238f7557-9573-8062-b50d-fe9db34ec410",
          "cachedResultUrl": "https://www.notion.so/238f755795738062b50dfe9db34ec410",
          "cachedResultName": "Source File"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "File ID|title",
              "title": "={{ $json.source_file_id }}"
            },
            {
              "key": "Filename|rich_text",
              "textContent": "={{ $json.source_file_name }}"
            },
            {
              "key": "File URL|url",
              "urlValue": "={{ $json.source_file_url }}"
            },
            {
              "key": "Summary|rich_text",
              "text": {
                "text": [
                  {
                    "text": "={{ $json.line_items_rich_text[0].text.content }}",
                    "annotationUi": {}
                  }
                ]
              },
              "richText": true
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "a09de3d7-f79a-4027-ab49-a4e2376d3f82",
      "name": "将收据添加到已支付现金流1",
      "type": "n8n-nodes-base.notion",
      "position": [
        -1648,
        928
      ],
      "parameters": {
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "238f7557-9573-80a2-96c8-e638a56411d2",
          "cachedResultUrl": "https://www.notion.so/238f7557957380a296c8e638a56411d2",
          "cachedResultName": "Cashflow (Ledger)"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Cashflow ID|title",
              "title": "={{ $('Prepare Line Items before send').item.json.receipt_no || 'cf_' + Date.now() + '_' + Math.floor(Math.random() * 1000) }}"
            },
            {
              "key": "Amount (Net)|number",
              "numberValue": "={{ $('Prepare Line Items before send').item.json.paid_amount }}"
            },
            {
              "key": "Currency|select",
              "selectValue": "={{ $('Prepare Line Items before send').item.json.currency }}"
            },
            {
              "key": "Fee|number",
              "numberValue": "={{ $('Prepare Line Items before send').item.json.tax_total }}"
            },
            {
              "key": "Gross Amount|number",
              "numberValue": "={{ $('Prepare Line Items before send').item.json.paid_amount }}"
            },
            {
              "key": "Notes|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.notes }}"
            },
            {
              "key": "Timestamp|date",
              "date": "={{ $('Prepare Line Items before send').item.json.paid_date }}"
            },
            {
              "key": "Timestamp (ISO)|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.paid_date }}"
            },
            {
              "key": "Type|select",
              "selectValue": "=Expense"
            },
            {
              "key": "Vendor / Recipient|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.vendor }}"
            },
            {
              "key": "Vendor Ref ID|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.receipt_no }}"
            },
            {
              "key": "Source File|relation",
              "relationValue": [
                "={{ $json.id }}"
              ]
            },
            {
              "key": "Recipient Account|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.vendor }}"
            },
            {
              "key": "Source Account|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.payment_method }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "54b5f427-4c28-4b83-a04d-40bc14df309d",
      "name": "创建新全额支付发票",
      "type": "n8n-nodes-base.notion",
      "position": [
        -1424,
        928
      ],
      "parameters": {
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "24df7557-9573-8073-9611-da8371404254",
          "cachedResultUrl": "https://www.notion.so/24df7557957380739611da8371404254",
          "cachedResultName": "Clearflow Invoice (Ledger)"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Attachments|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.attachments[0] }}"
            },
            {
              "key": "Currency|select",
              "selectValue": "={{ $('Decide Fate').item.json.currency }}"
            },
            {
              "key": "Destination Account|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.destination_account }}"
            },
            {
              "key": "Discount Percent|number",
              "numberValue": "={{ $('Decide Fate').item.json.discount_percent_for_notion }}"
            },
            {
              "key": "Due Date|date",
              "date": "={{ $('Decide Fate').item.json.due_date }}"
            },
            {
              "key": "Invoice No|title",
              "title": "={{ $('Decide Fate').item.json.invoice_no }}"
            },
            {
              "key": "Issue Date|date",
              "date": "={{ $('Decide Fate').item.json.issue_date }}"
            },
            {
              "key": "Line Items|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.line_items_rich_text[0].text.content }}"
            },
            {
              "key": "Notes|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.notes }}"
            },
            {
              "key": "Paid Amount|number",
              "numberValue": "={{ $('Decide Fate').item.json.paid_amount }}"
            },
            {
              "key": "Payment Ref|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.payment_ref }}"
            },
            {
              "key": "Recipient Account|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.destination_account }}"
            },
            {
              "key": "Status|select",
              "selectValue": "={{ $('Decide Fate').item.json.status }}"
            },
            {
              "key": "Subtotal|number",
              "numberValue": "={{ $('Decide Fate').item.json.subtotal }}"
            },
            {
              "key": "Tax Total|number",
              "numberValue": "={{ $('Decide Fate').item.json.tax_total }}"
            },
            {
              "key": "Vendor|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.vendor }}"
            },
            {
              "key": "=Source File|relation",
              "relationValue": [
                "={{ $('Send to Source File Invoice 2').item.json.id }}"
              ]
            },
            {
              "key": "Source Account|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.payment_method }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "48bc9ab1-03bb-40a6-844d-a4fff203fcd5",
      "name": "发送至归档源文件发票",
      "type": "n8n-nodes-base.notion",
      "position": [
        -1872,
        1120
      ],
      "parameters": {
        "title": "={{ $json.source_file_id }}",
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "239f7557-9573-80d6-a8fb-fc710833e5db",
          "cachedResultUrl": "https://www.notion.so/239f7557957380d6a8fbfc710833e5db",
          "cachedResultName": "Archived Source File (Duplicate)"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "File ID|title",
              "title": "={{ $json.source_file_id }}"
            },
            {
              "key": "Filename|rich_text",
              "textContent": "={{ $json.source_file_name }}"
            },
            {
              "key": "FILE URL|url",
              "urlValue": "={{ $json.source_file_url }}"
            },
            {
              "key": "Summary|rich_text",
              "text": {
                "text": [
                  {
                    "text": "={{ $json.line_items_rich_text[0].text.content }}",
                    "annotationUi": {}
                  }
                ]
              },
              "richText": true
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "d165164c-bcf8-4ef3-b434-9416865c6b26",
      "name": "归档发票",
      "type": "n8n-nodes-base.notion",
      "position": [
        -1648,
        1120
      ],
      "parameters": {
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "254f7557-9573-8012-b9b3-e7811bfac12c",
          "cachedResultUrl": "https://www.notion.so/254f755795738012b9b3e7811bfac12c",
          "cachedResultName": "Archived Clearflow Invoice (Ledger)"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Attachments|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.attachments[0] }}"
            },
            {
              "key": "Currency|select",
              "selectValue": "={{ $('Decide Fate').item.json.currency }}"
            },
            {
              "key": "Destination Account|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.destination_account }}"
            },
            {
              "key": "Discount Percent|number",
              "numberValue": "={{ $('Decide Fate').item.json.discount_percent_for_notion }}"
            },
            {
              "key": "Due Date|date",
              "date": "={{ $('Decide Fate').item.json.due_date }}"
            },
            {
              "key": "Invoice No|title",
              "title": "={{ $('Decide Fate').item.json.invoice_no }}"
            },
            {
              "key": "Issue Date|date",
              "date": "={{ $('Decide Fate').item.json.issue_date }}"
            },
            {
              "key": "Line Items|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.line_items_rich_text[0].text.content }}"
            },
            {
              "key": "Notes|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.notes }}"
            },
            {
              "key": "Paid Amount|number",
              "numberValue": "={{ $('Decide Fate').item.json.paid_amount }}"
            },
            {
              "key": "Payment Ref|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.payment_ref }}"
            },
            {
              "key": "Recipient Account|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.destination_account }}"
            },
            {
              "key": "Status|select",
              "selectValue": "={{ $('Decide Fate').item.json.status }}"
            },
            {
              "key": "Subtotal|number",
              "numberValue": "={{ $('Decide Fate').item.json.subtotal }}"
            },
            {
              "key": "Tax Total|number",
              "numberValue": "={{ $('Decide Fate').item.json.tax_total }}"
            },
            {
              "key": "Vendor|rich_text",
              "textContent": "={{ $('Decide Fate').item.json.vendor }}"
            },
            {
              "key": "=Source File|relation",
              "relationValue": [
                "={{ $json.id }}"
              ]
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "4157135a-0645-4f12-9315-c6b2299520b6",
      "name": "等待",
      "type": "n8n-nodes-base.wait",
      "position": [
        -1600,
        2544
      ],
      "webhookId": "9d0e00b6-5ee8-45c9-8597-34db73f701ae",
      "parameters": {
        "resume": "webhook",
        "options": {},
        "resumeUnit": "days",
        "resumeAmount": 2,
        "limitWaitTime": true
      },
      "typeVersion": 1.1
    },
    {
      "id": "da9cdafe-01c8-49c8-afaa-29cb46f67aef",
      "name": "发送消息",
      "type": "n8n-nodes-base.slack",
      "position": [
        -1824,
        2544
      ],
      "webhookId": "2df2d5da-990f-4499-9131-0bbf3595e9bc",
      "parameters": {
        "text": "Test Text",
        "select": "channel",
        "blocksUi": "={\n\t\"blocks\": [\n\t\t{\n\t\t\t\"type\": \"section\",\n\t\t\t\"text\": {\n\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\"text\": \"You have a new invoice to review:\\n*{{ $('Prepare Line Items before send').item.json.invoice_no || '—' }}*\"\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"type\": \"section\",\n\t\t\t\"fields\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Type:*\\nInvoice\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*When:*\\n{{ $('Prepare Line Items before send').item.json.issue_date || '—' }} → {{ $('Prepare Line Items before send').item.json.due_date || '—' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Vendor:*\\n{{ $('Prepare Line Items before send').item.json.vendor || '—' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Currency:*\\n{{ $('Prepare Line Items before send').item.json.currency || '—' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Invoice No:*\\n{{ $('Prepare Line Items before send').item.json.invoice_no || '—' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Status (detected):*\\n{{ $('Prepare Line Items before send').item.json.status || '—' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Amount Total:*\\n{{ $('Prepare Line Items before send').item.json.amount_total || 0 }} {{ $('Prepare Line Items before send').item.json.currency || '' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Paid / Due:*\\n{{ $('Prepare Line Items before send').item.json.paid_amount || 0 }} / {{ $('Prepare Line Items before send').item.json.amount_due || 0 }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Payment Method:*\\n{{ $('Prepare Line Items before send').item.json.payment_method || '—' }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\t\"text\": \"*Reason:*\\n{{ $('Prepare Line Items before send').item.json.reason || '—' }}\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"type\": \"image\",\n\t\t\t\"title\": {\n\t\t\t\t\"type\": \"plain_text\",\n\t\t\t\t\"text\": \"image1\",\n\t\t\t\t\"emoji\": true\n\t\t\t},\n\t\t\t\"image_url\": \"{{ $json.source_file_url }}\",\n\t\t\t\"alt_text\": \"image1\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"divider\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"actions\",\n\t\t\t\"block_id\": \"review_actions\",\n\t\t\t\"elements\": [\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"button\",\n\t\t\t\t\t\"action_id\": \"approve\",\n\t\t\t\t\t\"text\": {\n\t\t\t\t\t\t\"type\": \"plain_text\",\n\t\t\t\t\t\t\"text\": \"✅ Approve Paid\"\n\t\t\t\t\t},\n\t\t\t\t\t\"style\": \"primary\",\n\t\t\t\t\t\"url\": \"{{ $execution.resumeUrl }}?choice=approve&inv={{ encodeURIComponent($('Prepare Line Items before send').item.json.invoice_no || '') }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"button\",\n\t\t\t\t\t\"action_id\": \"unpaid\",\n\t\t\t\t\t\"text\": {\n\t\t\t\t\t\t\"type\": \"plain_text\",\n\t\t\t\t\t\t\"text\": \"📝 Approve Unpaid\"\n\t\t\t\t\t},\n\t\t\t\t\t\"style\": \"danger\",\n\t\t\t\t\t\"url\": \"{{ $execution.resumeUrl }}?choice=unpaid&inv={{ encodeURIComponent($('Prepare Line Items before send').item.json.invoice_no || '') }}\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"type\": \"button\",\n\t\t\t\t\t\"action_id\": \"archive\",\n\t\t\t\t\t\"text\": {\n\t\t\t\t\t\t\"type\": \"plain_text\",\n\t\t\t\t\t\t\"text\": \"❌ Archive\"\n\t\t\t\t\t},\n\t\t\t\t\t\"url\": \"{{ $execution.resumeUrl }}?choice=archive&inv={{ encodeURIComponent($('Prepare Line Items before send').item.json.invoice_no || '') }}\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C09B5P66Q4A",
          "cachedResultName": "required-invoice-review"
        },
        "messageType": "block",
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "id": "muT22qcO4YUdHAGU",
          "name": "Slack account"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "e2179126-0881-4701-9d4a-62d335a74abd",
      "name": "发送至归档源文件重复发票",
      "type": "n8n-nodes-base.notion",
      "position": [
        -2768,
        2032
      ],
      "parameters": {
        "title": "={{ $json.source_file_id }}",
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "239f7557-9573-80d6-a8fb-fc710833e5db",
          "cachedResultUrl": "https://www.notion.so/239f7557957380d6a8fbfc710833e5db",
          "cachedResultName": "Archived Source File (Duplicate)"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "File ID|title",
              "title": "={{ $json.source_file_id }}"
            },
            {
              "key": "Filename|rich_text",
              "textContent": "={{ $json.source_file_name }}"
            },
            {
              "key": "FILE URL|url",
              "urlValue": "={{ $json.source_file_url }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "cca27d76-da70-42fc-b6b2-5c5330490d2b",
      "name": "归档重复发票",
      "type": "n8n-nodes-base.notion",
      "position": [
        -2544,
        2032
      ],
      "parameters": {
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "254f7557-9573-8012-b9b3-e7811bfac12c",
          "cachedResultUrl": "https://www.notion.so/254f755795738012b9b3e7811bfac12c",
          "cachedResultName": "Archived Clearflow Invoice (Ledger)"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Attachments|rich_text",
              "textContent": "={{ $('Prepare Archive Duplicate Items').item.json.attachments[0] }}"
            },
            {
              "key": "Currency|select",
              "selectValue": "={{ $('Prepare Archive Duplicate Items').item.json.currency }}"
            },
            {
              "key": "Destination Account|rich_text",
              "textContent": "={{ $('Prepare Archive Duplicate Items').item.json.destination_account }}"
            },
            {
              "key": "Discount Percent|number",
              "numberValue": "={{ $('Prepare Archive Duplicate Items').item.json.discount_percent_for_notion }}"
            },
            {
              "key": "Due Date|date",
              "date": "={{ $('Prepare Archive Duplicate Items').item.json.due_date }}"
            },
            {
              "key": "Invoice No|title",
              "title": "={{ $('Prepare Archive Duplicate Items').item.json.invoice_no }}"
            },
            {
              "key": "Issue Date|date",
              "date": "={{ $('Prepare Archive Duplicate Items').item.json.issue_date }}"
            },
            {
              "key": "Line Items|rich_text",
              "textContent": "={{ $('Prepare Archive Duplicate Items').item.json.line_items_text }}"
            },
            {
              "key": "Notes|rich_text",
              "textContent": "={{ $('Prepare Archive Duplicate Items').item.json.notes }}"
            },
            {
              "key": "Paid Amount|number",
              "numberValue": "={{ $('Prepare Archive Duplicate Items').item.json.paid_amount }}"
            },
            {
              "key": "Payment Ref|rich_text",
              "textContent": "={{ $('Prepare Archive Duplicate Items').item.json.payment_ref }}"
            },
            {
              "key": "Recipient Account|rich_text",
              "textContent": "={{ $('Prepare Archive Duplicate Items').item.json.destination_account }}"
            },
            {
              "key": "Status|select",
              "selectValue": "={{ $('Prepare Archive Duplicate Items').item.json.status }}"
            },
            {
              "key": "Subtotal|number",
              "numberValue": "={{ $('Prepare Archive Duplicate Items').item.json.subtotal }}"
            },
            {
              "key": "Tax Total|number",
              "numberValue": "={{ $('Prepare Archive Duplicate Items').item.json.tax_total }}"
            },
            {
              "key": "Vendor|rich_text",
              "textContent": "={{ $('Prepare Archive Duplicate Items').item.json.vendor }}"
            },
            {
              "key": "=Source File|relation",
              "relationValue": [
                "={{ $json.id }}"
              ]
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "e4d849f2-4c10-4e93-9133-b78e861687f2",
      "name": "准备归档重复项目",
      "type": "n8n-nodes-base.code",
      "position": [
        -2992,
        2032
      ],
      "parameters": {
        "jsCode": "// Build \"summary items\" text from line_items (one output per input)\n\nfunction toNum(v) {\n  const n = Number(v);\n  return Number.isFinite(n) ? n : 0;\n}\nfunction clean(s) {\n  return String(s ?? \"\").trim();\n}\nfunction fmt(n) {\n  // 2dp but strip trailing .00 if integer\n  const f = (Math.round(n * 100) / 100).toFixed(2);\n  return f.endsWith(\".00\") ? String(Math.round(n)) : f;\n}\n\nconst out = [];\n\nfor (const { json: rec } of $input.all()) {\n  const currency = clean(rec.currency).toUpperCase(); // e.g., \"USD\"\n  const lis = Array.isArray(rec.line_items) ? rec.line_items : [];\n\n  // Group by description + unit_price to collapse duplicates\n  const groups = new Map();\n  for (const li of lis) {\n    const desc = clean(li?.description) || \"Item\";\n    const unit = toNum(li?.unit_price);\n    const qty  = toNum(li?.qty);\n    const amt  = toNum(li?.amount) || qty * unit;\n\n    const key = `${desc}||${unit}`;\n    if (!groups.has(key)) groups.set(key, { desc, unit, qty: 0, amount: 0 });\n    const g = groups.get(key);\n    g.qty += qty;\n    g.amount += amt;\n  }\n\n  // Render lines\n  const lines = [];\n  const rich = [];\n  for (const g of groups.values()) {\n    const qtyS  = fmt(g.qty);\n    const unitS = fmt(g.unit);\n    const amtS  = fmt(g.amount);\n    const curS  = currency ? ` ${currency}` : \"\";\n    const line  = `${qtyS} × ${g.desc} @ ${unitS}${curS} = ${amtS}${curS}`;\n    lines.push(line);\n    rich.push({ type: \"text\", text: { content: line } });\n  }\n\n  const summary = lines.join(\"\\n\");\n\n  out.push({\n    json: {\n      ...rec,\n      summary_items: summary,              // <-- your \"Summary Items\" field\n      line_items_text: summary,            // keep old name if you need it\n      line_items_rich_text: rich           // Notion rich_text-friendly\n    }\n  });\n}\n\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "237841d5-2268-4efe-9ae2-25e904dbd48a",
      "name": "发送至源文件发票3",
      "type": "n8n-nodes-base.notion",
      "position": [
        -704,
        2288
      ],
      "parameters": {
        "title": "={{ $json.source_file_id }}",
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "238f7557-9573-8062-b50d-fe9db34ec410",
          "cachedResultUrl": "https://www.notion.so/238f755795738062b50dfe9db34ec410",
          "cachedResultName": "Source File"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "File ID|title",
              "title": "={{ $json.source_file_id }}"
            },
            {
              "key": "Filename|rich_text",
              "textContent": "={{ $json.source_file_name }}"
            },
            {
              "key": "File URL|url",
              "urlValue": "={{ $json.source_file_url }}"
            },
            {
              "key": "Summary|rich_text",
              "text": {
                "text": [
                  {
                    "text": "={{ $json.line_items_rich_text[0].text.content }}",
                    "annotationUi": {}
                  }
                ]
              },
              "richText": true
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "010957b9-c7a4-4432-ac02-6ea1400f0418",
      "name": "将收据添加到已支付现金流2",
      "type": "n8n-nodes-base.notion",
      "position": [
        -480,
        2288
      ],
      "parameters": {
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "238f7557-9573-80a2-96c8-e638a56411d2",
          "cachedResultUrl": "https://www.notion.so/238f7557957380a296c8e638a56411d2",
          "cachedResultName": "Cashflow (Ledger)"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Cashflow ID|title",
              "title": "={{ $('Take Invoice Details 1').item.json.receipt_no || 'cf_' + Date.now() + '_' + Math.floor(Math.random() * 1000) }}"
            },
            {
              "key": "Amount (Net)|number",
              "numberValue": "={{ $('Take Invoice Details 1').item.json.paid_amount }}"
            },
            {
              "key": "Currency|select",
              "selectValue": "={{ $('Take Invoice Details 1').item.json.currency }}"
            },
            {
              "key": "Fee|number",
              "numberValue": "={{ $('Take Invoice Details 1').item.json.tax_total }}"
            },
            {
              "key": "Gross Amount|number",
              "numberValue": "={{ $('Take Invoice Details 1').item.json.paid_amount }}"
            },
            {
              "key": "Notes|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.notes }}"
            },
            {
              "key": "Timestamp|date",
              "date": "={{ $('Take Invoice Details 1').item.json.paid_date }}"
            },
            {
              "key": "Timestamp (ISO)|rich_text",
              "textContent": "={{ $('Take Invoice Details 1').item.json.paid_date }}"
            },
            {
              "key": "Type|select",
              "selectValue": "=Expense"
            },
            {
              "key": "Vendor / Recipient|rich_text",
              "textContent": "={{ $('Take Invoice Details 1').item.json.vendor }}"
            },
            {
              "key": "Vendor Ref ID|rich_text",
              "textContent": "={{ $('Take Invoice Details 1').item.json.receipt_no }}"
            },
            {
              "key": "Source File|relation",
              "relationValue": [
                "={{ $json.id }}"
              ]
            },
            {
              "key": "Recipient Account|rich_text",
              "textContent": "={{ $('Take Invoice Details 1').item.json.vendor }}"
            },
            {
              "key": "Source Account|rich_text",
              "textContent": "={{ $('Take Invoice Details 1').item.json.payment_method }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "5b13d0d1-28a9-47df-82a2-5b83d338c413",
      "name": "审批检查",
      "type": "n8n-nodes-base.switch",
      "position": [
        -1376,
        2528
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Paid",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "cf7a1e2d-9984-4052-9c1f-691f7a0ea50c",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.query.choice }}",
                    "rightValue": "approve"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Unpaid",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "78953ded-6256-4f3a-86c5-1189b02b20e7",
                    "operator": {
                      "name": "filter.operator.equals",
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.query.choice }}",
                    "rightValue": "unpaid"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Archive",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "3f329867-45ba-4287-b5f7-632c52567c79",
                    "operator": {
                      "name": "filter.operator.equals",
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.query.choice }}",
                    "rightValue": "archive"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.2
    },
    {
      "id": "0163f077-3850-4609-9def-1483cd5f720b",
      "name": "将收据添加到现金流1",
      "type": "n8n-nodes-base.notion",
      "position": [
        -704,
        2096
      ],
      "parameters": {
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "238f7557-9573-80a2-96c8-e638a56411d2",
          "cachedResultUrl": "https://www.notion.so/238f7557957380a296c8e638a56411d2",
          "cachedResultName": "Cashflow (Ledger)"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Cashflow ID|title",
              "title": "={{ $json.receipt_no || 'cf_' + Date.now() + '_' + Math.floor(Math.random() * 1000) }}"
            },
            {
              "key": "Amount (Net)|number",
              "numberValue": "={{ $json.paid_amount }}"
            },
            {
              "key": "Currency|select",
              "selectValue": "={{ $json.currency }}"
            },
            {
              "key": "Fee|number",
              "numberValue": "={{ $json.tax_total }}"
            },
            {
              "key": "Gross Amount|number",
              "numberValue": "={{ $json.paid_amount }}"
            },
            {
              "key": "Notes|rich_text",
              "textContent": "={{ $json.notes }}"
            },
            {
              "key": "Timestamp|date",
              "date": "={{ $json.paid_date }}"
            },
            {
              "key": "Timestamp (ISO)|rich_text",
              "textContent": "={{ $json.paid_date }}"
            },
            {
              "key": "Type|select",
              "selectValue": "=Expense"
            },
            {
              "key": "Vendor / Recipient|rich_text",
              "textContent": "={{ $json.vendor }}"
            },
            {
              "key": "Vendor Ref ID|rich_text",
              "textContent": "={{ $json.invoice_no }}"
            },
            {
              "key": "Source File|relation",
              "relationValue": [
                "={{ $json.db_page_id }}"
              ]
            },
            {
              "key": "Recipient Account|rich_text",
              "textContent": "={{ $json.vendor }}"
            },
            {
              "key": "Source Account|rich_text",
              "textContent": "={{ $json.payment_method }}"
            },
            {
              "key": "Notes|rich_text",
              "textContent": "={{ $json.line_items_rich_text[0].text.content }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "557a172b-f58a-4f82-a8b8-ddc3865bbe20",
      "name": "更新发票为全额支付1",
      "type": "n8n-nodes-base.notion",
      "position": [
        -480,
        2096
      ],
      "parameters": {
        "pageId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Take Invoice Details 1').item.json.db_page_id }}"
        },
        "options": {},
        "resource": "databasePage",
        "operation": "update",
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Receipt|relation",
              "relationValue": [
                "={{ $json.id }}"
              ]
            },
            {
              "key": "Paid Amount|number",
              "numberValue": "={{ $('Take Invoice Details 1').item.json.paid_amount }}"
            },
            {
              "key": "Status|select",
              "selectValue": "Paid"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "5e962f4f-f35a-46f6-a80f-bdd66553c3ed",
      "name": "创建新支付发票3",
      "type": "n8n-nodes-base.notion",
      "position": [
        -256,
        2288
      ],
      "parameters": {
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "24df7557-9573-8073-9611-da8371404254",
          "cachedResultUrl": "https://www.notion.so/24df7557957380739611da8371404254",
          "cachedResultName": "Clearflow Invoice (Ledger)"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Attachments|rich_text",
              "textContent": "={{ $('Take Invoice Details 1').item.json.attachments[0] }}"
            },
            {
              "key": "Currency|select",
              "selectValue": "={{ $('Take Invoice Details 1').item.json.currency }}"
            },
            {
              "key": "Destination Account|rich_text",
              "textContent": "={{ $('Take Invoice Details 1').item.json.destination_account }}"
            },
            {
              "key": "Discount Percent|number",
              "numberValue": "={{ $('Take Invoice Details 1').item.json.discount_percent_for_notion }}"
            },
            {
              "key": "Due Date|date",
              "date": "={{ $('Take Invoice Details 1').item.json.due_date }}"
            },
            {
              "key": "Invoice No|title",
              "title": "={{ $('Take Invoice Details 1').item.json.invoice_no }}"
            },
            {
              "key": "Issue Date|date",
              "date": "={{ $('Take Invoice Details 1').item.json.issue_date }}"
            },
            {
              "key": "Line Items|rich_text",
              "textContent": "={{ $('Take Invoice Details 1').item.json.line_items_rich_text[0].text.content }}"
            },
            {
              "key": "Notes|rich_text",
              "textContent": "={{ $('Take Invoice Details 1').item.json.notes }}"
            },
            {
              "key": "Paid Amount|number",
              "numberValue": "={{ $('Take Invoice Details 1').item.json.paid_amount }}"
            },
            {
              "key": "Payment Ref|rich_text",
              "textContent": "={{ $('Take Invoice Details 1').item.json.payment_ref }}"
            },
            {
              "key": "Recipient Account|rich_text",
              "textContent": "={{ $('Take Invoice Details 1').item.json.destination_account }}"
            },
            {
              "key": "Status|select",
              "selectValue": "={{ $('Take Invoice Details 1').item.json.status }}"
            },
            {
              "key": "Subtotal|number",
              "numberValue": "={{ $('Take Invoice Details 1').item.json.subtotal }}"
            },
            {
              "key": "Tax Total|number",
              "numberValue": "={{ $('Take Invoice Details 1').item.json.tax_total }}"
            },
            {
              "key": "Vendor|rich_text",
              "textContent": "={{ $('Take Invoice Details 1').item.json.vendor }}"
            },
            {
              "key": "=Source File|relation",
              "relationValue": [
                "={{ $('Send to Source File Invoice 3').item.json.id }}"
              ]
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "5fcebb58-8279-4d89-981f-27d34f2c2747",
      "name": "获取发票详情2",
      "type": "n8n-nodes-base.code",
      "position": [
        -1152,
        2576
      ],
      "parameters": {
        "jsCode": "// Code node: Pick Reviewed Invoice\n// Input: the Wait node callback (has query.choice, query.inv)\n// Also reads items from \"Prepare Line Items before send\"\n// Output: ONE item = the picked invoice + review info\n\nconst choice = $json?.query?.choice || '';              // approve | unpaid | archive\nconst invRaw = $json?.query?.inv || '';\nconst inv = String(invRaw).trim();\n\nconst prepared = $('Prepare Line Items before send').all().map(i => i.json);\nif (!prepared.length) {\n  throw new Error('No items found in \"Prepare Line Items before send\".');\n}\n\n// try exact invoice_no match first\nlet picked = inv\n  ? prepared.find(x => String(x.invoice_no || '').trim() === inv)\n  : undefined;\n\n// soft fallback(s)\nif (!picked) {\n  if (prepared.length === 1) {\n    picked = prepared[0];\n  } else {\n    const canon = s => String(s || '').toLowerCase().replace(/\\s+/g, '').trim();\n    const invC = canon(inv);\n    picked = prepared.find(x => canon(x.invoice_no) === invC) || prepared[0];\n  }\n}\n\n// annotate with the review decision so the next Switch can route\nconst out = {\n  ...picked,\n  review: {\n    choice,            // approve | unpaid | archive\n    inv,               // invoice_no from the button\n    source: 'slack_button'\n  }\n};\n\nreturn [{ json: out }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "d6375c43-4c99-41fe-9215-eec2ba228e17",
      "name": "获取发票详情1",
      "type": "n8n-nodes-base.code",
      "position": [
        -1152,
        2192
      ],
      "parameters": {
        "jsCode": "// Code node: Pick Reviewed Invoice\n// Input: the Wait node callback (has query.choice, query.inv)\n// Also reads items from \"Prepare Line Items before send\"\n// Output: ONE item = the picked invoice + review info\n\nconst choice = $json?.query?.choice || '';              // approve | unpaid | archive\nconst invRaw = $json?.query?.inv || '';\nconst inv = String(invRaw).trim();\n\nconst prepared = $('Prepare Line Items before send').all().map(i => i.json);\nif (!prepared.length) {\n  throw new Error('No items found in \"Prepare Line Items before send\".');\n}\n\n// try exact invoice_no match first\nlet picked = inv\n  ? prepared.find(x => String(x.invoice_no || '').trim() === inv)\n  : undefined;\n\n// soft fallback(s)\nif (!picked) {\n  if (prepared.length === 1) {\n    picked = prepared[0];\n  } else {\n    const canon = s => String(s || '').toLowerCase().replace(/\\s+/g, '').trim();\n    const invC = canon(inv);\n    picked = prepared.find(x => canon(x.invoice_no) === invC) || prepared[0];\n  }\n}\n\n// annotate with the review decision so the next Switch can route\nconst out = {\n  ...picked,\n  review: {\n    choice,            // approve | unpaid | archive\n    inv,               // invoice_no from the button\n    source: 'slack_button'\n  }\n};\n\nreturn [{ json: out }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "a9a19c40-4875-4c02-9c2b-3f65124ebdec",
      "name": "更新发票为全额支付",
      "type": "n8n-nodes-base.notion",
      "position": [
        -704,
        2480
      ],
      "parameters": {
        "pageId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Take Invoice Details 2').item.json.db_page_id }}"
        },
        "options": {},
        "resource": "databasePage",
        "operation": "update",
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Paid Amount|number",
              "numberValue": "={{ $('Take Invoice Details 2').item.json.paid_amount }}"
            },
            {
              "key": "Status|select",
              "selectValue": "=Unpaid"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "0af156a4-3324-4a1f-9f63-7a181adb4485",
      "name": "发送至源文件发票",
      "type": "n8n-nodes-base.notion",
      "position": [
        -704,
        2672
      ],
      "parameters": {
        "title": "={{ $json.source_file_id }}",
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "238f7557-9573-8062-b50d-fe9db34ec410",
          "cachedResultUrl": "https://www.notion.so/238f755795738062b50dfe9db34ec410",
          "cachedResultName": "Source File"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "File ID|title",
              "title": "={{ $json.source_file_id }}"
            },
            {
              "key": "Filename|rich_text",
              "textContent": "={{ $json.source_file_name }}"
            },
            {
              "key": "File URL|url",
              "urlValue": "={{ $json.source_file_url }}"
            },
            {
              "key": "Summary|rich_text",
              "text": {
                "text": [
                  {
                    "text": "={{ $json.line_items_rich_text[0].text.content }}",
                    "annotationUi": {}
                  }
                ]
              },
              "richText": true
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "a1eaf719-3843-424f-b758-b58b32cdc30c",
      "name": "创建新支付发票",
      "type": "n8n-nodes-base.notion",
      "position": [
        -480,
        2672
      ],
      "parameters": {
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "24df7557-9573-8073-9611-da8371404254",
          "cachedResultUrl": "https://www.notion.so/24df7557957380739611da8371404254",
          "cachedResultName": "Clearflow Invoice (Ledger)"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Attachments|rich_text",
              "textContent": "={{ $('Take Invoice Details 2').item.json.attachments[0] }}"
            },
            {
              "key": "Currency|select",
              "selectValue": "={{ $('Take Invoice Details 2').item.json.currency }}"
            },
            {
              "key": "Destination Account|rich_text",
              "textContent": "={{ $('Take Invoice Details 2').item.json.destination_account }}"
            },
            {
              "key": "Discount Percent|number",
              "numberValue": "={{ $('Take Invoice Details 2').item.json.discount_percent_for_notion }}"
            },
            {
              "key": "Due Date|date",
              "date": "={{ $('Take Invoice Details 2').item.json.due_date }}"
            },
            {
              "key": "Invoice No|title",
              "title": "={{ $('Take Invoice Details 2').item.json.invoice_no }}"
            },
            {
              "key": "Issue Date|date",
              "date": "={{ $('Take Invoice Details 2').item.json.issue_date }}"
            },
            {
              "key": "Line Items|rich_text",
              "textContent": "={{ $('Take Invoice Details 2').item.json.line_items_rich_text[0].text.content }}"
            },
            {
              "key": "Notes|rich_text",
              "textContent": "={{ $('Take Invoice Details 2').item.json.notes }}"
            },
            {
              "key": "Payment Ref|rich_text",
              "textContent": "={{ $('Take Invoice Details 2').item.json.payment_ref }}"
            },
            {
              "key": "Recipient Account|rich_text",
              "textContent": "={{ $('Take Invoice Details 2').item.json.destination_account }}"
            },
            {
              "key": "Status|select",
              "selectValue": "={{ $('Take Invoice Details 2').item.json.status }}"
            },
            {
              "key": "Subtotal|number",
              "numberValue": "={{ $('Take Invoice Details 2').item.json.subtotal }}"
            },
            {
              "key": "Tax Total|number",
              "numberValue": "={{ $('Take Invoice Details 2').item.json.tax_total }}"
            },
            {
              "key": "Vendor|rich_text",
              "textContent": "={{ $('Take Invoice Details 2').item.json.vendor }}"
            },
            {
              "key": "=Source File|relation",
              "relationValue": [
                "={{ $('Send to Source File Invoice ').item.json.id }}"
              ]
            },
            {
              "key": "Paid Amount|number",
              "numberValue": "={{ $('Take Invoice Details 2').item.json.paid_amount }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "2d7330e7-e3b9-4afc-9217-9dcbdbd4a3e9",
      "name": "归档发票1",
      "type": "n8n-nodes-base.notion",
      "position": [
        -704,
        2864
      ],
      "parameters": {
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "254f7557-9573-8012-b9b3-e7811bfac12c",
          "cachedResultUrl": "https://www.notion.so/254f755795738012b9b3e7811bfac12c",
          "cachedResultName": "Archived Clearflow Invoice (Ledger)"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Attachments|rich_text",
              "textContent": "={{ $('Take Invoice Details 3').item.json.attachments[0] }}"
            },
            {
              "key": "Currency|select",
              "selectValue": "={{ $('Take Invoice Details 3').item.json.currency }}"
            },
            {
              "key": "Destination Account|rich_text",
              "textContent": "={{ $('Take Invoice Details 3').item.json.destination_account }}"
            },
            {
              "key": "Discount Percent|number",
              "numberValue": "={{ $('Take Invoice Details 3').item.json.discount_percent_for_notion }}"
            },
            {
              "key": "Due Date|date",
              "date": "={{ $('Take Invoice Details 3').item.json.due_date }}"
            },
            {
              "key": "Invoice No|title",
              "title": "={{ $('Take Invoice Details 3').item.json.invoice_no }}"
            },
            {
              "key": "Issue Date|date",
              "date": "={{ $('Take Invoice Details 3').item.json.issue_date }}"
            },
            {
              "key": "Line Items|rich_text",
              "textContent": "={{ $('Take Invoice Details 3').item.json.line_items_rich_text[0].text.content }}"
            },
            {
              "key": "Notes|rich_text",
              "textContent": "={{ $('Take Invoice Details 3').item.json.notes || $('Take Invoice Details 3').item.json.reason}}"
            },
            {
              "key": "Paid Amount|number",
              "numberValue": "={{ $('Take Invoice Details 3').item.json.paid_amount }}"
            },
            {
              "key": "Payment Ref|rich_text",
              "textContent": "={{ $('Take Invoice Details 3').item.json.payment_ref }}"
            },
            {
              "key": "Recipient Account|rich_text",
              "textContent": "={{ $('Take Invoice Details 3').item.json.destination_account }}"
            },
            {
              "key": "Status|select",
              "selectValue": "={{ $('Take Invoice Details 3').item.json.status }}"
            },
            {
              "key": "Subtotal|number",
              "numberValue": "={{ $('Take Invoice Details 3').item.json.subtotal }}"
            },
            {
              "key": "Tax Total|number",
              "numberValue": "={{ $('Take Invoice Details 3').item.json.tax_total }}"
            },
            {
              "key": "Vendor|rich_text",
              "textContent": "={{ $('Take Invoice Details 3').item.json.vendor }}"
            },
            {
              "key": "=Source File|relation",
              "relationValue": [
                "={{ $json.id }}"
              ]
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "6342d55e-4e82-4755-8826-3d4e5b7f9be7",
      "name": "获取发票详情3",
      "type": "n8n-nodes-base.code",
      "position": [
        -1152,
        2864
      ],
      "parameters": {
        "jsCode": "// Code node: Pick Reviewed Invoice\n// Input: the Wait node callback (has query.choice, query.inv)\n// Also reads items from \"Prepare Line Items before send\"\n// Output: ONE item = the picked invoice + review info\n\nconst choice = $json?.query?.choice || '';              // approve | unpaid | archive\nconst invRaw = $json?.query?.inv || '';\nconst inv = String(invRaw).trim();\n\nconst prepared = $('Prepare Line Items before send').all().map(i => i.json);\nif (!prepared.length) {\n  throw new Error('No items found in \"Prepare Line Items before send\".');\n}\n\n// try exact invoice_no match first\nlet picked = inv\n  ? prepared.find(x => String(x.invoice_no || '').trim() === inv)\n  : undefined;\n\n// soft fallback(s)\nif (!picked) {\n  if (prepared.length === 1) {\n    picked = prepared[0];\n  } else {\n    const canon = s => String(s || '').toLowerCase().replace(/\\s+/g, '').trim();\n    const invC = canon(inv);\n    picked = prepared.find(x => canon(x.invoice_no) === invC) || prepared[0];\n  }\n}\n\n// annotate with the review decision so the next Switch can route\nconst out = {\n  ...picked,\n  review: {\n    choice,            // approve | unpaid | archive\n    inv,               // invoice_no from the button\n    source: 'slack_button'\n  }\n};\n\nreturn [{ json: out }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "c6f37078-96cb-4e97-b678-ebb688afb413",
      "name": "检查数据库存在1",
      "type": "n8n-nodes-base.if",
      "position": [
        -928,
        2192
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "95b89f55-ca6f-46ce-bc0c-8ea48b18c013",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.db_found }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "2b1a1c57-bad7-4629-a969-aced98962828",
      "name": "检查数据库存在2",
      "type": "n8n-nodes-base.if",
      "position": [
        -928,
        2576
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "95b89f55-ca6f-46ce-bc0c-8ea48b18c013",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.db_found }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "ed421b2f-4a7e-4098-9d09-96965813f3e4",
      "name": "发送至源文件发票4",
      "type": "n8n-nodes-base.notion",
      "position": [
        -1872,
        1504
      ],
      "parameters": {
        "title": "={{ $json.source_file_id }}",
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "238f7557-9573-8062-b50d-fe9db34ec410",
          "cachedResultUrl": "https://www.notion.so/238f755795738062b50dfe9db34ec410",
          "cachedResultName": "Source File"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "File ID|title",
              "title": "={{ $json.source_file_id }}"
            },
            {
              "key": "Filename|rich_text",
              "textContent": "={{ $json.source_file_name }}"
            },
            {
              "key": "File URL|url",
              "urlValue": "={{ $json.source_file_url }}"
            },
            {
              "key": "Summary|rich_text",
              "text": {
                "text": [
                  {
                    "text": "={{ $json.line_items_rich_text[0].text.content }}",
                    "annotationUi": {}
                  }
                ]
              },
              "richText": true
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "3f33a49f-f4d5-4483-b01b-ab7b05f1edef",
      "name": "将收据添加到部分支付现金流",
      "type": "n8n-nodes-base.notion",
      "position": [
        -1648,
        1504
      ],
      "parameters": {
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "238f7557-9573-80a2-96c8-e638a56411d2",
          "cachedResultUrl": "https://www.notion.so/238f7557957380a296c8e638a56411d2",
          "cachedResultName": "Cashflow (Ledger)"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Cashflow ID|title",
              "title": "={{ $('Prepare Line Items before send').item.json.receipt_no || 'cf_' + Date.now() + '_' + Math.floor(Math.random() * 1000) }}"
            },
            {
              "key": "Amount (Net)|number",
              "numberValue": "={{ $('Prepare Line Items before send').item.json.paid_amount }}"
            },
            {
              "key": "Currency|select",
              "selectValue": "={{ $('Prepare Line Items before send').item.json.currency }}"
            },
            {
              "key": "Fee|number",
              "numberValue": "={{ $('Prepare Line Items before send').item.json.tax_total }}"
            },
            {
              "key": "Gross Amount|number",
              "numberValue": "={{ $('Prepare Line Items before send').item.json.paid_amount }}"
            },
            {
              "key": "Notes|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.notes || \"Partial Paid\" }}"
            },
            {
              "key": "Timestamp|date",
              "date": "={{ $('Prepare Line Items before send').item.json.paid_date }}"
            },
            {
              "key": "Timestamp (ISO)|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.paid_date }}"
            },
            {
              "key": "Type|select",
              "selectValue": "=Expense"
            },
            {
              "key": "Vendor / Recipient|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.vendor }}"
            },
            {
              "key": "Vendor Ref ID|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.receipt_no }}"
            },
            {
              "key": "Source File|relation",
              "relationValue": [
                "={{ $json.id }}"
              ]
            },
            {
              "key": "Recipient Account|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.vendor }}"
            },
            {
              "key": "Source Account|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.payment_method }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "a988e2c7-ad51-4df3-b4d6-9d5cd6e123af",
      "name": "创建新部分支付发票",
      "type": "n8n-nodes-base.notion",
      "position": [
        -1424,
        1504
      ],
      "parameters": {
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "24df7557-9573-8073-9611-da8371404254",
          "cachedResultUrl": "https://www.notion.so/24df7557957380739611da8371404254",
          "cachedResultName": "Clearflow Invoice (Ledger)"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Attachments|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.attachments[0] }}"
            },
            {
              "key": "Currency|select",
              "selectValue": "={{ $('Prepare Line Items before send').item.json.currency }}"
            },
            {
              "key": "Destination Account|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.destination_account }}"
            },
            {
              "key": "Discount Percent|number",
              "numberValue": "={{ $('Prepare Line Items before send').item.json.discount_percent_for_notion }}"
            },
            {
              "key": "Due Date|date",
              "date": "={{ $('Prepare Line Items before send').item.json.due_date }}"
            },
            {
              "key": "Invoice No|title",
              "title": "={{ $('Prepare Line Items before send').item.json.invoice_no }}"
            },
            {
              "key": "Issue Date|date",
              "date": "={{ $('Prepare Line Items before send').item.json.issue_date }}"
            },
            {
              "key": "Line Items|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.line_items_rich_text[0].text.content }}"
            },
            {
              "key": "Notes|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.notes }}"
            },
            {
              "key": "Paid Amount|number",
              "numberValue": "={{ $('Prepare Line Items before send').item.json.paid_amount }}"
            },
            {
              "key": "Payment Ref|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.payment_ref }}"
            },
            {
              "key": "Recipient Account|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.destination_account }}"
            },
            {
              "key": "Status|select",
              "selectValue": "={{ $('Prepare Line Items before send').item.json.status }}"
            },
            {
              "key": "Subtotal|number",
              "numberValue": "={{ $('Prepare Line Items before send').item.json.subtotal }}"
            },
            {
              "key": "Tax Total|number",
              "numberValue": "={{ $('Prepare Line Items before send').item.json.tax_total }}"
            },
            {
              "key": "Vendor|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.vendor }}"
            },
            {
              "key": "=Source File|relation",
              "relationValue": [
                "={{ $('Send to Source File Invoice 4').item.json.id }}"
              ]
            },
            {
              "key": "Source Account|rich_text",
              "textContent": "={{ $('Prepare Line Items before send').item.json.payment_method }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "8e8ac821-5cd5-4bd2-b78d-203fd5cf57fb",
      "name": "将收据添加到现金流2",
      "type": "n8n-nodes-base.notion",
      "position": [
        -1872,
        1696
      ],
      "parameters": {
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "238f7557-9573-80a2-96c8-e638a56411d2",
          "cachedResultUrl": "https://www.notion.so/238f7557957380a296c8e638a56411d2",
          "cachedResultName": "Cashflow (Ledger)"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Cashflow ID|title",
              "title": "={{ $json.receipt_no || 'cf_' + Date.now() + '_' + Math.floor(Math.random() * 1000) }}"
            },
            {
              "key": "Amount (Net)|number",
              "numberValue": "={{ $json.paid_amount }}"
            },
            {
              "key": "Currency|select",
              "selectValue": "={{ $json.currency }}"
            },
            {
              "key": "Fee|number",
              "numberValue": "={{ $json.tax_total }}"
            },
            {
              "key": "Gross Amount|number",
              "numberValue": "={{ $json.paid_amount }}"
            },
            {
              "key": "Notes|rich_text",
              "textContent": "={{ $json.notes }}"
            },
            {
              "key": "Timestamp|date",
              "date": "={{ $json.paid_date }}"
            },
            {
              "key": "Timestamp (ISO)|rich_text",
              "textContent": "={{ $json.paid_date }}"
            },
            {
              "key": "Type|select",
              "selectValue": "=Expense"
            },
            {
              "key": "Vendor / Recipient|rich_text",
              "textContent": "={{ $json.vendor }}"
            },
            {
              "key": "Vendor Ref ID|rich_text",
              "textContent": "={{ $json.invoice_no }}"
            },
            {
              "key": "Source File|relation",
              "relationValue": [
                "={{ $json.db_page_id }}"
              ]
            },
            {
              "key": "Recipient Account|rich_text",
              "textContent": "={{ $json.vendor }}"
            },
            {
              "key": "Source Account|rich_text",
              "textContent": "={{ $json.payment_method }}"
            },
            {
              "key": "Notes|rich_text",
              "textContent": "={{ $json.line_items_rich_text[0].text.content }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "25951d6e-c27d-48b0-8bef-1b06bb931c30",
      "name": "更新发票为部分或全额支付",
      "type": "n8n-nodes-base.notion",
      "position": [
        -1648,
        1696
      ],
      "parameters": {
        "pageId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Prepare Line Items before send').item.json.db_page_id }}"
        },
        "options": {},
        "resource": "databasePage",
        "operation": "update",
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Receipt|relation",
              "relationValue": [
                "={{ $json.id }}"
              ]
            },
            {
              "key": "Paid Amount|number",
              "numberValue": "={{ $('Prepare Line Items before send').item.json.notion_paid_amount }}"
            },
            {
              "key": "Status|select",
              "selectValue": "={{ $('Prepare Line Items before send').item.json.notion_status }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "1ed7bced-88d4-4210-b323-c64bf1c1fece",
      "name": "检查解析错误2",
      "type": "n8n-nodes-base.if",
      "position": [
        -4688,
        1552
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "ddbbbfbb-e05b-4ca9-b607-65ad8ac748bb",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.IsErroredOnProcessing }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "1c81ef35-2d9b-435b-90ae-979dbebe8ace",
      "name": "OCR Space 解析",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -4912,
        1552
      ],
      "parameters": {
        "url": "https://api.ocr.space/parse/image",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "multipart-form-data",
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "filetype",
              "value": "jpg"
            },
            {
              "name": "file",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "data"
            },
            {
              "name": "scale",
              "value": "true"
            },
            {
              "name": "OCREngine",
              "value": "2"
            }
          ]
        },
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "id": "dkJjl1msBNIeIT5u",
          "name": "OCR Space"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "7dde97e8-b6cd-4527-a948-36d62ca9d235",
      "name": "获取二进制文件",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -5136,
        1552
      ],
      "parameters": {
        "url": "={{ $json.url_private_download }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        },
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "slackApi"
      },
      "credentials": {
        "slackApi": {
          "id": "muT22qcO4YUdHAGU",
          "name": "Slack account"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "d2da0e22-0a2f-4a66-8651-7a0bd73f3452",
      "name": "停止并报错",
      "type": "n8n-nodes-base.stopAndError",
      "position": [
        -4464,
        1504
      ],
      "parameters": {
        "errorMessage": "=OCR Server Parsing Failed Reason:{{ $json.ErrorMessage[0] }}"
      },
      "typeVersion": 1
    },
    {
      "id": "6376b9b6-40c7-4f01-8a11-8f5bee34dfd4",
      "name": "停止和错误1",
      "type": "n8n-nodes-base.stopAndError",
      "position": [
        -4688,
        1744
      ],
      "parameters": {
        "errorMessage": "=OCR Server Parsing Failed Reason:{{ $json.ErrorMessage[0] }}"
      },
      "typeVersion": 1
    },
    {
      "id": "17f5f3e0-7aa0-45c2-9c84-d840321ef94a",
      "name": "聚合1",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -2320,
        2032
      ],
      "parameters": {
        "include": "specifiedFields",
        "options": {},
        "aggregate": "aggregateAllItemData",
        "fieldsToInclude": "name, url"
      },
      "typeVersion": 1
    },
    {
      "id": "d707c4fc-d2a5-496d-9d6b-8c71f627ad21",
      "name": "发送重复通知",
      "type": "n8n-nodes-base.slack",
      "position": [
        -2096,
        2032
      ],
      "webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
      "parameters": {
        "text": "=🟥 Internal Duplicate Found:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0971ANKZM1",
          "cachedResultName": "duplicate-alert"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "id": "muT22qcO4YUdHAGU",
          "name": "Slack account"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "548ea7e3-8e51-4a56-891e-1db51e466b2d",
      "name": "聚合",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -1424,
        736
      ],
      "parameters": {
        "include": "specifiedFields",
        "options": {},
        "aggregate": "aggregateAllItemData",
        "fieldsToInclude": "name, url"
      },
      "typeVersion": 1
    },
    {
      "id": "893731ba-84cd-4189-9c3d-c1a73c158979",
      "name": "聚合2",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -1200,
        928
      ],
      "parameters": {
        "include": "specifiedFields",
        "options": {},
        "aggregate": "aggregateAllItemData",
        "fieldsToInclude": "name, url"
      },
      "typeVersion": 1
    },
    {
      "id": "2d9bf727-1b2f-4a1e-bc54-61c686f63d01",
      "name": "通知带收据的新发票",
      "type": "n8n-nodes-base.slack",
      "position": [
        -976,
        928
      ],
      "webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
      "parameters": {
        "text": "=🟩 Paid Invoice Created:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0976KA2QTC",
          "cachedResultName": "notification"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "id": "muT22qcO4YUdHAGU",
          "name": "Slack account"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "cb2e3474-2c42-4804-a1a0-aea65c45aece",
      "name": "通知新发票",
      "type": "n8n-nodes-base.slack",
      "position": [
        -1200,
        736
      ],
      "webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
      "parameters": {
        "text": "=🟨 Invoice Created:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0976KA2QTC",
          "cachedResultName": "notification"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "id": "muT22qcO4YUdHAGU",
          "name": "Slack account"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "6db4ac23-65d8-4e5a-b1ae-84777f7c6a0b",
      "name": "聚合3",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -1424,
        1312
      ],
      "parameters": {
        "include": "specifiedFields",
        "options": {},
        "aggregate": "aggregateAllItemData",
        "fieldsToInclude": "name, url"
      },
      "typeVersion": 1
    },
    {
      "id": "7766fd88-d419-47ee-b545-e6d6d574045f",
      "name": "通知更新发票",
      "type": "n8n-nodes-base.slack",
      "position": [
        -1200,
        1312
      ],
      "webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
      "parameters": {
        "text": "=🟩 Invoice Updated to Paid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0976KA2QTC",
          "cachedResultName": "notification"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "id": "muT22qcO4YUdHAGU",
          "name": "Slack account"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "ec4398cb-c999-43bb-9d71-76a9ec6e2747",
      "name": "聚合4",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -1424,
        1120
      ],
      "parameters": {
        "include": "specifiedFields",
        "options": {},
        "aggregate": "aggregateAllItemData",
        "fieldsToInclude": "name, url"
      },
      "typeVersion": 1
    },
    {
      "id": "8976661d-c12c-4b3b-92c9-337fb7b3be3f",
      "name": "聚合5",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -1200,
        1504
      ],
      "parameters": {
        "include": "specifiedFields",
        "options": {},
        "aggregate": "aggregateAllItemData",
        "fieldsToInclude": "name, url"
      },
      "typeVersion": 1
    },
    {
      "id": "20260742-e05b-4ce1-b2ca-40db32c1e66b",
      "name": "聚合6",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -1424,
        1696
      ],
      "parameters": {
        "include": "specifiedFields",
        "options": {},
        "aggregate": "aggregateAllItemData",
        "fieldsToInclude": "name, url"
      },
      "typeVersion": 1
    },
    {
      "id": "75d16382-b71a-413c-abb7-5f6ee529b268",
      "name": "通知部分支付更新发票",
      "type": "n8n-nodes-base.slack",
      "position": [
        -1200,
        1696
      ],
      "webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
      "parameters": {
        "text": "=🟡 Updated Invoice Partial Paid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0976KA2QTC",
          "cachedResultName": "notification"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "id": "muT22qcO4YUdHAGU",
          "name": "Slack account"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "be11d35c-a96d-432d-bf60-747e9242b8b0",
      "name": "通知创建部分支付发票",
      "type": "n8n-nodes-base.slack",
      "position": [
        -976,
        1504
      ],
      "webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
      "parameters": {
        "text": "=🟡 Created New Invoice Partial Paid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0976KA2QTC",
          "cachedResultName": "notification"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "id": "muT22qcO4YUdHAGU",
          "name": "Slack account"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "cb059bc5-42c4-4eee-a4c5-f70013b81d1f",
      "name": "通知发票已归档",
      "type": "n8n-nodes-base.slack",
      "position": [
        -1200,
        1120
      ],
      "webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
      "parameters": {
        "text": "=🟥 Database Duplicate Found:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0976KA2QTC",
          "cachedResultName": "notification"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "id": "muT22qcO4YUdHAGU",
          "name": "Slack account"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "29cc18f8-253c-4763-a2ae-e9f2668b4e8c",
      "name": "聚合8",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -480,
        2864
      ],
      "parameters": {
        "include": "specifiedFields",
        "options": {},
        "aggregate": "aggregateAllItemData",
        "fieldsToInclude": "name, url"
      },
      "typeVersion": 1
    },
    {
      "id": "852ca201-dfe6-4d5b-bf6d-9d06954f0604",
      "name": "通知发票已归档2",
      "type": "n8n-nodes-base.slack",
      "position": [
        -256,
        2864
      ],
      "webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
      "parameters": {
        "text": "=🟥 Manual Review Archived:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0976KA2QTC",
          "cachedResultName": "notification"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "id": "muT22qcO4YUdHAGU",
          "name": "Slack account"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "2e9ab2da-f324-4864-8617-4ebf6d6873ab",
      "name": "发送至归档源文件发票",
      "type": "n8n-nodes-base.notion",
      "position": [
        -928,
        2864
      ],
      "parameters": {
        "title": "={{ $json.source_file_id }}",
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "239f7557-9573-80d6-a8fb-fc710833e5db",
          "cachedResultUrl": "https://www.notion.so/239f7557957380d6a8fbfc710833e5db",
          "cachedResultName": "Archived Source File (Duplicate)"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "File ID|title",
              "title": "={{ $json.source_file_id }}"
            },
            {
              "key": "Filename|rich_text",
              "textContent": "={{ $json.source_file_name }}"
            },
            {
              "key": "FILE URL|url",
              "urlValue": "={{ $json.source_file_url }}"
            },
            {
              "key": "Summary|rich_text",
              "text": {
                "text": [
                  {
                    "text": "={{ $json.line_items_rich_text[0].text.content }}",
                    "annotationUi": {}
                  }
                ]
              },
              "richText": true
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "ff53fb86-f773-4690-b874-be81d77259ab",
      "name": "聚合7",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -256,
        2672
      ],
      "parameters": {
        "include": "specifiedFields",
        "options": {},
        "aggregate": "aggregateAllItemData",
        "fieldsToInclude": "name, url"
      },
      "typeVersion": 1
    },
    {
      "id": "38fd6b37-35cd-43f4-a6a1-caf4c20f0848",
      "name": "通知新发票1",
      "type": "n8n-nodes-base.slack",
      "position": [
        -32,
        2672
      ],
      "webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
      "parameters": {
        "text": "=🟨 Manual Review Invoice Created Unpaid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0976KA2QTC",
          "cachedResultName": "notification"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "id": "muT22qcO4YUdHAGU",
          "name": "Slack account"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "30d801f2-7ae0-4f43-882d-2df58867102e",
      "name": "聚合9",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -480,
        2480
      ],
      "parameters": {
        "include": "specifiedFields",
        "options": {},
        "aggregate": "aggregateAllItemData",
        "fieldsToInclude": "name, url"
      },
      "typeVersion": 1
    },
    {
      "id": "3fbead5e-2aa0-4222-a2ff-2a03901ffbe0",
      "name": "通知新发票2",
      "type": "n8n-nodes-base.slack",
      "position": [
        -256,
        2480
      ],
      "webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
      "parameters": {
        "text": "=🟨 Manual Review Invoice Updated Unpaid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0976KA2QTC",
          "cachedResultName": "notification"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "id": "muT22qcO4YUdHAGU",
          "name": "Slack account"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "9706d716-8bfc-433e-9d09-8a3a479a5ecc",
      "name": "聚合10",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -32,
        2288
      ],
      "parameters": {
        "include": "specifiedFields",
        "options": {},
        "aggregate": "aggregateAllItemData",
        "fieldsToInclude": "name, url"
      },
      "typeVersion": 1
    },
    {
      "id": "2731b665-b116-459e-9a90-1d28d63d1c85",
      "name": "通知带收据的新发票1",
      "type": "n8n-nodes-base.slack",
      "position": [
        192,
        2288
      ],
      "webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
      "parameters": {
        "text": "=🟩 Manual Review Invoice Created Paid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0976KA2QTC",
          "cachedResultName": "notification"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "id": "muT22qcO4YUdHAGU",
          "name": "Slack account"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "ffec1ef1-51d6-4870-b4e6-86af6c95ed87",
      "name": "聚合11",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -256,
        2096
      ],
      "parameters": {
        "include": "specifiedFields",
        "options": {},
        "aggregate": "aggregateAllItemData",
        "fieldsToInclude": "name, url"
      },
      "typeVersion": 1
    },
    {
      "id": "b206f8e8-74f5-4502-8dc1-ef154867890f",
      "name": "通知带收据的新发票2",
      "type": "n8n-nodes-base.slack",
      "position": [
        -32,
        2096
      ],
      "webhookId": "f6b10e04-64be-4474-9ffd-2c8f511c9020",
      "parameters": {
        "text": "=🟩 Manual Review Invoice Updated Paid:\n{{ ($json.data || []).map(i => `*<${i.url}|${i.name}>*\\n`).join('') }}\n────────────────────────",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0976KA2QTC",
          "cachedResultName": "notification"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "id": "muT22qcO4YUdHAGU",
          "name": "Slack account"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "b4ea52bb-acc9-42d9-a61b-0f89bcfabba0",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -6112,
        1440
      ],
      "parameters": {
        "width": 672,
        "height": 704,
        "content": "# 🔤 输入与格式检测"
      },
      "typeVersion": 1
    },
    {
      "id": "4a5b2a7d-80d5-4897-884c-00d0451daee0",
      "name": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -5376,
        1440
      ],
      "parameters": {
        "color": 2,
        "width": 1056,
        "height": 704,
        "content": "# 👨‍💻 解析数据"
      },
      "typeVersion": 1
    },
    {
      "id": "c3f62378-ecf0-4bd2-88d1-63e5268ef615",
      "name": "便签2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4272,
        1440
      ],
      "parameters": {
        "color": 3,
        "width": 1200,
        "height": 704,
        "content": "# 🤖 AI 格式化与重复检测"
      },
      "typeVersion": 1
    },
    {
      "id": "3f9110e8-9778-48e4-ab48-286bdfefc291",
      "name": "便签3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3040,
        1888
      ],
      "parameters": {
        "color": 7,
        "width": 1120,
        "height": 384,
        "content": "# ❌ 归档重复项"
      },
      "typeVersion": 1
    },
    {
      "id": "f32354a7-d05f-49e3-b60d-5d48c7f02126",
      "name": "便签4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3024,
        1200
      ],
      "parameters": {
        "color": 6,
        "width": 1040,
        "height": 384,
        "content": "# 🟢 检查数据库中的发票"
      },
      "typeVersion": 1
    },
    {
      "id": "4996b730-0f80-4a37-a702-fc8588934ece",
      "name": "便签5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1936,
        672
      ],
      "parameters": {
        "color": 4,
        "width": 1136,
        "height": 1200,
        "content": "# 🟢 发送数据与通知"
      },
      "typeVersion": 1
    },
    {
      "id": "49782403-dd3c-4457-97ca-bf49c393c395",
      "name": "便签7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1888,
        2016
      ],
      "parameters": {
        "color": 5,
        "width": 2384,
        "height": 1072,
        "content": "# 👤 人工审核与发送数据"
      },
      "typeVersion": 1
    },
    {
      "id": "ff8438b9-5804-473b-bd6b-c9840be62317",
      "name": "错误触发器",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        -6048,
        2192
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "fdaacf56-2efe-4272-80fc-365951cd7d48",
      "name": "创建数据库页面",
      "type": "n8n-nodes-base.notion",
      "position": [
        -5840,
        2192
      ],
      "parameters": {
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "list",
          "value": "258f7557-9573-8013-bb51-d528ba507e68",
          "cachedResultUrl": "https://www.notion.so/258f755795738013bb51d528ba507e68",
          "cachedResultName": "ERROR CHECK"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Message|rich_text",
              "textContent": "={{ $json.execution.error.message }}"
            },
            {
              "key": "Workflow|title",
              "title": "={{ $json.workflow.name }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "id": "FRdXwx5ANpHG8ZPX",
          "name": "Notion account"
        }
      },
      "typeVersion": 2.2
    }
  ],
  "pinData": {},
  "connections": {
    "Code": {
      "main": [
        [
          {
            "node": "Merge Data Value into One Key",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait": {
      "main": [
        [
          {
            "node": "Approval Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate": {
      "main": [
        [
          {
            "node": "Notify New Invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate1": {
      "main": [
        [
          {
            "node": "Send Duplicate Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate2": {
      "main": [
        [
          {
            "node": "Notify New Invoice with Receipt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate3": {
      "main": [
        [
          {
            "node": "Notify Update Invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate4": {
      "main": [
        [
          {
            "node": "Notify Invoice Archived",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate5": {
      "main": [
        [
          {
            "node": "Notify Create Invoice Partial Paid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate6": {
      "main": [
        [
          {
            "node": "Notify Update Invoice Partial Paid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate7": {
      "main": [
        [
          {
            "node": "Notify New Invoice1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate8": {
      "main": [
        [
          {
            "node": "Notify Invoice Archived2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate9": {
      "main": [
        [
          {
            "node": "Notify New Invoice2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate10": {
      "main": [
        [
          {
            "node": "Notify New Invoice with Receipt1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate11": {
      "main": [
        [
          {
            "node": "Notify New Invoice with Receipt2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Decide Fate": {
      "main": [
        [
          {
            "node": "Send to Source File Invoice 1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send to Source File Invoice 2",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Add Receipt into Cashflow",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send to Archive Source File Invoice",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send to Source File Invoice 4",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Add Receipt into Cashflow2",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Format": {
      "main": [
        [
          {
            "node": "Take Binary Files",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Take Binary Files for Document",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Check": {
      "main": [
        [
          {
            "node": "Check Format",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Error Trigger": {
      "main": [
        [
          {
            "node": "Create a database page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack Trigger": {
      "main": [
        [
          {
            "node": "Format Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Approval Check": {
      "main": [
        [
          {
            "node": "Take Invoice Details 1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Take Invoice Details 2",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Take Invoice Details 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send a message": {
      "main": [
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Archive Invoice": {
      "main": [
        [
          {
            "node": "Aggregate4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Basic LLM Chain": {
      "main": [
        [
          {
            "node": "Cleans AI Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OCR Space Parse": {
      "main": [
        [
          {
            "node": "Check parsing Error2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check DB Exist 1": {
      "main": [
        [
          {
            "node": "Add Receipt into Cashflow1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send to Source File Invoice 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check DB Exist 2": {
      "main": [
        [
          {
            "node": "Update Invoice to Paid Fully ",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send to Source File Invoice ",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check DB Invoice": {
      "main": [
        [
          {
            "node": "Merge Items Invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OCR Space Parse1": {
      "main": [
        [
          {
            "node": "Check parsing Error3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Archive Invoice 1": {
      "main": [
        [
          {
            "node": "Aggregate8",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Take Binary Files": {
      "main": [
        [
          {
            "node": "OCR Space Parse",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cleans AI Response": {
      "main": [
        [
          {
            "node": "Internal Check Duplicate Invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Query for DB": {
      "main": [
        [
          {
            "node": "Check DB Invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Items Invoice": {
      "main": [
        [
          {
            "node": "Prepare Line Items before send",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check parsing Error2": {
      "main": [
        [
          {
            "node": "Stop and Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Merge Data Value into One Key",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check parsing Error3": {
      "main": [
        [
          {
            "node": "Stop and Error1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Code",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Decide Fate for Data": {
      "main": [
        [
          {
            "node": "Create Query for DB",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Archive Duplicate Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anthropic Chat Model4": {
      "ai_languageModel": [
        [
          {
            "node": "Basic LLM Chain",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Take Invoice Details 1": {
      "main": [
        [
          {
            "node": "Check DB Exist 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Take Invoice Details 2": {
      "main": [
        [
          {
            "node": "Check DB Exist 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Take Invoice Details 3": {
      "main": [
        [
          {
            "node": "Send to Archive Source File Invoice ",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create new Invoice Paid ": {
      "main": [
        [
          {
            "node": "Aggregate7",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge with Original Data": {
      "main": [
        [
          {
            "node": "Decide Fate for Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Receipt into Cashflow": {
      "main": [
        [
          {
            "node": "Update Invoice to Paid Fully",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Archive Invoice Duplicate": {
      "main": [
        [
          {
            "node": "Aggregate1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create new Invoice Paid 3": {
      "main": [
        [
          {
            "node": "Aggregate10",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create new Invoice Unpaid": {
      "main": [
        [
          {
            "node": "Aggregate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Receipt into Cashflow1": {
      "main": [
        [
          {
            "node": "Update Invoice to Paid Fully 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Receipt into Cashflow2": {
      "main": [
        [
          {
            "node": "Update Invoice to Paid Partial or Fully",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create new Invoice Paid Full": {
      "main": [
        [
          {
            "node": "Aggregate2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send to Source File Invoice ": {
      "main": [
        [
          {
            "node": "Create new Invoice Paid ",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Invoice to Paid Fully": {
      "main": [
        [
          {
            "node": "Aggregate3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Data Value into One Key": {
      "main": [
        [
          {
            "node": "Basic LLM Chain",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send to Source File Invoice 1": {
      "main": [
        [
          {
            "node": "Create new Invoice Unpaid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send to Source File Invoice 2": {
      "main": [
        [
          {
            "node": "Add Receipt into Cashflow Paid 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send to Source File Invoice 3": {
      "main": [
        [
          {
            "node": "Add Receipt into Cashflow Paid 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send to Source File Invoice 4": {
      "main": [
        [
          {
            "node": "Add Receipt into Cashflow Paid Partially",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Invoice to Paid Fully ": {
      "main": [
        [
          {
            "node": "Aggregate9",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Line Items before send": {
      "main": [
        [
          {
            "node": "Decide Fate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Take Binary Files for Document": {
      "main": [
        [
          {
            "node": "OCR Space Parse1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Invoice to Paid Fully 1": {
      "main": [
        [
          {
            "node": "Aggregate11",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Archive Duplicate Items": {
      "main": [
        [
          {
            "node": "Send to Archive Source File Invoice Duplicate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Receipt into Cashflow Paid 1": {
      "main": [
        [
          {
            "node": "Create new Invoice Paid Full",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Receipt into Cashflow Paid 2": {
      "main": [
        [
          {
            "node": "Create new Invoice Paid 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Internal Check Duplicate Invoice": {
      "main": [
        [
          {
            "node": "Merge with Original Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create new Invoice Paid Partially": {
      "main": [
        [
          {
            "node": "Aggregate5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send to Archive Source File Invoice": {
      "main": [
        [
          {
            "node": "Archive Invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send to Archive Source File Invoice ": {
      "main": [
        [
          {
            "node": "Archive Invoice 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Invoice to Paid Partial or Fully": {
      "main": [
        [
          {
            "node": "Aggregate6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Receipt into Cashflow Paid Partially": {
      "main": [
        [
          {
            "node": "Create new Invoice Paid Partially",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send to Archive Source File Invoice Duplicate": {
      "main": [
        [
          {
            "node": "Archive Invoice Duplicate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。

这个工作流适合什么场景?

高级 - AI 摘要总结, 多模态 AI

需要付费吗?

本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。

工作流信息
难度等级
高级
节点数量92
分类2
节点类型14
难度说明

适合高级用户,包含 16+ 个节点的复杂工作流

外部链接
在 n8n.io 查看

分享此工作流