{
  "name": "Extracción de facturas y recibos (plano de Saphia Labs)",
  "nodes": [
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "simple": false,
        "filters": {
          "q": "has:attachment filename:pdf -label:n8n-facturas"
        },
        "options": {
          "downloadAttachments": true,
          "dataPropertyAttachmentsPrefixName": "attachment_"
        }
      },
      "id": "tr",
      "name": "Gmail Trigger",
      "type": "n8n-nodes-base.gmailTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Toma el primer adjunto PDF menor de 5 MB y lo deja en 'data'; lo demás va a revisión manual\nconst bin = $input.item.binary || {};\nconst key = Object.keys(bin).find(k => (bin[k].mimeType || '').includes('pdf'));\nif (!key) return { json: { skip: true, motivo: 'sin PDF' } };\nconst size = Number(bin[key].fileSize) || 0;\nif (size > 5 * 1024 * 1024) return { json: { skip: true, motivo: 'PDF mayor de 5 MB' } };\nreturn { json: { skip: false, archivo: bin[key].fileName || 'factura.pdf', mensajeId: $input.item.json.id || '' }, binary: { data: bin[key] } };",
        "mode": "runOnceForEachItem"
      },
      "id": "pick",
      "name": "Code: tomar el PDF",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        260,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "s",
              "leftValue": "={{ $json.skip }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if",
      "name": "IF: es PDF válido",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        520,
        0
      ]
    },
    {
      "parameters": {
        "resource": "document",
        "operation": "analyze",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-5-5"
        },
        "text": "Eres el auxiliar contable de [organización]. Recibes una factura en PDF. Extrae y devuelve SOLO un JSON:\n{\"proveedor\": \"razón social\", \"identificacion\": \"NIT, RUT o RFC tal como aparece\", \"numero\": \"número de factura\", \"fecha\": \"AAAA-MM-DD\", \"vencimiento\": \"AAAA-MM-DD o null\", \"moneda\": \"COP | MXN | USD | ...\", \"subtotal\": 0, \"impuestos\": 0, \"total\": 0, \"concepto\": \"una línea\", \"confianza\": \"alta | media | baja\"}\nReglas: copia los números tal como están, sin redondear ni convertir moneda; si un campo no aparece, usa null y baja la confianza; si hay varias facturas en el PDF, devuelve solo la primera y pon confianza baja; nada fuera del JSON.",
        "inputType": "binary",
        "binaryPropertyName": "data",
        "simplify": true,
        "options": {
          "maxTokens": 2000
        }
      },
      "id": "cl",
      "name": "Claude: leer la factura",
      "type": "@n8n/n8n-nodes-langchain.anthropic",
      "typeVersion": 1,
      "position": [
        780,
        -100
      ]
    },
    {
      "parameters": {
        "jsCode": "const texto = ($input.item.json.content || []).filter(b => b.type === 'text').map(b => b.text).join('\\n').trim();\nconst limpio = texto.replace(/^```(?:json)?\\s*/i, '').replace(/```\\s*$/, '');\nlet r; try { r = JSON.parse(limpio); } catch (e) { r = { error: texto.slice(0, 300) }; }\nconst src = $('Code: tomar el PDF').item.json;\nconst n = v => Number(String(v ?? '').replace(/[^\\d.-]/g, '')) || 0;\nconst sub = n(r.subtotal), imp = n(r.impuestos), tot = n(r.total);\nconst cuadra = Math.abs(sub + imp - tot) <= 1;\nconst fechaOk = /^\\d{4}-\\d{2}-\\d{2}$/.test(String(r.fecha || ''));\nconst conf = String(r.confianza || 'baja');\nconst estado = (!r.error && cuadra && fechaOk && r.proveedor && conf === 'alta') ? 'ok' : 'revisar';\nconst motivo = r.error ? 'respuesta no interpretable' : [!cuadra ? `subtotal + impuestos (${sub + imp}) ≠ total (${tot})` : '', !fechaOk ? 'fecha inválida' : '', !r.proveedor ? 'sin proveedor' : '', conf !== 'alta' ? 'confianza ' + conf : ''].filter(Boolean).join('; ');\nreturn { json: { fecha: $now.toISO(), archivo: src.archivo, proveedor: r.proveedor || '', identificacion: r.identificacion || '', numero: r.numero || '', fecha_factura: r.fecha || '', subtotal: sub, impuestos: imp, total: tot, estado, confianza: conf, motivo, mensajeId: src.mensajeId } };",
        "mode": "runOnceForEachItem"
      },
      "id": "val",
      "name": "Code: validar",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1040,
        -100
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "TU_SPREADSHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "facturas"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "fecha": "={{ $json.fecha }}",
            "archivo": "={{ $json.archivo }}",
            "proveedor": "={{ $json.proveedor }}",
            "identificacion": "={{ $json.identificacion }}",
            "numero": "={{ $json.numero }}",
            "fecha_factura": "={{ $json.fecha_factura }}",
            "subtotal": "={{ $json.subtotal }}",
            "impuestos": "={{ $json.impuestos }}",
            "total": "={{ $json.total }}",
            "estado": "={{ $json.estado }}",
            "confianza": "={{ $json.confianza }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "fecha",
              "displayName": "fecha",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "archivo",
              "displayName": "archivo",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "proveedor",
              "displayName": "proveedor",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "identificacion",
              "displayName": "identificacion",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "numero",
              "displayName": "numero",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "fecha_factura",
              "displayName": "fecha_factura",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "subtotal",
              "displayName": "subtotal",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "impuestos",
              "displayName": "impuestos",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "total",
              "displayName": "total",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "estado",
              "displayName": "estado",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "confianza",
              "displayName": "confianza",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {}
      },
      "id": "sh",
      "name": "Sheets: registrar factura",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1300,
        -100
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "r",
              "leftValue": "={{ $json.estado }}",
              "rightValue": "revisar",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "ifr",
      "name": "IF: revisar",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1560,
        -100
      ]
    },
    {
      "parameters": {
        "chatId": "TU_CHAT_ID",
        "text": "=🧾 Factura para revisar: {{ $json.archivo }}\n{{ $json.proveedor }} · No. {{ $json.numero }} · total {{ $json.total }}\nMotivo: {{ $json.motivo }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "id": "tg",
      "name": "Telegram: avisar revisión",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1820,
        -200
      ]
    },
    {
      "parameters": {
        "operation": "addLabels",
        "messageId": "={{ $json.mensajeId }}",
        "labelIds": [
          "TU_LABEL_ID"
        ]
      },
      "id": "lb",
      "name": "Gmail: etiquetar procesada",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1820,
        0
      ]
    }
  ],
  "connections": {
    "Gmail Trigger": {
      "main": [
        [
          {
            "node": "Code: tomar el PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: tomar el PDF": {
      "main": [
        [
          {
            "node": "IF: es PDF válido",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: es PDF válido": {
      "main": [
        [
          {
            "node": "Claude: leer la factura",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Claude: leer la factura": {
      "main": [
        [
          {
            "node": "Code: validar",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: validar": {
      "main": [
        [
          {
            "node": "Sheets: registrar factura",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets: registrar factura": {
      "main": [
        [
          {
            "node": "IF: revisar",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: revisar": {
      "main": [
        [
          {
            "node": "Telegram: avisar revisión",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Gmail: etiquetar procesada",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "America/Bogota"
  },
  "meta": {
    "templateCredsSetupCompleted": false,
    "probadoEn": "n8n 2.41.6, octubre 2026",
    "preparacion": "Crea una pestaña \"facturas\" con encabezado fecha, archivo, proveedor, identificacion, numero, fecha_factura, subtotal, impuestos, total, estado, confianza; y una etiqueta en Gmail (por ejemplo n8n-facturas) para marcar lo procesado. Reemplaza TU_SPREADSHEET_ID, TU_LABEL_ID y TU_CHAT_ID. Para escribir en el ERP, reemplaza el nodo de Sheets por el de tu sistema (Siigo, Alegra, QuickBooks).",
    "fuente": "https://saphialabs.com/recursos/flujos/#f4"
  }
}