{
  "name": "Cotizador WMC - Agente Gemini + Supabase - n8n 2.21.4 READY - PRUEBAS FINANZAS CORREGIDAS",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "cotizador-wmc-chat",
        "responseMode": "responseNode",
        "options": {
          "allowedOrigins": "*"
        }
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -256,
        1120
      ],
      "id": "6bd4fbd9-6f2d-4233-841f-109c49dc71a2",
      "name": "Webhook - Chat",
      "webhookId": "23c9a253-ba3d-47f0-85ca-21b3fde221c7"
    },
    {
      "parameters": {
        "jsCode": "const body = $json.body ?? {};\nconst headers = $json.headers ?? {};\nconst clean = (v, max = 10000) => typeof v === 'string' ? v.trim().slice(0, max) : '';\nconst auth = clean(headers.authorization || headers.Authorization, 12000);\nconst text = clean(body.text, 12000);\nconst conversationId = clean(body.conversationId, 200);\nconst messageId = clean(body.messageId, 200);\nconst userEmail = clean(body.userEmail, 320).toLowerCase();\nconst userName = clean(body.userName, 200);\nconst appSessionId = clean(body.appSessionId, 250) || `backend-${Date.now()}`;\nconst sentAt = clean(body.sentAt, 100) || new Date().toISOString();\nconst attachments = Array.isArray(body.attachments) ? body.attachments.slice(0, 8).map((a) => ({\n  filename: clean(a?.filename, 180), mediaType: clean(a?.mediaType, 120), url: clean(a?.url, 6000), storagePath: clean(a?.storagePath, 1000) || null,\n})) : [];\nif (!auth.startsWith('Bearer ')) throw new Error('Falta Authorization Bearer de Supabase.');\nif (!conversationId || !messageId) throw new Error('Faltan conversationId/messageId.');\nif (!text && attachments.length === 0) throw new Error('La solicitud no contiene texto ni archivos.');\nconst countryMap = [\n  ['GT','guatemala'],['CR','costa rica'],['SV','el salvador'],['SV','salvador'],['HN','honduras'],['NI','nicaragua']\n];\nconst low = text.toLowerCase();\nlet countryCode = null;\nfor (const [code, term] of countryMap) if (low.includes(term)) { countryCode = code; break; }\nreturn [{json:{auth,text,conversationId,messageId,userEmail,userName,appSessionId,sentAt,attachments,countryCode,startedAt:Date.now()}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        192,
        1120
      ],
      "id": "e76cc95f-8d3a-4b4d-bf25-94f8438fbdd0",
      "name": "Normalizar solicitud"
    },
    {
      "parameters": {
        "url": "=https://dbit.digitalcompass.agency/auth/v1/user",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"
            },
            {
              "name": "Authorization",
              "value": "={{ $json.auth }}"
            }
          ]
        },
        "options": {
          "timeout": 20000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        672,
        1120
      ],
      "id": "a33967fa-0e18-4df3-9b6d-60b7c58fcd9d",
      "name": "Supabase - validar JWT",
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "const req = $node[\"Normalizar solicitud\"].json;\nconst authUser = $json ?? {};\nconst email = String(authUser.email ?? '').trim().toLowerCase();\nconst id = String(authUser.id ?? '').trim();\nconst valid = Boolean(id && email && email.endsWith('@gomezleemarketing.com') && (!req.userEmail || req.userEmail === email));\nreturn [{json:{...req, userId:id, verifiedEmail:email, jwtValid:valid, jwtError: valid ? null : 'Sesión inválida o correo no coincide.'}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1056,
        1120
      ],
      "id": "b4d3336f-97c5-43df-bdbf-146c22502723",
      "name": "Validar identidad"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "0bfb8fea-20b0-4cf3-8e89-9160c914cc8e",
              "leftValue": "={{ $json.jwtValid }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1280,
        1120
      ],
      "id": "0f1608a5-5c88-4def-a4c4-b45584533f42",
      "name": "JWT válido?"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { ok:false, reply:'Tu sesión no es válida. Vuelve a iniciar sesión.' } }}",
        "options": {
          "responseCode": 401
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [
        1504,
        1360
      ],
      "id": "f0f3d61a-60ca-4a31-9ef4-898a3357b410",
      "name": "Responder 401"
    },
    {
      "parameters": {
        "url": "={{ 'https://dbit.digitalcompass.agency/rest/v1/cotizador_wmc_access?select=email,full_name,is_active,can_use_tarifario&email=eq.' + encodeURIComponent($json.verifiedEmail) + '&is_active=eq.true' + '&limit=1' }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"
            },
            {
              "name": "Authorization",
              "value": "={{ $json.auth }}"
            }
          ]
        },
        "options": {
          "timeout": 20000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1504,
        1040
      ],
      "id": "7c127c9b-ac74-4f1c-954f-a9709700c575",
      "name": "Supabase - consultar acceso",
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "const req = $node[\"Validar identidad\"].json;\nconst raw = $json ?? {};\n\n// n8n puede entregar la respuesta de PostgREST de varias formas:\n// 1) una fila directamente como $json (caso habitual cuando la respuesta HTTP es un array de 1 fila),\n// 2) un array,\n// 3) { data: [...] }, o\n// 4) { body: [...] } según la configuración/versión del nodo HTTP Request.\nlet row = null;\nif (Array.isArray(raw)) {\n  row = raw[0] ?? null;\n} else if (Array.isArray(raw?.data)) {\n  row = raw.data[0] ?? null;\n} else if (Array.isArray(raw?.body)) {\n  row = raw.body[0] ?? null;\n} else if (raw?.body && typeof raw.body === 'object' && !Array.isArray(raw.body)) {\n  row = raw.body;\n} else if (raw && typeof raw === 'object' && ('email' in raw || 'is_active' in raw || 'full_name' in raw)) {\n  row = raw;\n}\n\nconst normalizeEmail = (value) => String(value ?? '').trim().toLowerCase();\nconst rowEmail = normalizeEmail(row?.email);\nconst active = row?.is_active === true || String(row?.is_active ?? '').toLowerCase() === 'true';\nconst accessAllowed = Boolean(row && active && rowEmail === req.verifiedEmail);\n\nreturn [{\n  json: {\n    ...req,\n    accessAllowed,\n    fullName: row?.full_name || req.userName || req.verifiedEmail,\n    canUseTarifario: accessAllowed && (row?.can_use_tarifario === true || String(row?.can_use_tarifario ?? '').toLowerCase() === 'true'),\n    accessLookupOk: Boolean(row),\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1712,
        1040
      ],
      "id": "08b28c45-0d7f-4fdb-b8ed-3a473270e83e",
      "name": "Validar acceso"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "b01cd05b-234e-4d8d-a2f0-3f9b83cb857e",
              "leftValue": "={{ $json.accessAllowed }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1936,
        1040
      ],
      "id": "70927efa-1850-49dd-8dba-961e7db1ded5",
      "name": "Acceso permitido?"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { ok:false, reply:'No tienes acceso activo al Cotizador Walmart Connect.' } }}",
        "options": {
          "responseCode": 403
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [
        2160,
        1328
      ],
      "id": "e909c55a-b9c9-48b7-8ed0-1709510dd3ed",
      "name": "Responder 403"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/cotizador_wmc_get_my_agent_context",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"
            },
            {
              "name": "Authorization",
              "value": "={{ $node[\"Validar acceso\"].json.auth }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({p_query:$node[\"Validar acceso\"].json.text,p_country_code:$node[\"Validar acceso\"].json.countryCode,p_limit:20}) }}",
        "options": {
          "timeout": 30000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2160,
        928
      ],
      "id": "bb512cd4-00f6-4e3b-b570-882b7e809a4d",
      "name": "Contexto Supabase"
    },
    {
      "parameters": {
        "url": "={{ 'https://dbit.digitalcompass.agency/rest/v1/cotizador_wmc_messages?select=role,content,attachments,created_at&conversation_id=eq.' + encodeURIComponent($node[\"Validar acceso\"].json.conversationId) + '&order=created_at.asc&limit=40' }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"
            },
            {
              "name": "Authorization",
              "value": "={{ $node[\"Validar acceso\"].json.auth }}"
            }
          ]
        },
        "options": {
          "timeout": 20000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2384,
        928
      ],
      "id": "0d579c5b-76c3-4fdf-b944-1885a7893479",
      "name": "Historial Supabase",
      "alwaysOutputData": true,
      "continueOnFail": true
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/cotizador_wmc_get_my_pending_actions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"
            },
            {
              "name": "Authorization",
              "value": "={{ $node[\"Validar acceso\"].json.auth }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({p_limit:5}) }}",
        "options": {
          "timeout": 20000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2608,
        928
      ],
      "id": "0b8e60d4-c8f9-4f0a-a618-08cd0b6f8b60",
      "name": "Acciones pendientes",
      "alwaysOutputData": true,
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "const req = $node[\"Validar acceso\"].json;\nconst context = $node[\"Contexto Supabase\"].json ?? {};\nconst historyRaw = $node[\"Historial Supabase\"].json;\nconst pendingRaw = $node[\"Acciones pendientes\"].json;\n\nconst history = Array.isArray(historyRaw) ? historyRaw.slice(-30) : [];\nconst pending = Array.isArray(pendingRaw) ? pendingRaw : (Array.isArray(pendingRaw?.items) ? pendingRaw.items : []);\n\nconst system = `Eres el agente interno Cotizador Walmart Connect de GomezLee Marketing (GLM).\nTrabajas EXCLUSIVAMENTE con cotizaciones, presupuestos, tarifario, propuestas, ejecuciones y archivos relacionados con Walmart Connect/GLM. Si te piden algo fuera de este alcance, explica brevemente que no corresponde al Cotizador WMC.\n\nFUENTES Y REGLAS OBLIGATORIAS:\n1) El TARIFARIO SUPABASE incluido en CONTEXTO es la fuente oficial vigente. Nunca inventes una tarifa oficial.\n2) Los HISTÓRICOS son referencia para elementos específicos que no existan en el tarifario. Si debes estimar, usa referencias comparables de la forma más realista posible y marca isEstimated=true, estimationReason y confidence 0..1. Nunca presentes una estimación como tarifa oficial.\n3) Las cotizaciones Walmart Connect salen en USD y también incluyen moneda local usando el tipo de cambio entregado por CONTEXTO.\n4) Conserva la estructura funcional oficial WMC: Personal, Mobiliario - Equipo, Insumos de degustación y/o preparación, Logística e Instalación, Fee de agencia, total, condiciones comerciales, vigencia, observaciones/exclusiones y referencias visuales.\n5) Personal: total = unidades × días × costo unitario. Verifica TODA la matemática, subtotales, total USD y total local antes de responder.\n6) Respeta el país. No traslades precios de un país a otro salvo que el CONTEXTO lo permita. Referencias confirmadas para Guatemala: Impulsadora A capital USD 39/día; Supervisor capital USD 62/día; camisa polo/uniforme estándar USD 20/unidad.\n7) RTM/RTM+ suele implicar arcos, backpanels, mesas degustadoras, alfombras y montaje; cotizaciones de islas suelen concentrarse en impulso/personal, mobiliario y rebranding. No agregues partidas no solicitadas sin aclararlo.\n8) En CAM/multipaís, genera la combinación país × formato requerida; por ejemplo, 5 países × 3 formatos puede implicar 15 cotizaciones/hojas.\n9) Cuando Walmart haya dado el OK y pidan mensualización, usa action=generate_monthly_glm.\n10) Cambios de tarifario: JAMÁS los ejecutes silenciosamente. Primera solicitud -> tariff_change_proposal. Solo después de confirmación explícita del mismo usuario -> confirm_tariff_change con pendingActionId.\n11) Si permissions.canUseTarifario=false, NO propongas ni confirmes modificaciones del tarifario; explica que el usuario no tiene ese permiso.\n12) Si faltan datos esenciales para un archivo confiable, pregunta exactamente lo que falta y usa action=chat. ANTES de action=generate_quote debes tener confirmados por el usuario los datos que determinan el precio: país, formato, duración, cantidad y tipo/categoría del personal, y ubicación/cobertura cuando la tarifa dependa de Capital/Interior/mixta.\n13) PROHIBIDO asumir partidas no solicitadas: no agregues supervisor, uniformes, logística, mobiliario, insumos, rebranding ni ningún otro rubro solo porque aparezca en históricos o sea frecuente. Solo inclúyelos si el usuario los pidió explícitamente o los confirmó después de que tú preguntaras. Si el usuario dice simplemente \"impulsadoras\" y existen categorías A/AA/AAA o diferencias por ubicación, pregunta la categoría/ubicación antes de generar el archivo.\n14) El Fee de agencia sí forma parte estructural del machote oficial. Usa la tasa oficial que venga en CONTEXTO; si no existe una tasa explícita, usa 6% como valor del machote. totalUsd y totalLocal SIEMPRE deben incluir el fee. Fórmula obligatoria: subtotalUsd = suma de partidas que no sean Fee; feeUsd = subtotalUsd × feeRate; totalUsd = subtotalUsd + feeUsd; totalLocal = totalUsd × exchangeRate.\n15) Nunca completes silenciosamente Capital, Interior o mixta a partir del país. Si ese dato cambia la tarifa y no fue dicho, pregunta.\n16) Los adjuntos se entregan como metadatos y URLs firmadas/temporales. No inventes contenido de un adjunto que no esté disponible como texto en el contexto; si falta su contenido, indícalo claramente y solicita el dato mínimo necesario.\n17) Para referencias visuales o artes, genera prompts específicos, realistas y coherentes con el mobiliario/activación solicitada.\n18) Para propuestas en Google Slides, mantén estilo Walmart Connect: limpio, corporativo, azul marino profundo, títulos simples y visuales grandes, evitando inventar claims o especificaciones no entregadas.\n19) Cuando el usuario pida una imagen directamente, usa action=generate_image.\n20) Devuelve SOLO JSON válido, sin markdown ni texto fuera del JSON.\n\nESQUEMA DE RESPUESTA:\n{\n  \"reply\":\"respuesta para el usuario\",\n  \"action\":\"chat|generate_quote|generate_proposal|generate_image|generate_monthly_glm|tariff_change_proposal|confirm_tariff_change\",\n  \"requiresConfirmation\":false,\n  \"pendingAction\":null,\n  \"pendingActionId\":null,\n  \"quote\":null,\n  \"artifact\":null,\n  \"warnings\":[],\n  \"confidence\":0.0\n}\n\nCuando action=generate_quote, quote debe incluir:\ncountryCode,country,brandSolution,project,advertiser,format,exchangeRate,currencyCode,totalUsd,totalLocal,status,items[],conditions,validity,observations,visualReferences.\nCada item: section,description,units,days,unitCostUsd,totalUsd,unitCostLocal,totalLocal,isEstimated,confidence,estimationReason,metadata.\nNo incluyas el Fee como una partida inventada para cuadrar números: reporta agencyFeeRate (decimal, por ejemplo 0.06), subtotalUsd, agencyFeeUsd, agencyFeeLocal, totalUsd y totalLocal; los totales deben cuadrar matemáticamente.\nvisualReferences puede contener URLs existentes o objetos {title,imagePrompt,imageUrl}. Si el usuario pide una referencia visual nueva para la cotización, incluye un objeto con imagePrompt; el workflow generará esa imagen con el nodo nativo de Gemini y la insertará en Referencias visuales del Google Sheet.\n\nCuando action=generate_proposal:\nartifact={title,subtitle,slides:[{type:\"cover|section|visual|content|thankyou\",title,subtitle,body,imagePrompt,imageUrl}]}\n\nCuando action=generate_image:\nartifact={title,imagePrompt,aspectRatio}\n\nCuando action=generate_monthly_glm:\nartifact={title,sourceQuoteId,monthlyLines,metadata}\n\nCuando action=tariff_change_proposal:\npendingAction={operation:\"insert|update|deactivate\",tariffItemId,item,reason}.`;\n\nconst attachments = (req.attachments || []).map(a => ({\n  filename: a.filename || '',\n  mediaType: a.mediaType || '',\n  url: a.url || '',\n  storagePath: a.storagePath || null\n}));\n\nconst payload = {\n  currentRequest: req.text,\n  countryHint: req.countryCode,\n  user: {name:req.fullName,email:req.verifiedEmail},\n  permissions: {canUseTarifario:Boolean(req.canUseTarifario)},\n  attachments,\n  CONTEXTO: context,\n  ACCIONES_PENDIENTES: pending,\n  HISTORIAL: history.map(h => ({\n    role:h.role,\n    content:h.content,\n    attachments:h.attachments || [],\n    createdAt:h.created_at\n  }))\n};\n\nconst attachmentLinks = attachments\n  .filter(a => a.url)\n  .map((a,i) => `Adjunto ${i+1}: ${a.filename || 'archivo'} (${a.mediaType || 'tipo no indicado'})\\nURL: ${a.url}`)\n  .join('\\n\\n');\n\nconst geminiPrompt = [\n  'SOLICITUD Y CONTEXTO EN JSON:',\n  JSON.stringify(payload),\n  attachmentLinks ? '\\nENLACES DE ADJUNTOS PARA URL CONTEXT:\\n' + attachmentLinks : ''\n].join('\\n\\n');\n\nreturn [{json:{...req,system,geminiPrompt,model:\"gemini-3.7-flash\"}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2832,
        928
      ],
      "id": "ff64dd79-9c8f-4275-a99a-c93b88dd151a",
      "name": "Construir Gemini"
    },
    {
      "parameters": {
        "jsCode": "const req = $node[\"Construir Gemini\"].json;\nconst raw = $json ?? {};\n\nlet parsed = null;\nlet text = '';\n\n// El nodo nativo de Gemini puede devolver JSON ya simplificado o contenido en parts.\nif (raw && typeof raw === 'object' && typeof raw.action === 'string') {\n  parsed = raw;\n  try { text = JSON.stringify(raw); } catch {}\n}\n\nif (!parsed) {\n  if (typeof raw.mergedResponse === 'string') {\n    text = raw.mergedResponse;\n  } else if (raw.content?.parts && Array.isArray(raw.content.parts)) {\n    text = raw.content.parts.map(p => p?.text || '').join('');\n  } else if (raw.candidates?.[0]?.content?.parts && Array.isArray(raw.candidates[0].content.parts)) {\n    text = raw.candidates[0].content.parts.map(p => p?.text || '').join('');\n  } else if (typeof raw.text === 'string') {\n    text = raw.text;\n  } else if (typeof raw.output === 'string') {\n    text = raw.output;\n  } else if (typeof raw.response === 'string') {\n    text = raw.response;\n  } else if (raw.output && typeof raw.output.text === 'string') {\n    text = raw.output.text;\n  }\n\n  try {\n    const cleaned = String(text || '').trim()\n      .replace(/^```(?:json)?\\s*/i,'')\n      .replace(/\\s*```$/,'');\n    parsed = JSON.parse(cleaned);\n  } catch {\n    parsed = {\n      reply:'No pude interpretar de forma segura la respuesta del modelo. Intenta nuevamente.',\n      action:'chat',\n      warnings:['La respuesta de Gemini no fue JSON válido.'],\n      confidence:0\n    };\n  }\n}\n\nconst allowed = new Set([\n  'chat','generate_quote','generate_proposal','generate_image',\n  'generate_monthly_glm','tariff_change_proposal','confirm_tariff_change'\n]);\nlet action = allowed.has(parsed?.action) ? parsed.action : 'chat';\n\n// Seguridad adicional: usuarios sin permiso de Tarifario no pueden pedir mutaciones.\nif (!req.canUseTarifario && ['tariff_change_proposal','confirm_tariff_change'].includes(action)) {\n  action = 'chat';\n  parsed.reply = 'No tienes habilitado el permiso para modificar el Tarifario.';\n  parsed.pendingAction = null;\n  parsed.pendingActionId = null;\n  parsed.requiresConfirmation = false;\n}\n\n// Recalculo financiero determinista: Gemini propone la estructura, pero n8n valida toda la matemática.\nfunction n_(value, fallback = 0) {\n  const n = Number(value);\n  return Number.isFinite(n) ? n : fallback;\n}\nfunction money_(value) {\n  return Math.round((n_(value, 0) + Number.EPSILON) * 100) / 100;\n}\nfunction isFeeSection_(section) {\n  return String(section || '').toLowerCase().normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').includes('fee');\n}\nfunction normalizeQuoteFinancials_(input) {\n  if (!input || typeof input !== 'object') return input ?? null;\n  const q = JSON.parse(JSON.stringify(input));\n  const rate = n_(q.exchangeRate ?? q.exchange_rate, 1) > 0 ? n_(q.exchangeRate ?? q.exchange_rate, 1) : 1;\n  const originalItems = Array.isArray(q.items) ? q.items : [];\n\n  let baseUsd = 0;\n  let explicitFeeUsd = 0;\n  q.items = originalItems.map((raw) => {\n    const it = raw && typeof raw === 'object' ? { ...raw } : {};\n    const units = n_(it.units ?? it.quantity, 0);\n    const daysRaw = it.days;\n    const days = (daysRaw === '' || daysRaw === null || daysRaw === undefined) ? 1 : n_(daysRaw, 1);\n    const unitUsd = n_(it.unitCostUsd ?? it.unit_cost_usd, 0);\n    const computedUsd = money_(units * days * unitUsd);\n    const suppliedUsd = n_(it.totalUsd ?? it.total_usd, computedUsd);\n    const totalUsd = unitUsd > 0 && units > 0 ? computedUsd : money_(suppliedUsd);\n    const unitLocal = money_(unitUsd * rate);\n    const totalLocal = money_(totalUsd * rate);\n\n    if (isFeeSection_(it.section)) explicitFeeUsd += totalUsd;\n    else baseUsd += totalUsd;\n\n    return {\n      ...it,\n      units,\n      days,\n      unitCostUsd: money_(unitUsd),\n      totalUsd,\n      unitCostLocal: unitLocal,\n      totalLocal,\n    };\n  });\n\n  baseUsd = money_(baseUsd);\n  let feeRate = n_(q.agencyFeeRate ?? q.feeRate, NaN);\n  if (!Number.isFinite(feeRate) && explicitFeeUsd > 0 && baseUsd > 0) feeRate = explicitFeeUsd / baseUsd;\n  if (!Number.isFinite(feeRate) || feeRate < 0) feeRate = 0.06;\n  if (feeRate > 1) feeRate = feeRate / 100;\n\n  const feeUsd = money_(baseUsd * feeRate);\n  const totalUsd = money_(baseUsd + feeUsd);\n  const feeLocal = money_(feeUsd * rate);\n  const totalLocal = money_(totalUsd * rate);\n\n  q.exchangeRate = rate;\n  q.agencyFeeRate = feeRate;\n  q.subtotalUsd = baseUsd;\n  q.subtotalLocal = money_(baseUsd * rate);\n  q.agencyFeeUsd = feeUsd;\n  q.agencyFeeLocal = feeLocal;\n  q.totalUsd = totalUsd;\n  q.totalLocal = totalLocal;\n  return q;\n}\n\nif (action === 'generate_quote' && parsed?.quote) {\n  const previousTotal = Number(parsed.quote.totalUsd);\n  parsed.quote = normalizeQuoteFinancials_(parsed.quote);\n  if (Number.isFinite(previousTotal) && Math.abs(previousTotal - parsed.quote.totalUsd) > 0.01) {\n    parsed.warnings = Array.isArray(parsed.warnings) ? parsed.warnings : [];\n    parsed.warnings.push(`Total recalculado automáticamente para incluir el Fee de agencia: USD ${parsed.quote.totalUsd.toFixed(2)}.`);\n  }\n}\n\nconst normalized = {\n  reply: String(parsed?.reply || '').trim() || 'Listo.',\n  action,\n  requiresConfirmation: Boolean(parsed?.requiresConfirmation || action === 'tariff_change_proposal'),\n  pendingAction: parsed?.pendingAction ?? null,\n  pendingActionId: parsed?.pendingActionId ?? null,\n  quote: parsed?.quote ?? null,\n  artifact: parsed?.artifact ?? null,\n  warnings: Array.isArray(parsed?.warnings) ? parsed.warnings.map(String).slice(0,10) : [],\n  confidence: Number.isFinite(Number(parsed?.confidence)) ? Math.max(0,Math.min(1,Number(parsed.confidence))) : null,\n  model: 'gemini-3.7-flash'\n};\n\nreturn [{json:{...req,agent:normalized,rawModelText:text || JSON.stringify(parsed),durationMs:Date.now()-req.startedAt}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3680,
        896
      ],
      "id": "8c53f71f-e432-4502-b6af-63b88506b95d",
      "name": "Interpretar Gemini"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/cotizador_wmc_process_my_agent_action",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"
            },
            {
              "name": "Authorization",
              "value": "={{ $json.auth }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({p_action:$json.agent,p_conversation_id:$json.conversationId,p_display_name:$json.fullName,p_session_id:$json.appSessionId}) }}",
        "options": {
          "timeout": 30000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3904,
        896
      ],
      "id": "b29ae15b-1df6-4354-8b61-5ff1b8419b03",
      "name": "Procesar acción sensible",
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "const x = $node[\"Interpretar Gemini\"].json;\nreturn [{json:{\n  action:x.agent.action,\n  quote:x.agent.quote,\n  artifact:x.agent.artifact,\n  user:{id:x.userId,email:x.verifiedEmail,name:x.fullName},\n  conversationId:x.conversationId,\n  appSessionId:x.appSessionId,\n  model:x.agent.model,\n  auth:x.auth\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4128,
        896
      ],
      "id": "0fd9a611-5c2e-417c-bae3-1a52e5d06e33",
      "name": "Preparar artefacto"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://agenteit.digitalcompass.agency/webhook/cotizador-wmc-artifacts",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "={{ $json.auth }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({action:$json.action,quote:$json.quote,artifact:$json.artifact,user:$json.user,conversationId:$json.conversationId,appSessionId:$json.appSessionId,model:$json.model}) }}",
        "options": {
          "timeout": 180000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        4336,
        896
      ],
      "id": "c561406f-7235-4139-84aa-c4a7582f3809",
      "name": "Generar artefacto",
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "const x = $node[\"Interpretar Gemini\"].json;\nlet actionResult = {};\nlet artifactResult = {};\ntry { actionResult = $node[\"Procesar acción sensible\"].json ?? {}; } catch {}\ntry { artifactResult = $node[\"Generar artefacto\"].json ?? {}; } catch {}\nconst out = {...x.agent};\nif (actionResult.pendingActionId) out.pendingActionId = actionResult.pendingActionId;\nif (typeof actionResult.requiresConfirmation === 'boolean') out.requiresConfirmation = actionResult.requiresConfirmation;\nif (actionResult.ok === false && actionResult.error) {\n  out.warnings = [...(out.warnings||[]), String(actionResult.error)];\n  out.reply += `\\n\\n⚠️ ${actionResult.error}`;\n}\nif (artifactResult?.artifactUrl) {\n  out.artifactUrl = artifactResult.artifactUrl;\n  out.artifactType = artifactResult.artifactType || null;\n  out.quoteId = artifactResult.quoteId || null;\n} else if (['generate_quote','generate_proposal','generate_image','generate_monthly_glm'].includes(out.action)) {\n  const err = artifactResult?.error || artifactResult?.message || 'No se pudo generar el archivo solicitado.';\n  out.warnings = [...(out.warnings||[]), String(err)];\n  out.reply += `\\n\\n⚠️ ${err}`;\n}\nreturn [{json:{...x, final:{ok:true,...out}}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4560,
        896
      ],
      "id": "ba9f9bd0-02eb-41c7-81b2-789922d93942",
      "name": "Respuesta final"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://dbit.digitalcompass.agency/rest/v1/rpc/cotizador_wmc_log_my_agent_run",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "apikey",
              "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"
            },
            {
              "name": "Authorization",
              "value": "={{ $json.auth }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({p_conversation_id:$json.conversationId,p_prompt:$json.text,p_response:$json.final.reply,p_action:$json.final.action,p_model:$json.final.model,p_duration_ms:$json.durationMs,p_status:'ok',p_metadata:{warnings:$json.final.warnings||[],confidence:$json.final.confidence,hasArtifact:!!$json.final.artifactUrl}}) }}",
        "options": {
          "timeout": 20000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        4784,
        896
      ],
      "id": "0aa3b3a0-53de-4bd7-8616-fbea70545363",
      "name": "Registrar ejecución",
      "continueOnFail": true
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $node[\"Respuesta final\"].json.final }}",
        "options": {
          "responseCode": 200
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [
        5008,
        896
      ],
      "id": "60d2f7c5-b06a-4336-ac66-411d8bc2ed05",
      "name": "Responder al chat"
    },
    {
      "parameters": {
        "jsCode": "// Configuración local del workflow: NO usa variables de entorno.\nreturn [{json:{\n  ...$json,\n  config:{\n    supabaseUrl:\"https://dbit.digitalcompass.agency\",\n    supabaseAnonKey:\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE\",\n    artifactsWebhookUrl:\"https://agenteit.digitalcompass.agency/webhook/cotizador-wmc-artifacts\"\n  }\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -32,
        1120
      ],
      "id": "7e185670-291a-4b5c-9d2f-9650b55a8430",
      "name": "Configuración - sin ENV"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "={{ $json.system + '\\n\\n' + $json.geminiPrompt }}",
        "batching": {}
      },
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.7,
      "position": [
        3056,
        928
      ],
      "id": "ec4ef453-1485-40f3-b54e-ee071ad0daf0",
      "name": "Gemini - Basic LLM Chain",
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 1500
    },
    {
      "parameters": {
        "modelName": "models/gemini-3.7-flash",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "typeVersion": 1.1,
      "position": [
        3056,
        1136
      ],
      "id": "69d51497-c025-4911-b793-b82cd2c36f62",
      "name": "Gemini 3.7 Flash - Chat Model",
      "credentials": {
        "googlePalmApi": {
          "id": "jvsXYwL6IOoY2DBU",
          "name": "Isaac - Gemini Api Pago"
        }
      }
    }
  ],
  "connections": {
    "Webhook - Chat": {
      "main": [
        [
          {
            "node": "Configuración - sin ENV",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalizar solicitud": {
      "main": [
        [
          {
            "node": "Supabase - validar JWT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Supabase - validar JWT": {
      "main": [
        [
          {
            "node": "Validar identidad",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validar identidad": {
      "main": [
        [
          {
            "node": "JWT válido?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JWT válido?": {
      "main": [
        [
          {
            "node": "Supabase - consultar acceso",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Responder 401",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Supabase - consultar acceso": {
      "main": [
        [
          {
            "node": "Validar acceso",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validar acceso": {
      "main": [
        [
          {
            "node": "Acceso permitido?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Acceso permitido?": {
      "main": [
        [
          {
            "node": "Contexto Supabase",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Responder 403",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Contexto Supabase": {
      "main": [
        [
          {
            "node": "Historial Supabase",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Historial Supabase": {
      "main": [
        [
          {
            "node": "Acciones pendientes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Acciones pendientes": {
      "main": [
        [
          {
            "node": "Construir Gemini",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Construir Gemini": {
      "main": [
        [
          {
            "node": "Gemini - Basic LLM Chain",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Interpretar Gemini": {
      "main": [
        [
          {
            "node": "Procesar acción sensible",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Procesar acción sensible": {
      "main": [
        [
          {
            "node": "Preparar artefacto",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Preparar artefacto": {
      "main": [
        [
          {
            "node": "Generar artefacto",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generar artefacto": {
      "main": [
        [
          {
            "node": "Respuesta final",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Respuesta final": {
      "main": [
        [
          {
            "node": "Registrar ejecución",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Registrar ejecución": {
      "main": [
        [
          {
            "node": "Responder al chat",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configuración - sin ENV": {
      "main": [
        [
          {
            "node": "Normalizar solicitud",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini - Basic LLM Chain": {
      "main": [
        [
          {
            "node": "Interpretar Gemini",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini 3.7 Flash - Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Gemini - Basic LLM Chain",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  },
  "versionId": "41c2e0e2-451e-43ec-ada4-9dee7ed171e0",
  "meta": {
    "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70"
  },
  "id": "rtnO23gHlJfj45pA",
  "tags": []
}