{
  "id": "14M43oMMxpMzlaSF",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "RenewalFlow Intelligence",
  "tags": [],
  "nodes": [
    {
      "id": "5e2e4a16-b291-41dd-846d-754fd7b1fcfc",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        544,
        368
      ],
      "parameters": {
        "color": 7,
        "width": 704,
        "height": 240,
        "content": "### \ud83d\udea8 ERROR MONITORING\nIf credentials expire or Slack rate limits hit, this section fires an immediate administrative \nalert to prevent audit gaps."
      },
      "typeVersion": 1
    },
    {
      "id": "924b3593-c05c-4698-a6f9-12c34a47e26a",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1728,
        -160
      ],
      "parameters": {
        "color": 7,
        "width": 272,
        "height": 496,
        "content": "### 3. STATUS SYNC\nLogs the alert back to the spreadsheet  to ensure no duplicate messages are sent for the same renewal stage."
      },
      "typeVersion": 1
    },
    {
      "id": "40d0d02c-7c46-42dc-86a9-b85543325f9c",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1280,
        -160
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 496,
        "content": "### 2. NOTIFICATION LAYER\n\nRoutes the processed data. \nIf an alert is due,  it sends a formatted Slack message with  personalized UTM links and manual research steps."
      },
      "typeVersion": 1
    },
    {
      "id": "2698d004-da32-4d6e-918f-bc065750be37",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        544,
        -160
      ],
      "parameters": {
        "color": 7,
        "width": 704,
        "height": 496,
        "content": "### 1. DATA & LOGIC ENGINE\n The workflow wakes up daily, pulls your contract\ndatabase from Google Sheets, and runs the \nproprietary validation & alert logic."
      },
      "typeVersion": 1
    },
    {
      "id": "2777381b-db5a-430c-9843-0781e8ee2958",
      "name": "README: Production Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -32,
        -160
      ],
      "parameters": {
        "width": 544,
        "height": 796,
        "content": "# RenewalFlow Intelligence\n\n**HOW IT WORKS:**\n\nRuns daily at 9 AM, sends 4 alerts per contract:\n\u2022 Day 45: Research (\ud83d\udfe1 REMINDER)\n\u2022 Day 30: Quotes (\ud83d\udfe0 ATTENTION)\n\u2022 Day 14: Decision (\ud83d\udd34 URGENT)\n\u2022 Day 7: Action (\ud83d\udea8 FINAL NOTICE)\n\n**CATCH-UP LOGIC:**\nIf workflow misses runs (weekend/outage),\nalerts fire within 5-day window of each stage.\n\n##  SETUP CHECKLIST\n\n**BEFORE FIRST RUN:**\n\n- [ ] **Google Sheets:**\n  \u2022 Get Spreadsheet ID\n  \u2022 Replace YOUR_SPREADSHEET_ID in Load Contracts node\n  \u2022 Set up OAuth2 credentials in n8n\n  \u2022 Share sheet with n8n service account\n\n- [ ] **Sheet Structure (Columns A-F):**\n  A: Contract Name (text)\n  B: Vendor Name (text)\n  C: Renewal Date (YYYY-MM-DD format)\n  D: Current Annual Cost (number, no \u20ac symbol)\n  E: Vendor Pricing URL (https://...)\n  F: Status (Active, Renewed, or Cancelled)\n\n- [ ] **Slack:**\n  \u2022 Create #contract-renewals channel\n  \u2022 Required scopes: chat:write, chat:write.public\n  \u2022 Add bot to #contract-renewals channel\n  \u2022 Configure Slack credentials in all Slack nodes\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "07ab785a-57b7-47e6-9ab7-7c39067c66aa",
      "name": "Slack: Error Alert",
      "type": "n8n-nodes-base.slack",
      "position": [
        864,
        448
      ],
      "parameters": {
        "text": "\ud83d\udea8 *WORKFLOW ERROR: Contract Renewal Monitor*\n\n*Error Message:*\n{{ $json.error.message }}\n\n*Failed Node:*\n{{ $json.error.node.name }}\n\n*Timestamp:*\n{{ $now.toISO() }}\n\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n*Common Fixes:*\n\u2022 Google Sheets credentials expired \u2192 Reconnect OAuth\n\u2022 Slack bot removed from channel \u2192 Re-add bot to #contract-renewals\n\u2022 Workflow deactivated \u2192 Toggle 'Active' back on\n\u2022 Invalid date in sheet \u2192 Check YYYY-MM-DD format\n\n*Check Execution Logs:*\nWorkflow Executions \u2192 Latest Failed Run \u2192 View Details\n\n_Your contract monitoring is currently down. Fix immediately._",
        "otherOptions": {}
      },
      "typeVersion": 2.2
    },
    {
      "id": "26eab4af-a1ab-42f2-8f28-1622cda78570",
      "name": "Error Trigger",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        640,
        448
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "b2a53ed0-e735-4997-b671-450237dcd3d5",
      "name": "Google Sheets: Update Status",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1760,
        -32
      ],
      "parameters": {
        "columns": {
          "value": {
            "Status": "={{ $json.status }}, {{ $json._alertStage }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Google Sheets: Load Contracts').params.documentId.value }}"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "ea9a09ae-ccd0-47be-9a2f-bfc783bf6dcf",
      "name": "Slack: Daily Summary",
      "type": "n8n-nodes-base.slack",
      "position": [
        1536,
        160
      ],
      "parameters": {
        "text": "={{ $json.summaryMsg }}",
        "otherOptions": {}
      },
      "typeVersion": 2.2
    },
    {
      "id": "5a157762-b92f-44bb-8015-91694809db57",
      "name": "Slack: Send Alert",
      "type": "n8n-nodes-base.slack",
      "position": [
        1536,
        -32
      ],
      "parameters": {
        "text": "*{{ $json.urgency }} RENEWAL ALERT: {{ $json.contractName }}*\n\n\u26a1 *SKIP THE RESEARCH?*\nThis \u20ac{{ $json.currentCost.toLocaleString('en-US') }}/year contract could save you \u20ac{{ $json.potentialSavingsMin.toLocaleString('en-US') }}-\u20ac{{ $json.potentialSavingsMax.toLocaleString('en-US') }} with better negotiation.\n\nUpgrade to Premium for AI-powered price scraping and GPT-4o negotiation brief.\n\ud83d\udc49 <https://lemonsqueezy.com/checkout?utm_source=slack&utm_medium=alert&utm_campaign={{ $json.contractSlug }}&utm_content={{ $json._alertStage }}|*Unlock Analysis - \u20ac99*>\n\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n*Contract Details:*\n\u2022 *Vendor:* {{ $json.vendorName }}\n\u2022 *Renewal Date:* {{ $json.renewalDate }}\n\u2022 *Days Remaining:* {{ $json.daysUntilRenewal }}\n\u2022 *Annual Cost:* \u20ac{{ $json.currentCost.toLocaleString('en-US') }}\n\n*\ud83d\udccb MANUAL STEPS ({{ $json._alertStage }}):*\n{{ $json._alertMessage }}\n1. Visit: {{ $json.pricingUrl }}\n2. Search: \"{{ $json.vendorName }} alternatives 2026\"\n3. Email account manager for renewal quote.\n\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n_FREE tier - manual research required_",
        "otherOptions": {}
      },
      "typeVersion": 2.2
    },
    {
      "id": "f8cd0a7d-601f-428a-9087-f0282bae6af5",
      "name": "IF: Is Alert?",
      "type": "n8n-nodes-base.if",
      "position": [
        1312,
        64
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json._isAlert }}",
              "value2": true
            }
          ],
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "436c4d37-5745-419d-9cad-79d8ad015f9d",
      "name": "Brain: Validate & Route",
      "type": "n8n-nodes-base.code",
      "position": [
        1088,
        64
      ],
      "parameters": {
        "jsCode": "const alertStages = [\n  { days: 45, stage: 'alerted_45d', urgency: '\ud83d\udfe1 REMINDER', msg: '\ud83d\udcc5 Research Phase: Look for alternatives.' },\n  { days: 30, stage: 'alerted_30d', urgency: '\ud83d\udfe0 ATTENTION', msg: '\ud83d\udcb0 Quote Phase: Get competitor pricing.' },\n  { days: 14, stage: 'alerted_14d', urgency: '\ud83d\udd34 URGENT', msg: '\ud83c\udfaf Decision Phase: Finalize negotiation.' },\n  { days: 7, stage: 'alerted_7d', urgency: '\ud83d\udea8 FINAL NOTICE', msg: '\u26a0\ufe0f Action Phase: Confirm or Cancel.' }\n];\n\nconst now = new Date();\nconst results = [];\nconst processed = new Set();\n\nfor (const item of items) {\n  const data = item.json;\n  const status = data['Status'] || 'Active';\n  const rawDate = data['Renewal Date'];\n\n  // Skip invalid/completed contracts\n  if (!rawDate || status.includes('Cancelled') || status.includes('Renewed')) continue;\n\n  // Deduplication\n  const contractId = `${data['Contract Name']}-${rawDate}`;\n  if (processed.has(contractId)) continue;\n\n  // VALIDATION\n  const contractName = data['Contract Name']?.trim();\n  if (!contractName) continue;\n  \n  const currentCost = Number(data['Current Annual Cost']);\n  if (isNaN(currentCost) || currentCost < 0) continue;\n  \n  const url = data['Vendor Pricing URL'];\n  if (!url || (!url.startsWith('http://') && !url.startsWith('https://'))) continue;\n\n  // DATE CALCULATION (using standard JS Date)\n  const renewalDate = new Date(rawDate);\n  const diffInMs = renewalDate - now;\n  const daysUntil = Math.ceil(diffInMs / (1000 * 60 * 60 * 24));\n\n  // Skip expired or invalid dates\n  if (isNaN(daysUntil) || daysUntil < 0) continue;\n\n  // SLUG FOR UTM (deterministic fallback using row number)\n  let slug = contractName\n    .normalize('NFD')\n    .replace(/[\\u0300-\\u036f]/g, '')\n    .toLowerCase()\n    .replace(/[^a-z0-9]+/g, '-')\n    .replace(/^-+|-+$/g, '')\n    .substring(0, 50);\n  \n  // Fallback: use row number for consistency (not timestamp)\n  if (!slug) slug = `contract-row-${data.rowNumber || 'unknown'}`;\n\n  // MULTI-STAGE FILTER WITH CATCH-UP WINDOW\n  for (const alert of alertStages) {\n    // Window: If within alert.days OR slightly past it (catch-up for missed runs)\n    const inWindow = daysUntil <= alert.days && daysUntil > (alert.days - 5);\n    const notYetAlerted = !status.includes(alert.stage);\n\n    if (inWindow && notYetAlerted) {\n      processed.add(contractId);\n      \n      results.push({\n        json: {\n          contractName: contractName,\n          vendorName: data['Vendor Name'] || 'Unknown Vendor',\n          renewalDate: rawDate,\n          currentCost: currentCost,\n          pricingUrl: url,\n          status: status,\n          rowNumber: data.rowNumber,\n          daysUntilRenewal: daysUntil,\n          _alertStage: alert.stage,\n          _alertMessage: alert.msg,\n          urgency: alert.urgency,\n          contractSlug: slug,\n          _isAlert: true,\n          potentialSavingsMin: Math.round(currentCost * 0.15),\n          potentialSavingsMax: Math.round(currentCost * 0.25)\n        }\n      });\n      break;\n    }\n  }\n}\n\n// EMPTY RESULT HANDLER\nif (results.length === 0) {\n  return [{ \n    json: { \n      _isAlert: false, \n      summaryMsg: `\u2705 Daily renewal check complete.\\n\\nReviewed ${items.length} contract${items.length !== 1 ? 's' : ''}.\\nNo renewals in alert windows (45d, 30d, 14d, 7d).` \n    } \n  }];\n}\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "bb64915b-7e7d-496b-a375-55727079a89c",
      "name": "Google Sheets: Load Contracts",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        864,
        64
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_SPREADSHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "4f5e6780-c8ea-4fe5-84c3-5a6a7d60254f",
      "name": "Schedule: Daily 9AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "notes": "Runs at 9 AM in your configured timezone",
      "position": [
        640,
        64
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9 * * *"
            }
          ]
        }
      },
      "typeVersion": 1.2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "9d208650-c7a9-4f7f-b471-9943b3f04c14",
  "connections": {
    "Error Trigger": {
      "main": [
        [
          {
            "node": "Slack: Error Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: Is Alert?": {
      "main": [
        [
          {
            "node": "Slack: Send Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack: Daily Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack: Send Alert": {
      "main": [
        [
          {
            "node": "Google Sheets: Update Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule: Daily 9AM": {
      "main": [
        [
          {
            "node": "Google Sheets: Load Contracts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Brain: Validate & Route": {
      "main": [
        [
          {
            "node": "IF: Is Alert?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets: Load Contracts": {
      "main": [
        [
          {
            "node": "Brain: Validate & Route",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}