{
  "name": "05 · Supplier Invoice Match — catch over-billing before payment",
  "nodes": [
    {
      "parameters": {
        "content": "# 05 · Supplier Invoice Match\n\n**The problem:** suppliers over-bill. Line prices drift, quantities don't match what was delivered, and nobody checks because it takes an hour per invoice.\n\n**What this does:** every invoice email is read automatically, the line items extracted, matched against your PO **and** what was actually received, then discrepancies are flagged **before you pay**.\n\n**The ROI pitch:** one caught over-bill per month pays for this pack.\n\n**Setup:** Gmail label `Invoices`, plus a `POs` sheet.\n\nBuilt by **Maui Tech Hub** · mauitechhub.com/workflows",
        "height": 460,
        "width": 400,
        "color": 7
      },
      "id": "1c1ef318-52bf-426a-ba14-f162c2151b44",
      "name": "NOTE",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -420,
        -60
      ]
    },
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "filters": {
          "labelIds": [
            "Invoices"
          ]
        },
        "options": {}
      },
      "id": "8222e6ea-5dbb-4cdc-bb2b-5e6bb1a8e5fb",
      "name": "Watch Invoice Emails",
      "type": "n8n-nodes-base.gmailTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        100
      ],
      "notes": "Label supplier invoices 'Invoices' in Gmail"
    },
    {
      "parameters": {
        "operation": "get",
        "messageId": "={{ $json.id }}",
        "options": {
          "downloadAttachments": true
        }
      },
      "id": "8bc7c012-02a2-4ed9-b4e6-59a710061746",
      "name": "Get Attachment",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        250,
        100
      ]
    },
    {
      "parameters": {
        "operation": "pdf",
        "options": {}
      },
      "id": "e1a5334c-be1f-4c25-a98d-de1775bde573",
      "name": "Extract Invoice Text",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [
        500,
        100
      ]
    },
    {
      "parameters": {
        "resource": "text",
        "operation": "message",
        "modelId": {
          "__rl": true,
          "value": "gpt-4o-mini",
          "mode": "list"
        },
        "messages": {
          "values": [
            {
              "role": "system",
              "content": "You extract structured data from supplier invoices. Reply with ONLY valid JSON, no prose, no code fences."
            },
            {
              "role": "user",
              "content": "=Extract this invoice. Return JSON exactly:\n{\"supplier\":\"\",\"invoice_no\":\"\",\"date\":\"\",\"currency\":\"\",\"total\":0,\"lines\":[{\"item\":\"\",\"sku\":\"\",\"qty\":0,\"unit_price\":0,\"line_total\":0}]}\n\nInvoice text:\n{{ $json.text }}"
            }
          ]
        },
        "options": {}
      },
      "id": "83a937f2-8d43-4612-93e7-3925bab93b4e",
      "name": "AI: Read the Invoice",
      "type": "n8n-nodes-base.openAi",
      "typeVersion": 1.7,
      "position": [
        750,
        100
      ]
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "YOUR_SHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "POs",
          "mode": "name"
        },
        "options": {}
      },
      "id": "274adfd0-c1dc-41e8-9635-9b0d94328501",
      "name": "Read PO Records",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 1,
      "position": [
        1000,
        100
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Compare invoice lines to the PO: price drift, qty over-run, unknown items.\nconst CURRENCY = 'KD';        // <- change to '$', '£', '€', 'AED' ...\nconst inv = $('AI: Read the Invoice').item.json;\nlet parsed;\ntry {\n  const raw = (inv.message && inv.message.content) || inv.content || '';\n  parsed = JSON.parse(String(raw).replace(/```json|```/g,'').trim());\n} catch (e) {\n  return [{ json: { parse_failed: true,\n    alert: '⚠️ Could not read an invoice — needs a human' } }];\n}\nconst po = $input.all().map(i => i.json);\nconst tolPct = 0.02;          // 2% price tolerance <- tune\nconst problems = [];\nlet invoiced = 0;\nfor (const l of (parsed.lines || [])) {\n  invoiced += Number(l.line_total || 0);\n  const key = String(l.sku || l.item || '').trim().toLowerCase();\n  const match = po.find(p => String(p.SKU ?? p.sku ?? '').trim().toLowerCase() === key\n                          || String(p.Item ?? p.item ?? '').trim().toLowerCase() === key);\n  if (!match) { problems.push(`❓ ${l.item} — not on any PO (${CURRENCY} ${l.line_total})`); continue; }\n  const poPrice = Number(match.UnitPrice ?? match.unit_price ?? match.Cost ?? 0);\n  const ip = Number(l.unit_price || 0);\n  if (poPrice && Math.abs(ip-poPrice)/poPrice > tolPct)\n    problems.push(`💰 ${l.item} — price ${CURRENCY} ${ip} vs agreed ${CURRENCY} ${poPrice} `\n      + `(+${(((ip-poPrice)/poPrice)*100).toFixed(1)}%)`);\n  const poQty = Number(match.Qty ?? match.qty ?? 0);\n  if (poQty && Number(l.qty) > poQty)\n    problems.push(`📦 ${l.item} — billed ${l.qty} vs ordered ${poQty}`);\n}\nconst declared = Number(parsed.total || 0);\nif (declared && Math.abs(invoiced - declared) > 0.05)\n  problems.push(`🧮 Lines total ${CURRENCY} ${invoiced.toFixed(3)} but invoice says ${CURRENCY} ${declared.toFixed(3)}`);\nlet m = `*INVOICE CHECK — ${parsed.supplier || 'unknown supplier'}*\\n`;\nm += `Inv ${parsed.invoice_no || '?'} · ${parsed.date || ''} · `\n   + `*${CURRENCY} ${declared || invoiced}*\\n\\n`;\nm += problems.length\n  ? problems.map(p=>'• '+p).join('\\n') + '\\n\\n⚠️ *Hold payment pending review.*'\n  : '✅ All lines match the PO. Safe to pay.';\nreturn [{ json: { alert: m, problem_count: problems.length,\n  invoice_no: parsed.invoice_no, supplier: parsed.supplier,\n  declared_total: declared,\n  needs_review: problems.length > 0 } }];"
      },
      "id": "358d5a4d-f64d-4ed6-aba1-8dbea377916c",
      "name": "Three-Way Match",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1250,
        100
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "review",
              "leftValue": "={{ $json.needs_review }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "cb1c361e-aac5-4fef-bd4e-ae480dd2f12f",
      "name": "Anything Wrong?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1500,
        100
      ]
    },
    {
      "parameters": {
        "chatId": "YOUR_FINANCE_CHAT_ID",
        "text": "={{ $json.alert }}",
        "additionalFields": {}
      },
      "id": "579009e0-e168-4738-a249-4faf4e00d87e",
      "name": "Alert Finance",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1760,
        40
      ]
    },
    {
      "parameters": {
        "chatId": "YOUR_FINANCE_CHAT_ID",
        "text": "={{ $json.alert }}",
        "additionalFields": {}
      },
      "id": "97289132-3547-4f3c-9f2f-1cf3953ddac5",
      "name": "File as OK",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1760,
        200
      ]
    }
  ],
  "connections": {
    "Watch Invoice Emails": {
      "main": [
        [
          {
            "node": "Get Attachment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Attachment": {
      "main": [
        [
          {
            "node": "Extract Invoice Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Invoice Text": {
      "main": [
        [
          {
            "node": "AI: Read the Invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI: Read the Invoice": {
      "main": [
        [
          {
            "node": "Read PO Records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read PO Records": {
      "main": [
        [
          {
            "node": "Three-Way Match",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Three-Way Match": {
      "main": [
        [
          {
            "node": "Anything Wrong?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anything Wrong?": {
      "main": [
        [
          {
            "node": "Alert Finance",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "File as OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "instanceId": "mauitechhub-retail-ops"
  }
}