{
  "name": "INV 01 - Daily Invoice Scanner and Reminder Planner",
  "nodes": [
    {
      "name": "Setup",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1100,
        -420
      ],
      "parameters": {
        "content": "## Installation settings\n1. Set one installation currency in **Installation Settings**.\n2. Set the real owner email before activation.\n3. Test with an email address you own.\n4. Google Sheets is active. QuickBooks is a disconnected adapter.",
        "height": 300,
        "width": 720,
        "color": 5
      }
    },
    {
      "name": "Manual Test Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -1040,
        -40
      ],
      "parameters": {}
    },
    {
      "name": "Daily 08:00 Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -1040,
        120
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * *"
            }
          ]
        }
      }
    },
    {
      "name": "Installation Settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -800,
        40
      ],
      "parameters": {
        "mode": "manual",
        "duplicateItem": false,
        "assignments": {
          "assignments": [
            {
              "id": "cfg1",
              "name": "installation_currency",
              "value": "USD",
              "type": "string"
            },
            {
              "id": "cfg2",
              "name": "owner_email",
              "value": "SET_OWNER_EMAIL_BEFORE_ACTIVATION",
              "type": "string"
            },
            {
              "id": "cfg3",
              "name": "approval_fallback_hours",
              "value": 4,
              "type": "number"
            }
          ]
        },
        "options": {}
      }
    },
    {
      "name": "Read Invoice Tracker",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -560,
        40
      ],
      "parameters": {
        "authentication": "oAuth2",
        "resource": "sheet",
        "operation": "read",
        "documentId": {
          "mode": "id",
          "value": "SPREADSHEET_ID"
        },
        "sheetName": {
          "mode": "id",
          "value": "INVOICES_SHEET_ID"
        },
        "filtersUI": {},
        "combineFilters": "AND",
        "options": {}
      }
    },
    {
      "name": "Read Reminder Log",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -320,
        40
      ],
      "parameters": {
        "authentication": "oAuth2",
        "resource": "sheet",
        "operation": "read",
        "documentId": {
          "mode": "id",
          "value": "SPREADSHEET_ID"
        },
        "sheetName": {
          "mode": "id",
          "value": "REMINDER_LOG_SHEET_ID"
        },
        "filtersUI": {},
        "combineFilters": "AND",
        "options": {}
      },
      "alwaysOutputData": true
    },
    {
      "name": "Read Approval Queue",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -80,
        40
      ],
      "parameters": {
        "authentication": "oAuth2",
        "resource": "sheet",
        "operation": "read",
        "documentId": {
          "mode": "id",
          "value": "SPREADSHEET_ID"
        },
        "sheetName": {
          "mode": "id",
          "value": "APPROVAL_QUEUE_SHEET_ID"
        },
        "filtersUI": {},
        "combineFilters": "AND",
        "options": {}
      },
      "alwaysOutputData": true
    },
    {
      "name": "Plan Next Reminder State",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        180,
        40
      ],
      "parameters": {
        "jsCode": "const invoices=$('Read Invoice Tracker').all().map(i=>i.json).slice(0,500);\nconst logs=$('Read Reminder Log').all().map(i=>i.json); const queue=$('Read Approval Queue').all().map(i=>i.json);\nconst cfg=$('Installation Settings').first().json; const now=new Date(); const day=86400000;\nconst done=new Set(logs.filter(x=>['SENT','SKIPPED','CANCELLED'].includes(String(x.event_status).toUpperCase())).map(x=>String(x.idempotency_key)));\nconst queued=new Set(queue.filter(x=>{const s=String(x.status).toUpperCase(); return ['PENDING','EDITING','APPROVED'].includes(s)||(s==='DEFERRED'&&x.defer_until&&new Date(x.defer_until)>now);}).map(x=>String(x.idempotency_key)));\nconst stages=[{name:'DAY_1_FRIENDLY',days:1,route:'AUTO'},{name:'DAY_7_FIRM',days:7,route:'APPROVAL'},{name:'DAY_14_URGENT',days:14,route:'APPROVAL'},{name:'DAY_30_FINAL',days:30,route:'APPROVAL'}];\nconst money=(v,c)=>new Intl.NumberFormat('en',{style:'currency',currency:c}).format(Number(v)||0); const out=[];\nfor(const inv of invoices){ const status=String(inv.status||'').toUpperCase(); const balance=Number(inv.balance_due||0); const due=new Date(inv.due_date); const currency=String(inv.currency||'').toUpperCase();\n if(!inv.invoice_id||!inv.customer_email||Number.isNaN(due.getTime())||balance<=0||['PAID','VOID','CANCELLED','DISPUTED'].includes(status)||String(inv.is_disputed).toLowerCase()==='true'||currency!==String(cfg.installation_currency).toUpperCase()) continue;\n const overdue=Math.floor((now-due)/day); if(overdue<1) continue;\n const selected=[...stages].reverse().find(s=>overdue>=s.days); if(!selected) continue;\n const key=`${inv.invoice_id}:${selected.name}`; if(done.has(key)||queued.has(key)) continue;\n const aid=`APR-${Date.now()}-${String(inv.invoice_id).replace(/[^a-z0-9]/gi,'').slice(-8)}-${selected.days}`;\n const subjects={DAY_1_FRIENDLY:`Friendly reminder: Invoice ${inv.invoice_number} is due`,DAY_7_FIRM:`Payment reminder: Invoice ${inv.invoice_number} is overdue`,DAY_14_URGENT:`Urgent: Invoice ${inv.invoice_number} remains outstanding`,DAY_30_FINAL:`Final reminder: Invoice ${inv.invoice_number}`};\n const tones={DAY_1_FRIENDLY:'This is a friendly reminder',DAY_7_FIRM:'This is a follow-up reminder',DAY_14_URGENT:'This is an urgent reminder',DAY_30_FINAL:'This is our final automated reminder'};\n const body=`Hello ${inv.customer_name||'there'},\\n\\n${tones[selected.name]} that invoice ${inv.invoice_number} for ${money(balance,currency)} was due on ${inv.due_date}.\\n\\n${inv.payment_link?`You can pay here: ${inv.payment_link}\\n`:''}${inv.payment_instructions||''}\\n\\nIf payment has already been made, please disregard this message or reply with the payment details.\\n\\nKind regards,\\nAccounts Team`;\n out.push({json:{...inv,stage:selected.name,route:selected.route,days_overdue:overdue,idempotency_key:key,approval_id:aid,proposed_subject:subjects[selected.name],proposed_body:body,final_subject:subjects[selected.name],final_body:body,status:'PENDING',created_at:now.toISOString(),fallback_due_at:new Date(now.getTime()+Number(cfg.approval_fallback_hours)*3600000).toISOString(),expires_at:new Date(now.getTime()+7*day).toISOString(),balance_snapshot:balance,currency,telegram_chat_id:'OWNER_CHAT_ID',last_updated_at:now.toISOString()}}); }\nreturn out;"
      }
    },
    {
      "name": "Is Automatic Day 1?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        760,
        40
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "condition-1",
              "leftValue": "={{ $json.route }}",
              "rightValue": "AUTO",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "name": "Live Re-read Invoices - Day 1",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1040,
        -140
      ],
      "parameters": {
        "authentication": "oAuth2",
        "resource": "sheet",
        "operation": "read",
        "documentId": {
          "mode": "id",
          "value": "SPREADSHEET_ID"
        },
        "sheetName": {
          "mode": "id",
          "value": "INVOICES_SHEET_ID"
        },
        "filtersUI": {},
        "combineFilters": "AND",
        "options": {}
      }
    },
    {
      "name": "Validate Day 1 Before Send",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1280,
        -140
      ],
      "parameters": {
        "jsCode": "const planned=$('Plan Next Reminder State').item.json; const live=$input.all().map(i=>i.json).find(x=>String(x.invoice_id)===String(planned.invoice_id));\nif(!live) return []; const bad=['PAID','VOID','CANCELLED','DISPUTED'].includes(String(live.status).toUpperCase())||String(live.is_disputed).toLowerCase()==='true'||Number(live.balance_due)<=0||String(live.customer_email)!==String(planned.customer_email);\nreturn bad?[]:[{json:{...planned,...live,balance_at_event:Number(live.balance_due),event_id:`EVT-${Date.now()}-${live.invoice_id}`,event_type:'REMINDER',event_status:'READY',subject:planned.proposed_subject,body:planned.proposed_body,owner_action:'AUTO',approval_id:'',provider:'SMTP',created_at:new Date().toISOString()}}];"
      }
    },
    {
      "name": "Send Day 1 via SMTP",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        1520,
        -140
      ],
      "parameters": {
        "resource": "email",
        "operation": "send",
        "fromEmail": "SENDER_NAME <VERIFIED_FROM_ADDRESS>",
        "toEmail": "={{ $json.customer_email }}",
        "subject": "={{ $json.subject }}",
        "emailFormat": "text",
        "text": "={{ $json.body }}",
        "options": {
          "appendAttribution": false
        }
      }
    },
    {
      "name": "Build Day 1 Log Row",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1760,
        -140
      ],
      "parameters": {
        "jsCode": "const p=$('Validate Day 1 Before Send').item.json; return [{json:{...p,event_status:'SENT',provider_message_id:$json.messageId??$json.id??'',error_summary:'',created_at:new Date().toISOString()}}];"
      }
    },
    {
      "name": "Append Day 1 Reminder Log",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 3,
      "position": [
        2000,
        -140
      ],
      "parameters": {
        "authentication": "oAuth2",
        "resource": "sheet",
        "operation": "append",
        "documentId": {
          "mode": "id",
          "value": "SPREADSHEET_ID"
        },
        "sheetName": {
          "mode": "id",
          "value": "REMINDER_LOG_SHEET_ID"
        },
        "dataMode": "autoMapInputData",
        "options": {
          "handlingExtraData": "ignoreIt"
        }
      }
    },
    {
      "name": "Upsert Approval Queue",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 3,
      "position": [
        1040,
        220
      ],
      "parameters": {
        "authentication": "oAuth2",
        "resource": "sheet",
        "operation": "appendOrUpdate",
        "documentId": {
          "mode": "id",
          "value": "SPREADSHEET_ID"
        },
        "sheetName": {
          "mode": "id",
          "value": "APPROVAL_QUEUE_SHEET_ID"
        },
        "dataMode": "autoMapInputData",
        "columnToMatchOn": "approval_id",
        "options": {
          "handlingExtraData": "ignoreIt"
        }
      }
    },
    {
      "name": "Send Approval Request",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1280,
        220
      ],
      "parameters": {
        "resource": "message",
        "operation": "sendMessage",
        "chatId": "OWNER_CHAT_ID",
        "text": "={{ `<b>INVOICE ESCALATION REVIEW</b>\\n\\nStage: ${$(\"Plan Next Reminder State\").item.json.stage}\\nCustomer: ${$(\"Plan Next Reminder State\").item.json.customer_name}\\nInvoice: ${$(\"Plan Next Reminder State\").item.json.invoice_number}\\nBalance: ${$(\"Plan Next Reminder State\").item.json.currency} ${$(\"Plan Next Reminder State\").item.json.balance_snapshot}\\nDays overdue: ${$(\"Plan Next Reminder State\").item.json.days_overdue}\\n\\n<b>Subject</b>\\n${$(\"Plan Next Reminder State\").item.json.proposed_subject}\\n\\n<b>Proposed reminder</b>\\n${$(\"Plan Next Reminder State\").item.json.proposed_body}` }}",
        "replyMarkup": "inlineKeyboard",
        "inlineKeyboard": {
          "rows": [
            {
              "row": {
                "buttons": [
                  {
                    "text": "✅ Approve",
                    "additionalFields": {
                      "callback_data": "={{ \"approve|\" + $(\"Plan Next Reminder State\").item.json.approval_id }}"
                    }
                  },
                  {
                    "text": "✏️ Edit",
                    "additionalFields": {
                      "callback_data": "={{ \"edit|\" + $(\"Plan Next Reminder State\").item.json.approval_id }}"
                    }
                  }
                ]
              }
            },
            {
              "row": {
                "buttons": [
                  {
                    "text": "⏭ Skip",
                    "additionalFields": {
                      "callback_data": "={{ \"skip|\" + $(\"Plan Next Reminder State\").item.json.approval_id }}"
                    }
                  },
                  {
                    "text": "🕒 Defer",
                    "additionalFields": {
                      "callback_data": "={{ \"defer|\" + $(\"Plan Next Reminder State\").item.json.approval_id }}"
                    }
                  }
                ]
              }
            }
          ]
        },
        "additionalFields": {
          "parse_mode": "HTML",
          "appendAttribution": false
        }
      }
    },
    {
      "name": "Save Telegram Message Context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1520,
        220
      ],
      "parameters": {
        "jsCode": "const plans=$('Plan Next Reminder State').all().map(i=>i.json);\nreturn $input.all().map((item,index)=>{\n const response=item.json.result??item.json;\n const buttons=response.reply_markup?.inline_keyboard?.flat?.()??[];\n const callback=buttons.map(b=>String(b.callback_data??'')).find(v=>v.includes('|'))??'';\n const approvalId=callback.split('|')[1]??'';\n const plan=plans.find(p=>String(p.approval_id)===approvalId)??plans.filter(p=>p.route==='APPROVAL')[index];\n if(!plan) throw new Error('Unable to match Telegram response to an approval item');\n return {json:{...plan,telegram_message_id:response.message_id??'',last_updated_at:new Date().toISOString()},pairedItem:{item:index}};\n});"
      }
    },
    {
      "name": "Update Queue Message ID",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 3,
      "position": [
        1760,
        220
      ],
      "parameters": {
        "authentication": "oAuth2",
        "resource": "sheet",
        "operation": "appendOrUpdate",
        "documentId": {
          "mode": "id",
          "value": "SPREADSHEET_ID"
        },
        "sheetName": {
          "mode": "id",
          "value": "APPROVAL_QUEUE_SHEET_ID"
        },
        "dataMode": "autoMapInputData",
        "columnToMatchOn": "approval_id",
        "options": {
          "handlingExtraData": "ignoreIt"
        }
      }
    },
    {
      "name": "QuickBooks Adapter Notes",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1060,
        470
      ],
      "parameters": {
        "content": "## OPTIONAL QUICKBOOKS ONLINE ADAPTER (DISCONNECTED)\nConnect the QuickBooks node to the mapper only after OAuth is configured. The mapper converts QuickBooks invoice fields into the same canonical schema as the Google Sheet. Keep only one source connected per installation.",
        "height": 300,
        "width": 800,
        "color": 6
      }
    },
    {
      "name": "QuickBooks Online - Get Invoices (Disconnected)",
      "type": "n8n-nodes-base.quickbooks",
      "typeVersion": 1,
      "position": [
        -760,
        820
      ],
      "parameters": {
        "resource": "invoice",
        "operation": "getAll",
        "returnAll": true,
        "filters": {}
      }
    },
    {
      "name": "Map QuickBooks to Canonical Invoice",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -440,
        820
      ],
      "parameters": {
        "jsCode": "return $input.all().map(({json:q})=>({json:{invoice_id:String(q.Id),invoice_number:q.DocNumber||String(q.Id),customer_name:q.CustomerRef?.name||'',customer_email:q.BillEmail?.Address||'',company_name:q.CustomerRef?.name||'',issue_date:q.TxnDate||'',due_date:q.DueDate||'',original_amount:Number(q.TotalAmt||0),balance_due:Number(q.Balance||0),status:Number(q.Balance||0)>0?'OPEN':'PAID',currency:q.CurrencyRef?.value||'',payment_terms:q.SalesTermRef?.name||'',payment_link:'',payment_instructions:'',owner_notes:'Imported from QuickBooks',is_disputed:false,source:'QUICKBOOKS',source_record_id:String(q.Id),last_synced_at:new Date().toISOString(),updated_at:new Date().toISOString()}}));"
      }
    }
  ],
  "connections": {
    "Manual Test Trigger": {
      "main": [
        [
          {
            "node": "Installation Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily 08:00 Trigger": {
      "main": [
        [
          {
            "node": "Installation Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Installation Settings": {
      "main": [
        [
          {
            "node": "Read Invoice Tracker",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Invoice Tracker": {
      "main": [
        [
          {
            "node": "Read Reminder Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Reminder Log": {
      "main": [
        [
          {
            "node": "Read Approval Queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Approval Queue": {
      "main": [
        [
          {
            "node": "Plan Next Reminder State",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Plan Next Reminder State": {
      "main": [
        [
          {
            "node": "Is Automatic Day 1?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Automatic Day 1?": {
      "main": [
        [
          {
            "node": "Live Re-read Invoices - Day 1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Upsert Approval Queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Live Re-read Invoices - Day 1": {
      "main": [
        [
          {
            "node": "Validate Day 1 Before Send",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Day 1 Before Send": {
      "main": [
        [
          {
            "node": "Send Day 1 via SMTP",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Day 1 via SMTP": {
      "main": [
        [
          {
            "node": "Build Day 1 Log Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Day 1 Log Row": {
      "main": [
        [
          {
            "node": "Append Day 1 Reminder Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert Approval Queue": {
      "main": [
        [
          {
            "node": "Send Approval Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Approval Request": {
      "main": [
        [
          {
            "node": "Save Telegram Message Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Telegram Message Context": {
      "main": [
        [
          {
            "node": "Update Queue Message ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "timezone": "TIMEZONE",
    "saveManualExecutions": true,
    "saveExecutionProgress": true,
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all",
    "executionTimeout": 300
  },
  "pinData": {},
  "tags": []
}
