{
  "name": "Invoice Follow-up — Daily Runner (A)",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * *"
            }
          ]
        }
      },
      "id": "schedule-trigger-1",
      "name": "Schedule Trigger 08:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        200,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT id, invoice_number, client_name, client_email, amount, currency, due_date, status, followup_count, drive_file_id, airtable_record_id FROM invoices WHERE status IN ('unpaid', 'final_notice') AND due_date < CURRENT_DATE AND followup_count < 4 AND (last_followup_at IS NULL OR last_followup_at < NOW() - INTERVAL '3 days')",
        "options": {}
      },
      "id": "pg-select-overdue-1",
      "name": "Postgres — Select Overdue Invoices",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        420,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "leftValue": "={{ $items().length }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ]
        },
        "options": {}
      },
      "id": "if-any-rows-1",
      "name": "IF — Any Rows?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        640,
        300
      ]
    },
    {
      "parameters": {
        "batchSize": 1,
        "options": {}
      },
      "id": "split-batches-1",
      "name": "Split In Batches",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        860,
        200
      ]
    },
    {
      "parameters": {
        "jsCode": "// Compute days overdue, pick tone/subject, build Groq prompt\nconst config = {\n  businessName:    $env.BUSINESS_NAME    || 'Artenis Web Studio',\n  ownerName:       $env.OWNER_NAME       || 'Artenis',\n  paymentMethods:  $env.PAYMENT_METHODS  || 'Bank transfer to IBAN AL00 0000 0000 0000 0000 or PayPal to studio@email.com',\n  lateFeeClause:   $env.LATE_FEE_CLAUSE  || 'As per our agreement, a 2% late fee applies after 14 days overdue.',\n  currency:        $env.CURRENCY         || 'EUR',\n  tonePerRound: [\n    'friendly and gentle — assume they forgot',\n    'polite but clear — mention the due date passed',\n    'firm and direct — reference the late fee clause',\n    'final notice — state this is the last reminder before escalation'\n  ],\n  subjectLines: [\n    'Friendly reminder — Invoice {{invoice_number}} ({{currency}} {{amount}})',\n    'Invoice {{invoice_number}} is now overdue',\n    'Action required — Invoice {{invoice_number}} {{days_overdue}} days overdue',\n    'FINAL NOTICE — Invoice {{invoice_number}}'\n  ],\n  overdueAlertDays: 30\n};\n\nconst row = $input.first().json;\nconst dueDate = new Date(row.due_date);\nconst today = new Date();\nconst daysOverdue = Math.floor((today - dueDate) / (1000 * 60 * 60 * 24));\nconst followupCount = row.followup_count || 0;\nconst round = Math.min(followupCount, 3);\n\nconst subject = config.subjectLines[round]\n  .replace('{{invoice_number}}', row.invoice_number)\n  .replace('{{currency}}', row.currency || config.currency)\n  .replace('{{amount}}', row.amount)\n  .replace('{{days_overdue}}', daysOverdue);\n\nconst prompt = `Write a payment reminder email from ${config.ownerName} at ${config.businessName}.\\n\\nInvoice details:\\n- Invoice number: ${row.invoice_number}\\n- Amount due: ${row.currency || config.currency} ${row.amount}\\n- Due date: ${row.due_date}\\n- Days overdue: ${daysOverdue}\\n- This is reminder number ${followupCount + 1} of 4\\n\\nTone instruction: ${config.tonePerRound[round]}\\nPayment options: ${config.paymentMethods}\\n${round >= 2 ? `Late fee note: ${config.lateFeeClause}` : ''}\\n\\nRules:\\n- Under 120 words\\n- Address the client by name: ${row.client_name}\\n- Do not apologise for sending the reminder\\n- Do not sound aggressive or threatening\\n- End with a clear call to action\\nReturn plain text only. No subject line. No sign-off.`;\n\nreturn {\n  ...row,\n  daysOverdue,\n  followupCount,\n  round,\n  subject,\n  prompt,\n  overdueAlertDays: config.overdueAlertDays\n};"
      },
      "id": "code-compute-1",
      "name": "Compute Days Overdue & Build Prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1080,
        200
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.groq.com/openai/v1/chat/completions",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: 'llama-3.3-70b-versatile', temperature: 0.3, max_tokens: 400, messages: [ { role: 'system', content: 'You write professional business emails. Plain text only. No markdown. Follow all instructions exactly.' }, { role: 'user', content: $json.prompt } ] }) }}",
        "options": {}
      },
      "id": "http-groq-1",
      "name": "Groq Generate Email",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1300,
        200
      ]
    },
    {
      "parameters": {
        "jsCode": "// Parse Groq response, strip any markdown fences\nconst input = $input.first().json;\nconst raw = input.choices?.[0]?.message?.content || '';\nconst emailBody = raw.replace(/```[a-z]*\\n?/gi, '').trim();\n// Merge with the original invoice row data from upstream\nconst row = $('Compute Days Overdue & Build Prompt').first().json;\nreturn { ...row, emailBody };"
      },
      "id": "code-parse-1",
      "name": "Parse Groq Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1520,
        200
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "leftValue": "={{ $json.drive_file_id }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              }
            }
          ]
        },
        "options": {}
      },
      "id": "if-has-pdf-1",
      "name": "IF — Has Drive File?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1740,
        200
      ]
    },
    {
      "parameters": {
        "operation": "download",
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.drive_file_id }}"
        },
        "options": {}
      },
      "id": "drive-download-1",
      "name": "Google Drive — Download Invoice PDF",
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        1960,
        100
      ]
    },
    {
      "parameters": {
        "sendTo": "={{ $json.client_email }}",
        "subject": "={{ $json.subject }}",
        "emailType": "text",
        "message": "={{ $json.emailBody }}",
        "options": {
          "attachmentsUi": {
            "attachmentsBinary": [
              {
                "property": "data"
              }
            ]
          }
        }
      },
      "id": "gmail-send-attached-1",
      "name": "Gmail — Send Reminder With Attachment",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        2180,
        100
      ]
    },
    {
      "parameters": {
        "sendTo": "={{ $json.client_email }}",
        "subject": "={{ $json.subject }}",
        "emailType": "text",
        "message": "={{ $json.emailBody }}",
        "options": {}
      },
      "id": "gmail-send-plain-1",
      "name": "Gmail — Send Reminder Without Attachment",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1960,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE invoices SET followup_count = followup_count + 1, last_followup_at = NOW(), status = CASE WHEN followup_count + 1 >= 3 THEN 'final_notice' ELSE status END WHERE id = $1 AND status != 'paid' RETURNING id, invoice_number, followup_count, status, airtable_record_id",
        "options": {
          "queryReplacement": "={{ $json.id }}"
        }
      },
      "id": "pg-update-1",
      "name": "Postgres — Update Followup Count",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        2400,
        200
      ]
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "=https://api.airtable.com/v0/{{ $env.AIRTABLE_BASE_ID }}/{{ $env.AIRTABLE_TABLE_NAME }}/{{ $json.airtable_record_id }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ fields: { status: $json.status, followup_count: $json.followup_count } }) }}",
        "options": {}
      },
      "id": "airtable-patch-1",
      "name": "Airtable — Sync Status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2620,
        200
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "leftValue": "={{ $('Compute Days Overdue & Build Prompt').first().json.daysOverdue }}",
              "rightValue": 30,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            },
            {
              "leftValue": "={{ $json.followup_count }}",
              "rightValue": 3,
              "operator": {
                "type": "number",
                "operation": "gte"
              }
            }
          ]
        },
        "options": {}
      },
      "id": "if-critical-1",
      "name": "IF — Critical Alert?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2840,
        200
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.GOTIFY_URL }}/message?token={{ $env.GOTIFY_TOKEN }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ title: 'Invoice critically overdue', message: 'Invoice ' + $json.invoice_number + ' reached round ' + $json.followup_count + '.', priority: 7 }) }}",
        "options": {}
      },
      "id": "gotify-overdue-1",
      "name": "Gotify — Overdue Alert",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3060,
        100
      ]
    }
  ],
  "connections": {
    "Schedule Trigger 08:00": {
      "main": [
        [
          {
            "node": "Postgres — Select Overdue Invoices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Postgres — Select Overdue Invoices": {
      "main": [
        [
          {
            "node": "IF — Any Rows?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF — Any Rows?": {
      "main": [
        [
          {
            "node": "Split In Batches",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Split In Batches": {
      "main": [
        [],
        [
          {
            "node": "Compute Days Overdue & Build Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute Days Overdue & Build Prompt": {
      "main": [
        [
          {
            "node": "Groq Generate Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Groq Generate Email": {
      "main": [
        [
          {
            "node": "Parse Groq Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Groq Response": {
      "main": [
        [
          {
            "node": "IF — Has Drive File?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF — Has Drive File?": {
      "main": [
        [
          {
            "node": "Google Drive — Download Invoice PDF",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Gmail — Send Reminder Without Attachment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Drive — Download Invoice PDF": {
      "main": [
        [
          {
            "node": "Gmail — Send Reminder With Attachment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail — Send Reminder With Attachment": {
      "main": [
        [
          {
            "node": "Postgres — Update Followup Count",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail — Send Reminder Without Attachment": {
      "main": [
        [
          {
            "node": "Postgres — Update Followup Count",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Postgres — Update Followup Count": {
      "main": [
        [
          {
            "node": "Airtable — Sync Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Airtable — Sync Status": {
      "main": [
        [
          {
            "node": "IF — Critical Alert?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF — Critical Alert?": {
      "main": [
        [
          {
            "node": "Gotify — Overdue Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Split In Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gotify — Overdue Alert": {
      "main": [
        [
          {
            "node": "Split In Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
