{
  "id": "StripePayoutRecon1",
  "name": "Stripe Payout Reconciliation (synthetic demo)",
  "nodes": [
    {
      "id": "manual-trigger",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "parameters": {}
    },
    {
      "id": "read-payouts",
      "name": "Read Payouts",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1,
      "position": [
        200,
        0
      ],
      "parameters": {
        "fileSelector": "/workspace/portfolio/stripe-payout-recon/data/stripe_payouts.csv",
        "options": {}
      },
      "executeOnce": true
    },
    {
      "id": "parse-payouts",
      "name": "Parse Payouts",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [
        400,
        0
      ],
      "parameters": {
        "options": {
          "headerRow": true
        }
      }
    },
    {
      "id": "read-balance-txns",
      "name": "Read Balance Txns",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1,
      "position": [
        600,
        0
      ],
      "parameters": {
        "fileSelector": "/workspace/portfolio/stripe-payout-recon/data/stripe_balance_transactions.csv",
        "options": {}
      },
      "executeOnce": true
    },
    {
      "id": "parse-balance-txns",
      "name": "Parse Balance Txns",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [
        800,
        0
      ],
      "parameters": {
        "options": {
          "headerRow": true
        }
      }
    },
    {
      "id": "read-bank-statement",
      "name": "Read Bank Statement",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1,
      "position": [
        1000,
        0
      ],
      "parameters": {
        "fileSelector": "/workspace/portfolio/stripe-payout-recon/data/bank_statement.csv",
        "options": {}
      },
      "executeOnce": true
    },
    {
      "id": "parse-bank-statement",
      "name": "Parse Bank Statement",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [
        1200,
        0
      ],
      "parameters": {
        "options": {
          "headerRow": true
        }
      }
    },
    {
      "id": "reconcile",
      "name": "Reconcile",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1400,
        0
      ],
      "parameters": {
        "jsCode": "// Reconciliation logic used inside the n8n Code node \"Reconcile\".\nconst DATE_WINDOW = 2, TIMING_WINDOW = 7;\nconst num = v => Math.round(parseFloat(v) * 100);           // cents\nconst day = s => Math.round(new Date(s + 'T00:00:00Z').getTime() / 864e5);\nconst payouts = $('Parse Payouts').all().map(i => i.json);\nconst bts = $('Parse Balance Txns').all().map(i => i.json);\nconst bank = $('Parse Bank Statement').all().map(i => i.json)\n  .filter(b => /STRIPE/i.test(b.description) && num(b.amount) > 0);\nconst used = new Set(), rows = [];\nconst find = (p, pred, win) => bank.filter(b => !used.has(b.bank_txn_id) && pred(b) && Math.abs(day(b.date) - day(p.arrival_date)) <= win)\n  .sort((a, b) => Math.abs(day(a.date) - day(p.arrival_date)) - Math.abs(day(b.date) - day(p.arrival_date)))[0];\nfor (const p of payouts.sort((a, b) => a.arrival_date.localeCompare(b.arrival_date))) {\n  const amt = num(p.amount), fee = num(p.fee);\n  const ledgerNet = bts.filter(t => t.payout_id === p.payout_id && t.type === 'charge').reduce((s, t) => s + num(t.net), 0);\n  let status, reason, b;\n  if ((b = find(p, x => num(x.amount) === amt, DATE_WINDOW))) { status = 'MATCHED'; reason = 'Exact amount within \u00b1' + DATE_WINDOW + ' days'; }\n  else if ((b = find(p, x => num(x.amount) === amt, TIMING_WINDOW))) { status = 'TIMING_DIFFERENCE'; reason = `Exact amount but deposit ${day(b.date) - day(p.arrival_date)} days from arrival date`; }\n  else if ((b = find(p, x => Math.abs(num(x.amount) - amt) === fee, DATE_WINDOW))) { status = 'AMOUNT_OFF_BY_FEE'; reason = `Bank amount differs from payout by exactly the fee (${p.fee})`; }\n  else { status = 'MISSING_DEPOSIT'; reason = 'No bank credit found for this payout'; }\n  if (ledgerNet !== amt) { status = status === 'MATCHED' ? 'LEDGER_MISMATCH' : status; reason += `; balance txns net ${(ledgerNet/100).toFixed(2)} != payout`; }\n  if (b) used.add(b.bank_txn_id);\n  rows.push({ payout_id: p.payout_id, payout_date: p.arrival_date, payout_amount: p.amount, fee: p.fee,\n    bank_txn_id: b ? b.bank_txn_id : '', bank_date: b ? b.date : '', bank_amount: b ? b.amount : '',\n    difference: b ? ((num(b.amount) - amt) / 100).toFixed(2) : (-amt / 100).toFixed(2),\n    status, exception: status === 'MATCHED' ? 'no' : 'yes', reason });\n}\nfor (const b of bank.filter(b => !used.has(b.bank_txn_id))) {\n  const twin = rows.find(r => r.bank_amount === b.amount && Math.abs(day(r.bank_date) - day(b.date)) <= DATE_WINDOW);\n  rows.push({ payout_id: twin ? twin.payout_id : '', payout_date: '', payout_amount: '', fee: '',\n    bank_txn_id: b.bank_txn_id, bank_date: b.date, bank_amount: b.amount, difference: b.amount,\n    status: twin ? 'DUPLICATE_DEPOSIT' : 'UNMATCHED_BANK_CREDIT', exception: 'yes',\n    reason: twin ? `Second bank credit for ${twin.payout_id} (already matched to ${twin.bank_txn_id})` : 'Stripe-labelled bank credit with no payout' });\n}\nreturn rows.map(json => ({ json }));\n"
      },
      "executeOnce": true
    },
    {
      "id": "report-to-csv",
      "name": "Report to CSV",
      "type": "n8n-nodes-base.convertToFile",
      "typeVersion": 1.1,
      "position": [
        1600,
        -120
      ],
      "parameters": {
        "options": {
          "fileName": "reconciliation_report.csv"
        }
      }
    },
    {
      "id": "write-report",
      "name": "Write Report",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1,
      "position": [
        1800,
        -120
      ],
      "parameters": {
        "operation": "write",
        "fileName": "/workspace/portfolio/stripe-payout-recon/output/reconciliation_report.csv",
        "options": {}
      }
    },
    {
      "id": "build-summary",
      "name": "Build Summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1600,
        120
      ],
      "parameters": {
        "jsCode": "const rows = $input.all().map(i => i.json);\nconst by = {}; rows.forEach(r => by[r.status] = (by[r.status] || 0) + 1);\nconst ex = rows.filter(r => r.exception === 'yes');\nconst lines = ['# Payout reconciliation summary (SYNTHETIC DEMO DATA)', '',\n  `Run at: ${new Date().toISOString()}`, `Rows: ${rows.length} | Exceptions: ${ex.length}`, '', '## Counts by status',\n  ...Object.entries(by).map(([k, v]) => `- ${k}: ${v}`), '', '## Exceptions',\n  ...ex.map(r => `- ${r.payout_id || '(none)'} / ${r.bank_txn_id || '(no bank line)'}: ${r.status} - ${r.reason}`)];\nconst text = lines.join('\\n') + '\\n';\nreturn [{ json: { summary: text, counts: by, exceptions: ex.length },\n  binary: { data: { data: Buffer.from(text).toString('base64'), mimeType: 'text/markdown', fileName: 'summary.md', fileExtension: 'md' } } }];\n"
      }
    },
    {
      "id": "write-summary",
      "name": "Write Summary",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1,
      "position": [
        1800,
        120
      ],
      "parameters": {
        "operation": "write",
        "fileName": "/workspace/portfolio/stripe-payout-recon/output/summary.md",
        "options": {}
      }
    },
    {
      "id": "optional:-llm-explain-exceptions",
      "name": "Optional: LLM explain exceptions",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1560,
        300
      ],
      "parameters": {
        "content": "## Optional LLM step (not wired)\nAdd an AI node (e.g. a local Ollama model via the Ollama Chat Model node) after **Build Summary** to turn exception rows into plain-English notes. Left out so the demo needs no API key.",
        "width": 380,
        "height": 180
      }
    }
  ],
  "connections": {
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Read Payouts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Payouts": {
      "main": [
        [
          {
            "node": "Parse Payouts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Payouts": {
      "main": [
        [
          {
            "node": "Read Balance Txns",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Balance Txns": {
      "main": [
        [
          {
            "node": "Parse Balance Txns",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Balance Txns": {
      "main": [
        [
          {
            "node": "Read Bank Statement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Bank Statement": {
      "main": [
        [
          {
            "node": "Parse Bank Statement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Bank Statement": {
      "main": [
        [
          {
            "node": "Reconcile",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reconcile": {
      "main": [
        [
          {
            "node": "Report to CSV",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Report to CSV": {
      "main": [
        [
          {
            "node": "Write Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Summary": {
      "main": [
        [
          {
            "node": "Write Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {}
}