{
  "name": "Conciliación de pagos asistida por IA (plano de Saphia Labs)",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 6 * * *"
            }
          ]
        }
      },
      "id": "tr",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "TU_SPREADSHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "pagos_procesador"
        },
        "options": {}
      },
      "id": "rp",
      "name": "Sheets: reporte del procesador",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        260,
        -100
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "TU_SPREADSHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "pagos_internos"
        },
        "options": {}
      },
      "id": "ri",
      "name": "Sheets: registro interno",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        260,
        100
      ],
      "executeOnce": true
    },
    {
      "parameters": {
        "jsCode": "// Cruce determinista: misma referencia y mismo monto, estado aprobado/pagado. Esto NO lo hace la IA.\nconst n = v => Number(String(v ?? '').replace(/[^\\d.-]/g, '')) || 0;\nconst proc = $('Sheets: reporte del procesador').all().map(i => i.json).filter(r => r.referencia && r.referencia !== 'referencia');\nconst inte = $input.all().map(i => i.json).filter(r => r.referencia && r.referencia !== 'referencia');\nconst usados = new Set();\nconst casados = [], noProc = [], noInt = [];\nfor (const p of proc) {\n  const idx = inte.findIndex((r, i) => !usados.has(i) && r.referencia === p.referencia && n(r.monto) === n(p.monto) && String(p.estado).toLowerCase() === 'aprobado' && String(r.estado).toLowerCase() === 'pagado');\n  if (idx >= 0) { usados.add(idx); casados.push(p.referencia); } else noProc.push({ lado: 'procesador', ...p, monto: n(p.monto), comision: n(p.comision), neto: n(p.neto) });\n}\ninte.forEach((r, i) => { if (!usados.has(i)) noInt.push({ lado: 'interno', ...r, monto: n(r.monto) }); });\nreturn [{ json: { casadas_ref: casados, total_procesador: proc.length, total_interno: inte.length, casados: casados.length, diferencias: noProc.length + noInt.length, noProc, noInt } }];"
      },
      "id": "cruce",
      "name": "Code: cruce determinista",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        520,
        0
      ]
    },
    {
      "parameters": {
        "resource": "text",
        "operation": "message",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-5-5"
        },
        "messages": {
          "values": [
            {
              "role": "user",
              "content": "={{ 'REFERENCIAS YA CASADAS (una transacción interna por cada una): ' + $json.casadas_ref.join(', ') + '\\n\\nNO CASADAS DEL PROCESADOR\\n' + JSON.stringify($json.noProc, null, 1) + '\\n\\nNO CASADAS DEL REGISTRO INTERNO\\n' + JSON.stringify($json.noInt, null, 1) + '\\n\\nSi una referencia interna no casada ya aparece entre las casadas con el mismo monto, es un duplicado. Devuelve un array JSON con un objeto por transacción.' }}"
            }
          ]
        },
        "options": {
          "system": "Eres analista de conciliación de [organización]. Recibes dos listas de transacciones que no casaron: las del procesador de pagos y las del registro interno, con fecha, referencia, monto, comisión y estado. Para cada una devuelve un JSON con: {\"referencia\": \"...\", \"lado\": \"procesador | interno\", \"clasificacion\": \"desfase_fecha | comision | contracargo | duplicado | error_captura | reembolso | sin_explicacion\", \"pareja_probable\": \"referencia del otro lado o null\", \"causa\": \"una frase\", \"accion\": \"ninguna | revisar | reclamar | corregir\", \"confianza\": \"alta | media | baja\"}\nReglas: una transacción del procesador por 97.000 y una interna por 100.000 el mismo día con la misma referencia es comisión, no error; dos internas iguales con un solo pago es duplicado; si no encuentras pareja ni patrón, sin_explicacion con confianza baja; nunca modifiques montos; nada fuera del JSON.",
          "maxTokens": 4000
        }
      },
      "id": "cl",
      "name": "Claude: clasificar diferencias",
      "type": "@n8n/n8n-nodes-langchain.anthropic",
      "typeVersion": 1,
      "position": [
        780,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const texto = ($json.content || []).filter(b => b.type === 'text').map(b => b.text).join('\\n').trim();\nconst limpio = texto.replace(/^```(?:json)?\\s*/i, '').replace(/```\\s*$/, '');\nlet arr; try { arr = JSON.parse(limpio); } catch (e) { arr = [{ referencia: '?', lado: '?', clasificacion: 'sin_explicacion', causa: 'respuesta no interpretable: ' + texto.slice(0, 200), accion: 'revisar', confianza: 'baja' }]; }\nif (!Array.isArray(arr)) arr = arr.diferencias || arr.resultado || [arr];\nconst res = $('Code: cruce determinista').first().json;\nreturn arr.map(d => ({ json: { fecha: $now.toISODate(), referencia: d.referencia || '', lado: d.lado || '', clasificacion: d.clasificacion || '', pareja_probable: d.pareja_probable || '', causa: d.causa || '', accion: d.accion || 'revisar', confianza: d.confianza || 'baja', estado: (d.accion || 'revisar') === 'ninguna' ? 'cerrada' : 'pendiente', resumen: `${res.casados}/${res.total_procesador} casadas; ${res.diferencias} diferencias` } }));"
      },
      "id": "parse",
      "name": "Code: leer clasificación",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1040,
        0
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "TU_SPREADSHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "conciliacion"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "fecha": "={{ $json.fecha }}",
            "referencia": "={{ $json.referencia }}",
            "lado": "={{ $json.lado }}",
            "clasificacion": "={{ $json.clasificacion }}",
            "pareja_probable": "={{ $json.pareja_probable }}",
            "causa": "={{ $json.causa }}",
            "accion": "={{ $json.accion }}",
            "confianza": "={{ $json.confianza }}",
            "estado": "={{ $json.estado }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "fecha",
              "displayName": "fecha",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "referencia",
              "displayName": "referencia",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "lado",
              "displayName": "lado",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "clasificacion",
              "displayName": "clasificacion",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "pareja_probable",
              "displayName": "pareja_probable",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "causa",
              "displayName": "causa",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "accion",
              "displayName": "accion",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "confianza",
              "displayName": "confianza",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "estado",
              "displayName": "estado",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {}
      },
      "id": "sh",
      "name": "Sheets: hoja de conciliación",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1300,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const todas = $input.all().map(i => i.json);\nconst accion = todas.filter(d => d.accion && d.accion !== 'ninguna');\nif (!accion.length) return [];\nconst lineas = accion.map(d => `• ${d.referencia} (${d.lado}): ${d.clasificacion} → ${d.accion}. ${d.causa}`);\nreturn [{ json: { texto: `🔎 Conciliación de hoy: ${$('Code: leer clasificación').first().json.resumen}\\nRequieren acción:\\n` + lineas.join('\\n') } }];"
      },
      "id": "alert",
      "name": "Code: resumen de alertas",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1560,
        0
      ]
    },
    {
      "parameters": {
        "chatId": "TU_CHAT_ID",
        "text": "={{ $json.texto }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML"
        }
      },
      "id": "tg",
      "name": "Telegram: alertar",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1820,
        0
      ]
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Sheets: reporte del procesador",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets: reporte del procesador": {
      "main": [
        [
          {
            "node": "Sheets: registro interno",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets: registro interno": {
      "main": [
        [
          {
            "node": "Code: cruce determinista",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: cruce determinista": {
      "main": [
        [
          {
            "node": "Claude: clasificar diferencias",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude: clasificar diferencias": {
      "main": [
        [
          {
            "node": "Code: leer clasificación",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: leer clasificación": {
      "main": [
        [
          {
            "node": "Sheets: hoja de conciliación",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets: hoja de conciliación": {
      "main": [
        [
          {
            "node": "Code: resumen de alertas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: resumen de alertas": {
      "main": [
        [
          {
            "node": "Telegram: alertar",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "America/Bogota"
  },
  "meta": {
    "templateCredsSetupCompleted": false,
    "probadoEn": "n8n 2.41.6, octubre 2026",
    "preparacion": "Reemplaza las lecturas de Sheets por tus fuentes reales: el reporte del procesador (SFTP, correo o API, con columnas fecha, referencia, monto, comision, neto, estado) y el registro interno (ERP o Sheets, con fecha, referencia, monto, cliente, estado). Crea una pestaña \"conciliacion\" con encabezado fecha, referencia, lado, clasificacion, pareja_probable, causa, accion, confianza, estado. Reemplaza TU_SPREADSHEET_ID y TU_CHAT_ID. Los datos de tarjeta nunca entran al flujo.",
    "fuente": "https://saphialabs.com/recursos/flujos/#f9"
  }
}