{
  "name": "Logixb2b - Presupuesto prospeccion B2B (capas + TCO)",
  "meta": {
    "templateCredsSetupCompleted": false,
    "description": "Manual → Config capas EUR → Validar → TCO + ranking + semaforo → Slack techo opcional → brief OpenAI opcional → Sheets Presupuesto_Prospeccion + Historial (o skip demo) → Slack OK → resumen local. Europe/Madrid. REEMPLAZA_*. Sin SMTP. Demo completa sin credenciales."
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "Europe/Madrid"
  },
  "active": false,
  "pinData": {},
  "tags": [
    {
      "name": "Logixb2b"
    },
    {
      "name": "recursos"
    },
    {
      "name": "prospeccion"
    },
    {
      "name": "presupuesto"
    }
  ],
  "nodes": [
    {
      "id": "pb-sticky",
      "name": "INSTRUCCIONES (conectar + VPS)",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -920,
        -40
      ],
      "parameters": {
        "content": "## INSTRUCCIONES - Presupuesto prospeccion B2B\n\n**Recurso:** `/recursos/presupuesto-prospeccion-b2b-n8n`\n**Guia:** /cuanto-cuesta-automatizar-prospeccion-b2b-presupuesto-real\n**Zona:** Europe/Madrid · active: false al importar.\n\n### 1) Importar\nn8n → ⋯ → **Import from File** → este JSON (no pegues texto).\n\n### 2) Demo YA (sin credenciales)\nEjecuta **Manual Trigger**. Con REEMPLAZA_* el flujo pone skipSheets/skipSlack y termina en **Resumen final local** con TCO, coste/lead, stackHealth y recommendation.\n\n### 3) Produccion\n1. Google Sheets OAuth + pestanas **Presupuesto_Prospeccion** y **Presupuesto_Historial**.\n2. Sustituye sheetsId y slackWebhookUrl (quita REEMPLAZA).\n3. Opcional: enableAiBrief=true + openaiApiKey.\n4. Form Trigger (en lienzo, desconectado) con los mismos campos EUR.\n\n### 4) Que resuelve\nUna cifra unica de TCO mensual por capas (datos, verify, ESP, VPS n8n, CRM, horas), semaforo vs techo y traza en Sheets para decidir renovaciones.\n\n### 5) VPS UE\nDocker + Postgres + HTTPS + GENERIC_TIMEZONE=Europe/Madrid.\n\n### 6) RGPD\nSolo importes agregados. /uso-responsable-de-ia-y-transparencia\n",
        "height": 1560,
        "width": 700,
        "color": 4
      }
    },
    {
      "id": "pb-sticky-cols",
      "name": "COLUMNAS SHEETS",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -920,
        1200
      ],
      "parameters": {
        "content": "## Presupuesto_Prospeccion\nfecha | batchLabel | runSource | monthlyTotal | costPerLead | budgetCapEur | withinBudget | overByEur | underByEur | stackHealth | roiRatio | topLayerName | topLayerEur | dataProvidersEur | verifyApiEur | espWarmupEur | vpsN8nEur | crmSeatsEur | hoursOpsEur | recommendation | executiveBrief | layerBreakdownText | complianceFlag\n\n## Presupuesto_Historial\nfecha | batchLabel | runSource | monthlyTotal | stackHealth | topLayerName | costPerLead | withinBudget",
        "height": 380,
        "width": 700,
        "color": 5
      }
    },
    {
      "id": "pb-trigger",
      "name": "Manual Trigger presupuesto",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        420
      ],
      "parameters": {}
    },
    {
      "id": "pb-form",
      "name": "Form Trigger (produccion - cablear)",
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.2,
      "position": [
        0,
        680
      ],
      "parameters": {
        "formTitle": "Presupuesto prospeccion B2B",
        "formDescription": "Introduce capas EUR mensuales. Europe/Madrid.",
        "formFields": {
          "values": [
            {
              "fieldLabel": "batchLabel",
              "fieldType": "text",
              "requiredField": true
            },
            {
              "fieldLabel": "dataProvidersEur",
              "fieldType": "number",
              "requiredField": true
            },
            {
              "fieldLabel": "verifyApiEur",
              "fieldType": "number",
              "requiredField": true
            },
            {
              "fieldLabel": "espWarmupEur",
              "fieldType": "number",
              "requiredField": true
            },
            {
              "fieldLabel": "vpsN8nEur",
              "fieldType": "number",
              "requiredField": true
            },
            {
              "fieldLabel": "crmSeatsEur",
              "fieldType": "number",
              "requiredField": true
            },
            {
              "fieldLabel": "hoursOpsEur",
              "fieldType": "number",
              "requiredField": true
            },
            {
              "fieldLabel": "budgetCapEur",
              "fieldType": "number",
              "requiredField": true
            },
            {
              "fieldLabel": "leadsTargetMonth",
              "fieldType": "number",
              "requiredField": true
            },
            {
              "fieldLabel": "expectedMeetings",
              "fieldType": "number",
              "requiredField": true
            },
            {
              "fieldLabel": "avgWonLeadValueEur",
              "fieldType": "number",
              "requiredField": true
            }
          ]
        },
        "responseMode": "onReceived",
        "options": {}
      }
    },
    {
      "id": "pb-schedule",
      "name": "Schedule semanal (opcional - cablear)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        900
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "weeksInterval": 1,
              "triggerAtHour": 9
            }
          ]
        }
      }
    },
    {
      "id": "pb-config",
      "name": "Config capas EUR",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        280,
        420
      ],
      "parameters": {
        "mode": "manual",
        "duplicateItem": false,
        "includeOtherFields": false,
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "batchLabel",
              "value": "presupuesto-demo-2026-08-10",
              "type": "string"
            },
            {
              "id": "a2",
              "name": "demoMode",
              "value": "true",
              "type": "string"
            },
            {
              "id": "a3",
              "name": "enableAiBrief",
              "value": "false",
              "type": "string"
            },
            {
              "id": "a4",
              "name": "skipSheets",
              "value": "false",
              "type": "string"
            },
            {
              "id": "a5",
              "name": "skipSlack",
              "value": "false",
              "type": "string"
            },
            {
              "id": "a6",
              "name": "teamSize",
              "value": "3",
              "type": "string"
            },
            {
              "id": "a7",
              "name": "leadsTargetMonth",
              "value": "400",
              "type": "string"
            },
            {
              "id": "a8",
              "name": "dataProvidersEur",
              "value": "89",
              "type": "string"
            },
            {
              "id": "a9",
              "name": "verifyApiEur",
              "value": "35",
              "type": "string"
            },
            {
              "id": "a10",
              "name": "espWarmupEur",
              "value": "67",
              "type": "string"
            },
            {
              "id": "a11",
              "name": "vpsN8nEur",
              "value": "18",
              "type": "string"
            },
            {
              "id": "a12",
              "name": "crmSeatsEur",
              "value": "75",
              "type": "string"
            },
            {
              "id": "a13",
              "name": "hoursOpsEur",
              "value": "240",
              "type": "string"
            },
            {
              "id": "a14",
              "name": "budgetCapEur",
              "value": "600",
              "type": "string"
            },
            {
              "id": "a15",
              "name": "expectedMeetings",
              "value": "12",
              "type": "string"
            },
            {
              "id": "a16",
              "name": "avgWonLeadValueEur",
              "value": "2500",
              "type": "string"
            },
            {
              "id": "a17",
              "name": "closeRate",
              "value": "0.25",
              "type": "string"
            },
            {
              "id": "a18",
              "name": "sheetsId",
              "value": "REEMPLAZA_SHEETS_ID",
              "type": "string"
            },
            {
              "id": "a19",
              "name": "slackWebhookUrl",
              "value": "https://hooks.example.com/services/REEMPLAZA_SLACK",
              "type": "string"
            },
            {
              "id": "a20",
              "name": "openaiApiUrl",
              "value": "https://api.openai.com/v1/chat/completions",
              "type": "string"
            },
            {
              "id": "a21",
              "name": "openaiModel",
              "value": "gpt-4o-mini",
              "type": "string"
            },
            {
              "id": "a22",
              "name": "openaiApiKey",
              "value": "REEMPLAZA_OPENAI_API_KEY",
              "type": "string"
            },
            {
              "id": "a23",
              "name": "timezone",
              "value": "Europe/Madrid",
              "type": "string"
            },
            {
              "id": "a24",
              "name": "runSource",
              "value": "manual",
              "type": "string"
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "pb-validate",
      "name": "Validar Config",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        540,
        420
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const c = $input.first().json;\nconst n = (k) => {\n  const v = Number(String(c[k] ?? '').replace(',', '.').trim());\n  return Number.isFinite(v) ? v : NaN;\n};\nconst required = [\n  'dataProvidersEur','verifyApiEur','espWarmupEur','vpsN8nEur','crmSeatsEur','hoursOpsEur',\n  'budgetCapEur','leadsTargetMonth','expectedMeetings','avgWonLeadValueEur'\n];\nconst errors = [];\nfor (const k of required) {\n  const v = n(k);\n  if (!Number.isFinite(v) || v < 0) errors.push(k + ' invalido');\n}\nif (!c.batchLabel || String(c.batchLabel).trim().length < 3) errors.push('batchLabel corto');\nconst sheetsPlaceholder = String(c.sheetsId || '').includes('REEMPLAZA');\nconst slackPlaceholder = String(c.slackWebhookUrl || '').includes('REEMPLAZA');\nconst skipSheets = String(c.skipSheets || 'false') === 'true' || sheetsPlaceholder;\nconst skipSlack = String(c.skipSlack || 'false') === 'true' || slackPlaceholder;\nreturn [{\n  json: {\n    ...c,\n    configValid: errors.length === 0,\n    configErrors: errors.join('; ') || '',\n    skipSheets,\n    skipSlack,\n    runSource: c.runSource || 'manual',\n    timezone: c.timezone || 'Europe/Madrid'\n  }\n}];"
      }
    },
    {
      "id": "pb-if-valid",
      "name": "IF config valida",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        780,
        420
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "pb-if-valid-c",
              "leftValue": "={{ $json.configValid }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "pb-slack-err",
      "name": "Slack config invalida",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1020,
        640
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $json.slackWebhookUrl }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"text\": \"Logixb2b presupuesto ERROR config · {{ $json.batchLabel }} · {{ $json.configErrors }} · Europe/Madrid\"\n}",
        "options": {}
      },
      "continueOnFail": true
    },
    {
      "id": "pb-stop",
      "name": "Stop (config invalida)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1260,
        640
      ],
      "parameters": {}
    },
    {
      "id": "pb-calc",
      "name": "Calcular TCO mensual",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1020,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const c = $('Validar Config').first().json;\nconst n = (k) => {\n  const v = Number(String(c[k] ?? '').replace(',', '.').trim());\n  return Number.isFinite(v) ? v : 0;\n};\nconst layers = {\n  dataProvidersEur: n('dataProvidersEur'),\n  verifyApiEur: n('verifyApiEur'),\n  espWarmupEur: n('espWarmupEur'),\n  vpsN8nEur: n('vpsN8nEur'),\n  crmSeatsEur: n('crmSeatsEur'),\n  hoursOpsEur: n('hoursOpsEur')\n};\nconst ranked = Object.entries(layers)\n  .map(([name, eur]) => ({ name, eur }))\n  .sort((a, b) => b.eur - a.eur);\nconst monthlyTotal = ranked.reduce((a, b) => a + b.eur, 0);\nconst budgetCapEur = n('budgetCapEur');\nconst leadsTargetMonth = Math.max(1, n('leadsTargetMonth'));\nconst expectedMeetings = Math.max(0, n('expectedMeetings'));\nconst avgLeadValueEur = n('avgWonLeadValueEur');\nconst closeRate = Math.min(1, Math.max(0, n('closeRate') || 0.25));\nconst costPerLead = monthlyTotal / leadsTargetMonth;\nconst projectedRevenue = expectedMeetings * avgLeadValueEur * closeRate;\nconst roiRatio = monthlyTotal > 0 ? projectedRevenue / monthlyTotal : 0;\nconst withinBudget = monthlyTotal <= budgetCapEur;\nconst overByEur = Math.max(0, monthlyTotal - budgetCapEur);\nconst underByEur = Math.max(0, budgetCapEur - monthlyTotal);\nconst stackHealth = withinBudget ? (roiRatio >= 3 ? 'verde' : 'ambar') : 'rojo';\nconst top = ranked[0] || { name: '', eur: 0 };\nconst recommendation =\n  stackHealth === 'rojo'\n    ? 'Recorta primero ' + top.name + ' (' + top.eur + ' EUR) o sube budgetCapEur con justificacion.'\n    : stackHealth === 'ambar'\n      ? 'Dentro de techo: mejora conversion (reuniones/cierre) antes de sumar SaaS.'\n      : 'Techo y ROI sanos: congela capas 30 dias y mide reuniones CRM.';\nconst executiveBrief =\n  'TCO ' + Math.round(monthlyTotal * 100) / 100 + ' EUR/mes · coste/lead ' +\n  Math.round(costPerLead * 100) / 100 + ' · techo ' + budgetCapEur +\n  ' · health ' + stackHealth + ' · top ' + top.name + ' · ' + recommendation;\nreturn [{\n  json: {\n    batchLabel: c.batchLabel,\n    runSource: c.runSource || 'manual',\n    demoMode: String(c.demoMode) === 'true',\n    enableAiBrief: String(c.enableAiBrief) === 'true',\n    skipSheets: !!c.skipSheets,\n    skipSlack: !!c.skipSlack,\n    teamSize: n('teamSize'),\n    leadsTargetMonth,\n    expectedMeetings,\n    avgLeadValueEur,\n    closeRate,\n    budgetCapEur,\n    layers,\n    rankedLayers: ranked,\n    layerBreakdownText: ranked.map((r) => r.name + '=' + r.eur).join(' | '),\n    monthlyTotal: Math.round(monthlyTotal * 100) / 100,\n    costPerLead: Math.round(costPerLead * 100) / 100,\n    projectedRevenue: Math.round(projectedRevenue * 100) / 100,\n    roiRatio: Math.round(roiRatio * 100) / 100,\n    withinBudget,\n    overByEur: Math.round(overByEur * 100) / 100,\n    underByEur: Math.round(underByEur * 100) / 100,\n    stackHealth,\n    topLayerName: top.name,\n    topLayerEur: top.eur,\n    recommendation,\n    executiveBrief,\n    sheetsId: c.sheetsId,\n    slackWebhookUrl: c.slackWebhookUrl,\n    openaiApiUrl: c.openaiApiUrl || 'https://api.openai.com/v1/chat/completions',\n    openaiModel: c.openaiModel || 'gpt-4o-mini',\n    openaiApiKey: c.openaiApiKey || '',\n    timezone: c.timezone || 'Europe/Madrid',\n    complianceFlag: 'rgpd_agregados_sin_pii_presupuesto_prospeccion',\n    _generatedAt: new Date().toISOString()\n  }\n}];"
      }
    },
    {
      "id": "pb-if-budget",
      "name": "IF dentro del techo",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1260,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "pb-if-budget-c",
              "leftValue": "={{ $json.withinBudget }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "pb-pass-ok",
      "name": "Passthrough techo OK",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1500,
        200
      ],
      "parameters": {}
    },
    {
      "id": "pb-if-slack-over",
      "name": "IF avisar Slack techo",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1500,
        420
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "pb-if-slack-over-c",
              "leftValue": "={{ String($json.skipSlack) }}",
              "rightValue": "true",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "pb-slack-over",
      "name": "Slack techo superado",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1740,
        360
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $json.slackWebhookUrl }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"text\": \"Logixb2b presupuesto · batch {{ $json.batchLabel }} · TCO {{ $json.monthlyTotal }} EUR supera techo {{ $json.budgetCapEur }} (+{{ $json.overByEur }}) · top {{ $json.topLayerName }}={{ $json.topLayerEur }} · {{ $json.recommendation }} · Europe/Madrid\"\n}",
        "options": {}
      },
      "continueOnFail": true
    },
    {
      "id": "pb-skip-over",
      "name": "Skip Slack techo (demo)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1740,
        520
      ],
      "parameters": {}
    },
    {
      "id": "pb-restore-budget",
      "name": "Restaurar cifras tras techo",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1980,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "return [$('Calcular TCO mensual').first()];"
      }
    },
    {
      "id": "pb-if-brief",
      "name": "IF enableAiBrief",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2220,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "pb-if-brief-c",
              "leftValue": "={{ String($json.enableAiBrief) }}",
              "rightValue": "true",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "pb-prompt",
      "name": "Armar body OpenAI brief",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2460,
        140
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const j = $input.first().json;\nconst user =\n  'Eres analista de operaciones B2B. Con SOLO estas cifras (no inventes otras), escribe en espanol un briefing de 5-7 frases: TCO, coste/lead, techo, health, capa top, recomendacion y 1 riesgo.\\n' +\n  JSON.stringify({\n    monthlyTotal: j.monthlyTotal,\n    costPerLead: j.costPerLead,\n    budgetCapEur: j.budgetCapEur,\n    withinBudget: j.withinBudget,\n    stackHealth: j.stackHealth,\n    layers: j.layers,\n    rankedLayers: j.rankedLayers,\n    topLayerName: j.topLayerName,\n    recommendation: j.recommendation,\n    roiRatio: j.roiRatio\n  });\nreturn [{\n  json: {\n    ...j,\n    openaiBody: {\n      model: j.openaiModel || 'gpt-4o-mini',\n      temperature: 0.2,\n      messages: [\n        { role: 'system', content: 'Responde solo con el briefing. Sin markdown de codigo.' },\n        { role: 'user', content: user }\n      ]\n    }\n  }\n}];"
      }
    },
    {
      "id": "pb-openai",
      "name": "HTTP Brief OpenAI",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2700,
        140
      ],
      "continueOnFail": true,
      "parameters": {
        "method": "POST",
        "url": "={{ $json.openaiApiUrl }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $json.openaiApiKey }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json.openaiBody }}",
        "options": {}
      }
    },
    {
      "id": "pb-apply-brief",
      "name": "Aplicar briefing OpenAI",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2940,
        140
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const base = $('Armar body OpenAI brief').first().json;\nconst raw = $input.first().json;\nconst content =\n  (raw.choices && raw.choices[0] && raw.choices[0].message && raw.choices[0].message.content) ||\n  base.executiveBrief;\nconst { openaiBody, openaiApiKey, ...rest } = base;\nreturn [{ json: { ...rest, executiveBrief: String(content || '').slice(0, 1200) } }];"
      }
    },
    {
      "id": "pb-brief-skip",
      "name": "Sin brief LLM (passthrough)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        2460,
        420
      ],
      "parameters": {}
    },
    {
      "id": "pb-sanitize",
      "name": "Limpiar fila Sheets",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3180,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const j = $input.first().json;\nreturn [{\n  json: {\n    batchLabel: j.batchLabel,\n    runSource: j.runSource || 'manual',\n    monthlyTotal: j.monthlyTotal,\n    costPerLead: j.costPerLead,\n    budgetCapEur: j.budgetCapEur,\n    withinBudget: j.withinBudget,\n    overByEur: j.overByEur,\n    underByEur: j.underByEur,\n    stackHealth: j.stackHealth,\n    roiRatio: j.roiRatio,\n    topLayerName: j.topLayerName,\n    topLayerEur: j.topLayerEur,\n    dataProvidersEur: j.layers?.dataProvidersEur ?? 0,\n    verifyApiEur: j.layers?.verifyApiEur ?? 0,\n    espWarmupEur: j.layers?.espWarmupEur ?? 0,\n    vpsN8nEur: j.layers?.vpsN8nEur ?? 0,\n    crmSeatsEur: j.layers?.crmSeatsEur ?? 0,\n    hoursOpsEur: j.layers?.hoursOpsEur ?? 0,\n    recommendation: j.recommendation,\n    executiveBrief: String(j.executiveBrief || '').slice(0, 1200),\n    layerBreakdownText: j.layerBreakdownText,\n    complianceFlag: j.complianceFlag,\n    sheetsId: j.sheetsId,\n    slackWebhookUrl: j.slackWebhookUrl,\n    skipSheets: !!j.skipSheets,\n    skipSlack: !!j.skipSlack,\n    timezone: j.timezone || 'Europe/Madrid'\n  }\n}];"
      }
    },
    {
      "id": "pb-if-sheets",
      "name": "IF escribir Sheets",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        3420,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "pb-if-sheets-c",
              "leftValue": "={{ String($json.skipSheets) }}",
              "rightValue": "true",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "pb-sheets",
      "name": "Append Sheets Presupuesto_Prospeccion",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        3660,
        180
      ],
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "={{ $json.sheetsId }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Presupuesto_Prospeccion",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "fecha": "={{ $now.setZone('Europe/Madrid').toFormat('yyyy-MM-dd HH:mm') }}",
            "batchLabel": "={{ $json.batchLabel }}",
            "runSource": "={{ $json.runSource }}",
            "monthlyTotal": "={{ $json.monthlyTotal }}",
            "costPerLead": "={{ $json.costPerLead }}",
            "budgetCapEur": "={{ $json.budgetCapEur }}",
            "withinBudget": "={{ $json.withinBudget }}",
            "overByEur": "={{ $json.overByEur }}",
            "underByEur": "={{ $json.underByEur }}",
            "stackHealth": "={{ $json.stackHealth }}",
            "roiRatio": "={{ $json.roiRatio }}",
            "topLayerName": "={{ $json.topLayerName }}",
            "topLayerEur": "={{ $json.topLayerEur }}",
            "dataProvidersEur": "={{ $json.dataProvidersEur }}",
            "verifyApiEur": "={{ $json.verifyApiEur }}",
            "espWarmupEur": "={{ $json.espWarmupEur }}",
            "vpsN8nEur": "={{ $json.vpsN8nEur }}",
            "crmSeatsEur": "={{ $json.crmSeatsEur }}",
            "hoursOpsEur": "={{ $json.hoursOpsEur }}",
            "recommendation": "={{ $json.recommendation }}",
            "executiveBrief": "={{ $json.executiveBrief }}",
            "layerBreakdownText": "={{ $json.layerBreakdownText }}",
            "complianceFlag": "={{ $json.complianceFlag }}"
          }
        },
        "options": {}
      }
    },
    {
      "id": "pb-sheets-hist",
      "name": "Append Sheets Presupuesto_Historial",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        3900,
        180
      ],
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Limpiar fila Sheets').item.json.sheetsId }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Presupuesto_Historial",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "fecha": "={{ $now.setZone('Europe/Madrid').toFormat('yyyy-MM-dd HH:mm') }}",
            "batchLabel": "={{ $('Limpiar fila Sheets').item.json.batchLabel }}",
            "runSource": "={{ $('Limpiar fila Sheets').item.json.runSource }}",
            "monthlyTotal": "={{ $('Limpiar fila Sheets').item.json.monthlyTotal }}",
            "stackHealth": "={{ $('Limpiar fila Sheets').item.json.stackHealth }}",
            "topLayerName": "={{ $('Limpiar fila Sheets').item.json.topLayerName }}",
            "costPerLead": "={{ $('Limpiar fila Sheets').item.json.costPerLead }}",
            "withinBudget": "={{ $('Limpiar fila Sheets').item.json.withinBudget }}"
          }
        },
        "options": {}
      }
    },
    {
      "id": "pb-sheets-skip",
      "name": "Skip Sheets (demo)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        3660,
        420
      ],
      "parameters": {}
    },
    {
      "id": "pb-restore-sheets",
      "name": "Restaurar cifras post Sheets",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4140,
        180
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "return [$('Limpiar fila Sheets').first()];"
      }
    },
    {
      "id": "pb-if-slack-ok",
      "name": "IF Slack OK",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        4380,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "pb-if-slack-ok-c",
              "leftValue": "={{ String($json.skipSlack) }}",
              "rightValue": "true",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "pb-slack-ok",
      "name": "Slack resumen presupuesto OK",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        4620,
        180
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $json.slackWebhookUrl }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"text\": \"Logixb2b presupuesto · {{ $json.batchLabel }} · TCO {{ $json.monthlyTotal }} EUR · coste/lead {{ $json.costPerLead }} · health {{ $json.stackHealth }} · top {{ $json.topLayerName }} · {{ $json.recommendation }} · Europe/Madrid\"\n}",
        "options": {}
      },
      "continueOnFail": true
    },
    {
      "id": "pb-slack-skip",
      "name": "Skip Slack OK (demo)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        4620,
        420
      ],
      "parameters": {}
    },
    {
      "id": "pb-done",
      "name": "Resumen final local",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4860,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const j = $input.first().json;\nreturn [{\n  json: {\n    ok: true,\n    batchLabel: j.batchLabel,\n    monthlyTotal: j.monthlyTotal,\n    costPerLead: j.costPerLead,\n    budgetCapEur: j.budgetCapEur,\n    withinBudget: j.withinBudget,\n    stackHealth: j.stackHealth,\n    topLayerName: j.topLayerName,\n    topLayerEur: j.topLayerEur,\n    recommendation: j.recommendation,\n    executiveBrief: j.executiveBrief,\n    layerBreakdownText: j.layerBreakdownText,\n    skipSheets: j.skipSheets,\n    skipSlack: j.skipSlack,\n    message: 'Presupuesto calculado · Europe/Madrid · Logixb2b · Import OK'\n  }\n}];"
      }
    }
  ],
  "connections": {
    "Manual Trigger presupuesto": {
      "main": [
        [
          {
            "node": "Config capas EUR",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config capas EUR": {
      "main": [
        [
          {
            "node": "Validar Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validar Config": {
      "main": [
        [
          {
            "node": "IF config valida",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF config valida": {
      "main": [
        [
          {
            "node": "Calcular TCO mensual",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack config invalida",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack config invalida": {
      "main": [
        [
          {
            "node": "Stop (config invalida)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calcular TCO mensual": {
      "main": [
        [
          {
            "node": "IF dentro del techo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF dentro del techo": {
      "main": [
        [
          {
            "node": "Passthrough techo OK",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "IF avisar Slack techo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Passthrough techo OK": {
      "main": [
        [
          {
            "node": "Restaurar cifras tras techo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF avisar Slack techo": {
      "main": [
        [
          {
            "node": "Slack techo superado",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Skip Slack techo (demo)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack techo superado": {
      "main": [
        [
          {
            "node": "Restaurar cifras tras techo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Skip Slack techo (demo)": {
      "main": [
        [
          {
            "node": "Restaurar cifras tras techo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restaurar cifras tras techo": {
      "main": [
        [
          {
            "node": "IF enableAiBrief",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF enableAiBrief": {
      "main": [
        [
          {
            "node": "Armar body OpenAI brief",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Sin brief LLM (passthrough)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Armar body OpenAI brief": {
      "main": [
        [
          {
            "node": "HTTP Brief OpenAI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Brief OpenAI": {
      "main": [
        [
          {
            "node": "Aplicar briefing OpenAI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aplicar briefing OpenAI": {
      "main": [
        [
          {
            "node": "Limpiar fila Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sin brief LLM (passthrough)": {
      "main": [
        [
          {
            "node": "Limpiar fila Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Limpiar fila Sheets": {
      "main": [
        [
          {
            "node": "IF escribir Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF escribir Sheets": {
      "main": [
        [
          {
            "node": "Append Sheets Presupuesto_Prospeccion",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Skip Sheets (demo)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append Sheets Presupuesto_Prospeccion": {
      "main": [
        [
          {
            "node": "Append Sheets Presupuesto_Historial",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append Sheets Presupuesto_Historial": {
      "main": [
        [
          {
            "node": "Restaurar cifras post Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Restaurar cifras post Sheets": {
      "main": [
        [
          {
            "node": "IF Slack OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Skip Sheets (demo)": {
      "main": [
        [
          {
            "node": "IF Slack OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Slack OK": {
      "main": [
        [
          {
            "node": "Slack resumen presupuesto OK",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Skip Slack OK (demo)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack resumen presupuesto OK": {
      "main": [
        [
          {
            "node": "Resumen final local",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Skip Slack OK (demo)": {
      "main": [
        [
          {
            "node": "Resumen final local",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
