{
  "name": "DSB Sales Late Sweep",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 7,13,20 * * 1-6"
            }
          ]
        }
      },
      "id": "ls-trigger",
      "name": "Schedule (7am, 1pm, 8pm CST)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        -2400,
        800
      ]
    },
    {
      "parameters": {
        "jsCode": "// Compute past N business days (excluding weekends) ending today (CST).\nconst N = 7;\nconst central = new Date().toLocaleDateString('en-CA', { timeZone: 'America/Chicago' });\nconst [y, m, d] = central.split('-').map(Number);\nconst end = new Date(y, m - 1, d);\nconst dates = [];\nconst cur = new Date(end);\nwhile (dates.length < N) {\n  const day = cur.getDay();\n  if (day !== 0 && day !== 6) {\n    const ds = cur.getFullYear() + '-' + String(cur.getMonth() + 1).padStart(2, '0') + '-' + String(cur.getDate()).padStart(2, '0');\n    dates.push(ds);\n  }\n  cur.setDate(cur.getDate() - 1);\n}\ndates.reverse();\nreturn [{ json: { backfillDates: dates } }];"
      },
      "id": "ls-dates",
      "name": "Compute Backfill Dates",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -2160,
        800
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.apify.com/v2/acts/MIlgC3KEFTXzIcnMt/runs?token=YOUR_APIFY_TOKEN",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ crmUsername: \"YOUR_CRM_USERNAME\", crmPassword: \"YOUR_CRM_PASSWORD\", salesBackfillDates: $json.backfillDates }) }}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "ls-start-apify",
      "name": "Start Apify Backfill",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -1920,
        800
      ]
    },
    {
      "parameters": {
        "amount": 90,
        "unit": "seconds"
      },
      "id": "ls-wait1",
      "name": "Wait 90s",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        -1680,
        800
      ]
    },
    {
      "parameters": {
        "url": "=https://api.apify.com/v2/actor-runs/{{ $node['Start Apify Backfill'].json.data.id }}?token=YOUR_APIFY_TOKEN",
        "options": {
          "timeout": 10000
        }
      },
      "id": "ls-poll",
      "name": "Poll Run Status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -1440,
        800
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "is-done",
              "leftValue": "={{ $json.data.status }}",
              "rightValue": "SUCCEEDED",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "ls-done",
      "name": "Run Done?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -1200,
        800
      ]
    },
    {
      "parameters": {
        "amount": 60,
        "unit": "seconds"
      },
      "id": "ls-wait2",
      "name": "Wait & Retry",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        -1200,
        1040
      ]
    },
    {
      "parameters": {
        "jsCode": "const runStatus = $input.first().json;\nconst datasetId = runStatus?.data?.defaultDatasetId;\nconst runId = runStatus?.data?.id || 'unknown';\nif (!datasetId) {\n  return [{ json: { error: true, message: 'No defaultDatasetId. runId=' + runId } }];\n}\nconst token = 'YOUR_APIFY_TOKEN';\nconst url = `https://api.apify.com/v2/datasets/${datasetId}/items?token=${token}`;\nconst raw = await this.helpers.httpRequest({ method: 'GET', url, encoding: 'utf-8', returnFullResponse: false, timeout: 60000, headers: { Accept: 'application/json' } });\nlet items = raw;\nif (typeof raw === 'string') { try { items = JSON.parse(raw); } catch (e) { return [{ json: { error: true, message: 'JSON parse failed: ' + raw.substring(0,200) } }]; } }\nif (!Array.isArray(items) || items.length === 0) {\n  return [{ json: { error: true, message: 'Empty dataset for runId=' + runId, datasetId } }];\n}\nconst backfills = items.filter((i) => i._type === 'sale_made_backfill');\nreturn [{ json: { backfills, runId, datasetId, count: backfills.length } }];"
      },
      "id": "ls-fetch",
      "name": "Fetch Dataset",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -960,
        800
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "has-data",
              "leftValue": "={{ $json.error }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "false"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "ls-data-ok",
      "name": "Data OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -720,
        800
      ]
    },
    {
      "parameters": {
        "jsCode": "// Split each backfill item into its own n8n item so the next HTTP node fans out per date.\nconst backfills = $json.backfills || [];\nreturn backfills.map((b) => ({ json: { scrape_date: b.scrape_date, agents: b.agents || [], mode: 'sales_only' } }));"
      },
      "id": "ls-split",
      "name": "Split Per Date",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -480,
        720
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_SUPABASE_REF.supabase.co/functions/v1/ingest-daily-scrape",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "Bearer YOUR_ROLI_ANON_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ scrape_date: $json.scrape_date, agents: $json.agents, mode: 'sales_only' }) }}",
        "options": {
          "timeout": 30000
        }
      },
      "id": "ls-ingest",
      "name": "Ingest sales_only",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -240,
        720
      ]
    },
    {
      "parameters": {
        "mode": "combine",
        "combineBy": "combineAll",
        "options": {}
      },
      "id": "ls-merge",
      "name": "Aggregate Results",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        0,
        720
      ]
    },
    {
      "parameters": {
        "jsCode": "// Build a Slack summary of all backfilled dates and any deltas detected.\nconst items = $input.all().map((i) => i.json);\nlet totalUpdated = 0;\nlet totalInserted = 0;\nlet totalAgents = 0;\nconst lines = [];\nfor (const r of items) {\n  if (!r) continue;\n  const date = r.scrape_date || 'unknown';\n  const updated = r.sales_only_updated || 0;\n  const inserted = r.sales_only_inserted || 0;\n  const ups = r.upserted || 0;\n  totalUpdated += updated;\n  totalInserted += inserted;\n  totalAgents += ups;\n  lines.push(`\u2022 *${date}* \u2014 ${ups} agents (updated: ${updated}, inserted: ${inserted})`);\n}\nconst now = new Date();\nconst centralStr = now.toLocaleString('en-US', { timeZone: 'America/Chicago', hour: 'numeric', minute: '2-digit', hour12: true });\nconst message = {\n  text: `DSB sales late sweep complete (${items.length} dates)`,\n  blocks: [\n    { type: 'header', text: { type: 'plain_text', text: ':arrows_counterclockwise: DSB Sales Late Sweep', emoji: true } },\n    { type: 'section', text: { type: 'mrkdwn', text: `*Sweep run at ${centralStr} CST*\\n${items.length} dates re-scraped (Sale Made only)\\n*Total:* ${totalAgents} agents | ${totalUpdated} updated | ${totalInserted} inserted` } },\n    { type: 'divider' },\n    { type: 'section', text: { type: 'mrkdwn', text: lines.join('\\n') || '_No data_' } },\n    { type: 'context', elements: [{ type: 'mrkdwn', text: 'Sales-only mode preserves dials/talk/leads. Catches sales entered after the daily scrape ran.' }] }\n  ]\n};\nreturn [{ json: message }];"
      },
      "id": "ls-summary",
      "name": "Build Slack Summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        720
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "YOUR_SLACK_INBOUND_WEBHOOK_URL",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "ls-slack",
      "name": "Send to Slack",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        480,
        720
      ]
    }
  ],
  "connections": {
    "Schedule (7am, 1pm, 8pm CST)": {
      "main": [
        [
          {
            "node": "Compute Backfill Dates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute Backfill Dates": {
      "main": [
        [
          {
            "node": "Start Apify Backfill",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start Apify Backfill": {
      "main": [
        [
          {
            "node": "Wait 90s",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 90s": {
      "main": [
        [
          {
            "node": "Poll Run Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Poll Run Status": {
      "main": [
        [
          {
            "node": "Run Done?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Done?": {
      "main": [
        [
          {
            "node": "Fetch Dataset",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait & Retry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait & Retry": {
      "main": [
        [
          {
            "node": "Poll Run Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Dataset": {
      "main": [
        [
          {
            "node": "Data OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Data OK?": {
      "main": [
        [
          {
            "node": "Split Per Date",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Split Per Date": {
      "main": [
        [
          {
            "node": "Ingest sales_only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ingest sales_only": {
      "main": [
        [
          {
            "node": "Aggregate Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Results": {
      "main": [
        [
          {
            "node": "Build Slack Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Slack Summary": {
      "main": [
        [
          {
            "node": "Send to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": true
  }
}