{
  "name": "DEMO — Invoice payment reminders (PauSolutions)",
  "nodes": [
    {
      "parameters": {
        "content": "## Invoice payment reminders — DEMO\nFictional web studio and invoices, no credentials.\n\n- 3 stages: friendly (3+ days overdue), firmer (10+), final (20+, flagged to the owner).\n- Paid invoices are skipped, so reminders stop by themselves.\n- Connect your invoice list and email, then enable the email node.\n\nInteractive version: pausolutions.com/demos/payment-reminders/\n\n_All dates in this demo are examples (sample \"today\": 21 Oct 2026). In production the workflow uses the real current date._",
        "height": 330,
        "width": 460
      },
      "id": "1286c532-bc20-4980-acb1-bc8b28a939be",
      "name": "About this demo",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -80,
        -340
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1,
                2,
                3,
                4,
                5
              ],
              "triggerAtHour": 9
            }
          ]
        }
      },
      "id": "f8aca091-8466-4000-a69b-d928f82ffd27",
      "name": "Every weekday at 9:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {},
      "id": "bf68561d-0e12-4000-a15f-2fa48490663b",
      "name": "Test run",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        200
      ]
    },
    {
      "parameters": {
        "jsCode": "// DEMO DATA — fictional invoices. In production, read your invoice list\n// (Google Sheets, Excel, or an export from your invoicing software).\nconst invoices = [\n  {\n    \"id\": \"i1\",\n    \"number\": \"INV-1101\",\n    \"client\": \"Oakridge Furniture Ltd (demo)\",\n    \"email\": \"accounts@oakridge.example\",\n    \"amount\": 480,\n    \"due\": \"2026-10-18\",\n    \"paid\": false,\n    \"stageSent\": 0\n  },\n  {\n    \"id\": \"i2\",\n    \"number\": \"INV-1097\",\n    \"client\": \"Green Leaf Café (demo)\",\n    \"email\": \"owner@greenleaf.example\",\n    \"amount\": 250,\n    \"due\": \"2026-10-08\",\n    \"paid\": false,\n    \"stageSent\": 1\n  },\n  {\n    \"id\": \"i3\",\n    \"number\": \"INV-1090\",\n    \"client\": \"Hartley & Co (demo)\",\n    \"email\": \"admin@hartley.example\",\n    \"amount\": 1200,\n    \"due\": \"2026-09-25\",\n    \"paid\": false,\n    \"stageSent\": 2\n  },\n  {\n    \"id\": \"i4\",\n    \"number\": \"INV-1099\",\n    \"client\": \"North Bikes (demo)\",\n    \"email\": \"info@northbikes.example\",\n    \"amount\": 320,\n    \"due\": \"2026-10-10\",\n    \"paid\": true,\n    \"stageSent\": 1\n  },\n  {\n    \"id\": \"i5\",\n    \"number\": \"INV-1104\",\n    \"client\": \"Bluebell Design (demo)\",\n    \"email\": \"hello@bluebell.example\",\n    \"amount\": 150,\n    \"due\": \"2026-11-05\",\n    \"paid\": false,\n    \"stageSent\": 0\n  }\n];\nreturn invoices.map((i) => ({ json: i }));"
      },
      "id": "13eaa950-6f75-4080-addd-d0e91be9c030",
      "name": "Invoices (demo data)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        100
      ]
    },
    {
      "parameters": {
        "jsCode": "// Same logic as the interactive demo on pausolutions.com/demos/ (rules + templates, no AI).\nconst TEXT = {\n  \"locale\": \"en-IE\",\n  \"firm\": \"Brightside Web Studio (demo)\",\n  \"stageNames\": [\n    \"Friendly\",\n    \"Firmer\",\n    \"Final\"\n  ],\n  \"subjects\": [\n    \"Friendly reminder: invoice {number} ({amount})\",\n    \"Second reminder: invoice {number} is {days} days overdue\",\n    \"Final reminder: invoice {number} ({amount})\"\n  ],\n  \"bodies\": [\n    \"Dear {client},\\n\\njust a friendly reminder that invoice {number} for {amount}, due on {due}, is still open. If you've already paid, please ignore this message. Thank you!\\n\\n{firm}\",\n    \"Dear {client},\\n\\ninvoice {number} for {amount} was due on {due} and is now {days} days overdue. Could you let us know when we can expect payment? If there's a problem with the invoice, just reply and we'll sort it out.\\n\\n{firm}\",\n    \"Dear {client},\\n\\ndespite our previous reminders, invoice {number} for {amount} (due on {due}) is still unpaid. Please arrange payment by {deadline}. If we don't hear from you, we'll call you to agree next steps.\\n\\n{firm}\"\n  ],\n  \"summarySubject\": \"Overdue invoices for the owner\"\n};\nconst STAGES = [3,10,20];\nconst today = \"2026-10-21\"; // Demo: fixed example date, matching the sample data. In production use: new Date().toISOString().slice(0, 10)\n\nfunction fill(tpl, vars) {\n  return tpl.replace(/\\{(\\w+)\\}/g, (m, k) => (k in vars ? String(vars[k]) : m));\n}\n\nfunction daysBetween(fromIso, toIso) {\n  return Math.round((Date.parse(toIso + \"T00:00:00Z\") - Date.parse(fromIso + \"T00:00:00Z\")) / 86400000);\n}\n\nfunction addDays(iso, days) {\n  return new Date(Date.parse(iso + \"T00:00:00Z\") + days * 86400000).toISOString().slice(0, 10);\n}\n\nfunction longDate(iso, locale) {\n  return new Intl.DateTimeFormat(locale, { day: \"numeric\", month: \"long\", year: \"numeric\", timeZone: \"UTC\" }).format(new Date(iso + \"T00:00:00Z\"));\n}\n\nfunction money(amount, locale) {\n  return new Intl.NumberFormat(locale, { style: \"currency\", currency: \"EUR\" }).format(amount);\n}\n\nfunction cleanText(value, max, multiline) {\n  const s = String(value ?? \"\").replace(multiline ? /[\\u0000-\\u0009\\u000B-\\u001F\\u007F]/g : /[\\u0000-\\u001F\\u007F]/g, \" \");\n  const collapsed = multiline ? s.replace(/[ \\t]+/g, \" \").replace(/\\n{3,}/g, \"\\n\\n\") : s.replace(/\\s+/g, \" \");\n  return collapsed.trim().slice(0, max);\n}\n\nfunction hasTerm(text, term, whole) {\n  if (!/^[\\p{L}\\p{N}]/u.test(term)) return text.includes(term);\n  const escaped = term.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n  return new RegExp(\"(^|[^\\\\p{L}\\\\p{N}])\" + escaped + (whole ? \"(?![\\\\p{L}\\\\p{N}])\" : \"\"), \"u\").test(text);\n}\n\nfunction planPayments(invoices, today, text, stages) {\n  return invoices.map((inv) => {\n    const daysOverdue = daysBetween(inv.due, today);\n    const stage = stages.filter((d) => daysOverdue >= d).length;\n    const base = { id: inv.id, invoice: inv.number, client: inv.client, email: inv.email, amount: inv.amount, daysOverdue, stage, stageName: null, flagOwner: false, subject: null, body: null };\n    if (inv.paid) return { ...base, status: \"paid\" };\n    if (daysOverdue <= 0) return { ...base, status: \"not-due\" };\n    if (stage === 0 || stage <= inv.stageSent) return { ...base, status: \"overdue\", flagOwner: inv.stageSent >= 3 };\n    const vars = { client: inv.client, number: inv.number, amount: money(inv.amount, text.locale), due: longDate(inv.due, text.locale), days: daysOverdue, firm: text.firm, deadline: longDate(addDays(today, 7), text.locale) };\n    return { ...base, status: \"email\", stageName: text.stageNames[stage - 1], flagOwner: stage === 3, subject: fill(text.subjects[stage - 1], vars), body: fill(text.bodies[stage - 1], vars) };\n  });\n}\n\nreturn planPayments($input.all().map((i) => i.json), today, TEXT, STAGES).map((o) => ({ json: o }));"
      },
      "id": "4add238f-2e6d-4c00-a4ea-5a369174e5b9",
      "name": "Pick reminder stage",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        100
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "a3450da0-338f-4c00-ad72-7419b23ead60",
              "leftValue": "={{ $json.status }}",
              "rightValue": "email",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "cb75d723-31ae-4c00-a542-ae9a7118d285",
      "name": "Only invoices due a reminder",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.2,
      "position": [
        740,
        0
      ],
      "notes": "Paid, not yet due and already-reminded invoices are skipped.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "fromEmail": "accounts@brightside.example",
        "toEmail": "={{ $json.email }}",
        "subject": "={{ $json.subject }}",
        "emailFormat": "text",
        "text": "={{ $json.body }}",
        "options": {}
      },
      "id": "dfb896c4-49fe-4000-a18f-ef7dfdb55c1c",
      "name": "Send payment reminder",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        980,
        0
      ],
      "disabled": true,
      "notes": "Disabled in the demo. Connect your own SMTP/Gmail/Outlook account, then enable.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "jsCode": "// Same logic as the interactive demo on pausolutions.com/demos/ (rules + templates, no AI).\nfunction paymentOverview(outcomes) {\n  const sum = (list) => ({ count: list.length, total: list.reduce((t, o) => t + o.amount, 0) });\n  const overdue = outcomes.filter((o) => o.status === \"overdue\" || o.status === \"email\");\n  return {\n    paid: sum(outcomes.filter((o) => o.status === \"paid\")),\n    notDue: sum(outcomes.filter((o) => o.status === \"not-due\")),\n    overdue: sum(overdue),\n    emails: outcomes.filter((o) => o.status === \"email\").length,\n    flaggedForOwner: outcomes.filter((o) => o.flagOwner).map((o) => o.invoice),\n  };\n}\n\nreturn [{ json: { subject: \"Overdue invoices for the owner\", ...paymentOverview($input.all().map((i) => i.json)) } }];"
      },
      "id": "ae2f3d66-e798-4800-a018-44df940b24ea",
      "name": "Overview for the owner",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        740,
        240
      ]
    }
  ],
  "pinData": {},
  "connections": {
    "Every weekday at 9:00": {
      "main": [
        [
          {
            "node": "Invoices (demo data)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Test run": {
      "main": [
        [
          {
            "node": "Invoices (demo data)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Invoices (demo data)": {
      "main": [
        [
          {
            "node": "Pick reminder stage",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick reminder stage": {
      "main": [
        [
          {
            "node": "Only invoices due a reminder",
            "type": "main",
            "index": 0
          },
          {
            "node": "Overview for the owner",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Only invoices due a reminder": {
      "main": [
        [
          {
            "node": "Send payment reminder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "ce445a76-2f5e-4000-a073-23cf8084435a",
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": [],
  "_demo": {
    "by": "PauSolutions",
    "note": "DEMO with fictional data. No credentials included. Email nodes are disabled until you connect your own account.",
    "tags": [
      "demo",
      "invoices"
    ]
  }
}
