{
  "nodes": [
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "simple": false,
        "filters": {
          "q": "subject:\"cdp-enviar-skus\""
        },
        "options": {}
      },
      "type": "n8n-nodes-base.gmailTrigger",
      "typeVersion": 1.3,
      "position": [
        -16,
        2624
      ],
      "id": "1141e3d7-acbb-44a3-b82e-e953ac55cf8c",
      "name": "Gmail Trigger",
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {}
      },
      "id": "f30ebe85-41c1-4c89-95f6-be31338e7ce7",
      "name": "\ud83d\udcf1 Trigger Telegram",
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1.1,
      "position": [
        -240,
        1920
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "notes": "v0.6.0: Recebe SKUs via Telegram (texto ou arquivo .xlsx/.csv). Necessita configurar credenciais do bot (@BotFather)."
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 1
          },
          "conditions": [
            {
              "id": "robust-check",
              "leftValue": "={{ !$json.error && $json.valid_skus > 0 }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "22d612e4-b501-45b6-9ea0-67acd12c2cd4",
      "name": "\ud83d\udce7 Email entrada OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1328,
        3200
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 1
          },
          "conditions": [
            {
              "id": "robust-check",
              "leftValue": "={{ !$json.error && $json.valid_skus > 0 }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b56b187b-aedb-4316-ac84-90bce238562f",
      "name": "\ud83d\udcf1 Telegram entrada OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1328,
        2768
      ]
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "1IGhsIhrwlnMaCduR-W-eIi9O4mMO2pPYjE-tefgIPII",
          "mode": "list",
          "cachedResultName": "cdp_skus",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IGhsIhrwlnMaCduR-W-eIi9O4mMO2pPYjE-tefgIPII/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": "SKUs",
          "mode": "name",
          "cachedResultName": "SKUs",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IGhsIhrwlnMaCduR-W-eIi9O4mMO2pPYjE-tefgIPII/edit#gid=0"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "row_number": "={{ $json.row_number }}",
            "PROCESSADO": "={{ $json.PROCESSADO }}",
            "ENCONTRADO": "={{ $json.ENCONTRADO }}",
            "NOTIFICADO": "={{ $json.NOTIFICADO }}"
          },
          "matchingColumns": [
            "row_number"
          ],
          "schema": [
            {
              "id": "row_number",
              "displayName": "row_number",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "canBeUsedToMatch": true,
              "readOnly": true,
              "removed": false
            },
            {
              "id": "PROCESSADO",
              "displayName": "PROCESSADO",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "ENCONTRADO",
              "displayName": "ENCONTRADO",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "NOTIFICADO",
              "displayName": "NOTIFICADO",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {}
      },
      "id": "1fdb8d2b-e29c-4bad-b483-291399bdf1d4",
      "name": "\u2705 Marcar PROCESSADO \u2192 CDP_SKUs",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        2448,
        2336
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "notes": "Updates PROCESSADO/ENCONTRADO/NOTIFICADO=\u23f3 Processando for each dispatched sheet row, matched by row_number so duplicate CODIGO rows are initialized too."
    },
    {
      "parameters": {
        "jsCode": "// cdp_router \u2014 pair scraper HTTP responses to SKUs for sheet PROCESSADO updates.\n\nconst out = [];\nconst PROCESSING = '\u23f3 Processando';\n\nfunction responseAccepted(resp) {\n  return Boolean(resp && (resp.accepted || resp.job_id || resp.body?.job_id));\n}\n\nfunction pushRowsForBatch(batch) {\n  const rows = Array.isArray(batch.sheet_rows) ? batch.sheet_rows : batch.items || [];\n  for (const it of rows) {\n    if (!it || !it.sku) continue;\n    const rowNumber = it.row_number;\n    if (rowNumber === undefined || rowNumber === null || rowNumber === '') continue;\n    out.push({\n      json: {\n        sku: String(it.sku).trim(),\n        row_number: rowNumber,\n        PROCESSADO: PROCESSING,\n        ENCONTRADO: PROCESSING,\n        NOTIFICADO: PROCESSING,\n      },\n    });\n  }\n}\n\nconst first = $input.first().json || {};\nif (first.parallel_dispatch) {\n  const responses = Array.isArray(first.scraper_responses) ? first.scraper_responses : [];\n  const batches = Array.isArray(first.scraper_batches) ? first.scraper_batches : [];\n  for (let i = 0; i < responses.length; i++) {\n    const resp = responses[i];\n    if (!responseAccepted(resp)) continue;\n    const batch =\n      batches.find((b) => Number(b.batch_index) === Number(resp.batch_index)) || batches[i];\n    if (batch) pushRowsForBatch(batch);\n  }\n  return out;\n}\n\nconst httpItems = $input.all();\nconst batches = $('\u2699\ufe0f Formatar Payload Scraper').all();\n\nfor (let i = 0; i < httpItems.length; i++) {\n  const resp = httpItems[i].json;\n  const hasJob = Boolean(resp.job_id);\n  const sc = resp.statusCode != null ? Number(resp.statusCode) : hasJob ? 200 : 0;\n  if (!hasJob && (sc < 200 || sc >= 300)) continue;\n  const batch = batches[i]?.json;\n  if (!batch) continue;\n  pushRowsForBatch(batch);\n}\nreturn out;\n",
        "mode": "runOnceForAllItems"
      },
      "id": "c738d1b2-1afd-4163-ba21-6b9800541563",
      "name": "\ud83d\udd17 Emparelhar SKUs \u2192 PROCESSADO",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2224,
        2336
      ],
      "notes": "Pairs each HTTP response with Formatar batch by index; emits one item per SKU for all channels. Skips non-2xx responses."
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "1ZBU2d3XVsngOYQH12yU7Mg9DcIzVet2dDmhMtZqHSOo",
          "mode": "list",
          "cachedResultName": "cdp_resultados",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1ZBU2d3XVsngOYQH12yU7Mg9DcIzVet2dDmhMtZqHSOo/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": "Historico",
          "mode": "name",
          "cachedResultName": "Historico",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1ZBU2d3XVsngOYQH12yU7Mg9DcIzVet2dDmhMtZqHSOo/edit#gid=0"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "job_id": "={{ $json.job_id }}",
            "origem": "={{ $json.origem }}",
            "solicitante": "={{ $json.solicitante }}",
            "disparado_em": "={{ $json.disparado_em }}",
            "concluido_em": "={{ $json.concluido_em }}",
            "tempo_segundos": "={{ $json.tempo_segundos }}",
            "status": "={{ $json.status }}",
            "skus_lidos": "={{ $json.skus_lidos }}",
            "skus_validos": "={{ $json.skus_validos }}",
            "skus_encontrados": "={{ $json.skus_encontrados }}",
            "skus_falhos": "={{ $json.skus_falhos }}",
            "taxa_sucesso_sku": "={{ $json.taxa_sucesso_sku }}",
            "taxa_sucesso_sites": "={{ $json.taxa_sucesso_sites }}",
            "sites_pesquisados": "={{ $json.sites_pesquisados }}",
            "resumo_sites": "={{ $json.resumo_sites }}",
            "lista_skus_csv": "={{ $json.lista_skus_csv }}",
            "skus_repetidos": "={{ $json.skus_repetidos || '\u2014' }}",
            "job_error": "={{ $json.job_error }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "job_id",
              "displayName": "job_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "origem",
              "displayName": "origem",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "solicitante",
              "displayName": "solicitante",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "disparado_em",
              "displayName": "disparado_em",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "concluido_em",
              "displayName": "concluido_em",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "tempo_segundos",
              "displayName": "tempo_segundos",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "skus_lidos",
              "displayName": "skus_lidos",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "skus_validos",
              "displayName": "skus_validos",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "skus_encontrados",
              "displayName": "skus_encontrados",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "skus_falhos",
              "displayName": "skus_falhos",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "taxa_sucesso_sku",
              "displayName": "taxa_sucesso_sku",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "taxa_sucesso_sites",
              "displayName": "taxa_sucesso_sites",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "sites_pesquisados",
              "displayName": "sites_pesquisados",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "resumo_sites",
              "displayName": "resumo_sites",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "lista_skus_csv",
              "displayName": "lista_skus_csv",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "skus_repetidos",
              "displayName": "skus_repetidos",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "job_error",
              "displayName": "job_error",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {}
      },
      "id": "8f40d678-48f1-446b-9ef1-4199ef0a5695",
      "name": "\ud83d\udcdd Erro \u2192 CDP_Resultados (Hist\u00f3rico)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        2448,
        2720
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// cdp_router - format Scraper dispatch failures for Historico and optional email alert.\n\nfunction env(name) {\n  try {\n    if (typeof $env !== 'undefined' && $env && $env[name]) {\n      return String($env[name]).trim();\n    }\n  } catch (e) {}\n  try {\n    if (typeof process !== 'undefined' && process.env && process.env[name]) {\n      return String(process.env[name]).trim();\n    }\n  } catch (e) {}\n  return '';\n}\n\nfunction workflowName() {\n  try {\n    if (typeof $workflow !== 'undefined' && $workflow && $workflow.name) {\n      return String($workflow.name);\n    }\n  } catch (e) {}\n  return '';\n}\n\nfunction isDevWorkflow() {\n  return workflowName().trim().toLowerCase().startsWith('dev -') || env('CDP_ENV').toLowerCase() === 'dev';\n}\n\nfunction envFor(name) {\n  if (isDevWorkflow() && name === 'NOTIFICATION_EMAIL_TO') {\n    return env('CDP_DEV_NOTIFICATION_EMAIL_TO');\n  }\n  return env(name);\n}\n\nfunction compactError(resp) {\n  const raw =\n    resp.error ||\n    resp.message ||\n    resp.detail ||\n    resp.body?.detail ||\n    resp.body?.message ||\n    resp.body?.error ||\n    JSON.stringify(resp).substring(0, 500);\n  return typeof raw === 'string' ? raw : JSON.stringify(raw);\n}\n\nfunction historicoFromError(item, resp, batch) {\n  const nowIso = new Date().toISOString();\n  const batchIndex = resp.batch_index || batch?.batch_index || 'unknown';\n  const totalBatches = resp.total_batches || batch?.total_batches || 'unknown';\n  const statusCode = resp.statusCode ?? resp.status_code ?? 'N/A';\n  const errorMsg = compactError(resp);\n  const items = Array.isArray(batch?.items) ? batch.items : Array.isArray(item.items) ? item.items : [];\n  const batchSize = Number(resp.batch_size || batch?.batch_size || items.length || 0);\n  const emailTo = String(\n    item.reply_email || item.email_from || item.email || envFor('NOTIFICATION_EMAIL_TO') || ''\n  ).trim();\n\n  let html = '<h2>CDP Job Dispatcher - Scraper API Error</h2>';\n  html += '<p><strong>Time:</strong> ' + nowIso + '</p>';\n  html += '<p><strong>Batch:</strong> ' + batchIndex + ' / ' + totalBatches + '</p>';\n  html += '<p><strong>HTTP Status:</strong> ' + statusCode + '</p>';\n  html += '<p><strong>Error:</strong></p>';\n  html += '<pre style=\"background:#fee2e2;padding:12px;border-radius:8px\">' + errorMsg + '</pre>';\n  html += '<p style=\"color:#718096;font-size:12px\">Automated alert from CDP Job Dispatcher.</p>';\n\n  return {\n    job_id: String(resp.job_id || 'dispatch-scraper-batch-' + batchIndex),\n    origem: 'dispatcher_error_scraper',\n    solicitante: String(item.reply_email || item.email_from || item.chat_id || ''),\n    disparado_em: String(resp.started_at || item.dispatched_at || nowIso),\n    concluido_em: nowIso,\n    tempo_segundos: '0',\n    status: '\u274c ERRO_DISPATCH',\n    skus_lidos: String(batchSize),\n    skus_validos: String(batchSize),\n    skus_encontrados: '0',\n    skus_falhos: String(batchSize),\n    taxa_sucesso_sku: '0%',\n    taxa_sucesso_sites: '0%',\n    sites_pesquisados: String(Array.isArray(batch?.sites) ? batch.sites.length : 0),\n    resumo_sites: '{}',\n    lista_skus_csv: String(items.map((i) => i.sku).filter(Boolean).join(', ')),\n    skus_repetidos: '\u2014',\n    job_error: '[HTTP ' + statusCode + '] ' + String(errorMsg).substring(0, 1000),\n    email_from: emailTo,\n    email_subject: 'Dispatcher Error - Scraper batch ' + batchIndex,\n    email_html: html,\n  };\n}\n\nconst item = $input.first().json || {};\n\nif (item.parallel_dispatch) {\n  const responses = Array.isArray(item.scraper_responses) ? item.scraper_responses : [];\n  const batches = Array.isArray(item.scraper_batches) ? item.scraper_batches : [];\n  return responses\n    .filter((resp) => !resp.accepted)\n    .map((resp, index) => {\n      const batch =\n        batches.find((b) => Number(b.batch_index) === Number(resp.batch_index)) || batches[index] || {};\n      return { json: historicoFromError(item, resp, batch) };\n    });\n}\n\nreturn [{ json: historicoFromError(item, item, item) }];\n",
        "mode": "runOnceForAllItems"
      },
      "id": "dd0fcac0-5213-41f7-81ec-702dc4be33b7",
      "name": "\u274c Formatar Erro de Despacho",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2224,
        2720
      ]
    },
    {
      "parameters": {
        "conditions": {
          "conditions": [
            {
              "id": "check-job-id",
              "leftValue": "={{ $json.job_id }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              }
            }
          ],
          "combinator": "and",
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          }
        },
        "options": {}
      },
      "id": "a5f7cb38-e051-42aa-9030-b442c1971b42",
      "name": "\u2705 API OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2000,
        2720
      ],
      "notes": "Routes to success or error path based on API response status."
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $json.api_jobs_url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "X-API-Key",
              "value": "={{ $json.api_key }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ items: $json.items, sites: $json.sites, callback_url: $json.callback_url, priority: $json.priority || 5, force_refresh: $json.force_refresh === true, batch_group_id: $json.batch_group_id, chat_id: $json.reply_channel === 'telegram' ? $json.chat_id : undefined, command_route: $json.command_route, metadata: $json.metadata, reply_channel: $json.reply_channel, command_origin: $json.command_origin, reply_email: $json.reply_email, notify: $json.notify }) }}",
        "options": {
          "batching": {
            "batch": {
              "batchSize": 1,
              "batchInterval": 3000
            }
          },
          "response": {
            "response": {
              "responseFormat": "json"
            }
          },
          "timeout": 60000
        }
      },
      "id": "e919ee2c-1f5e-47ca-879d-ce9b23fed935",
      "name": "\ud83d\ude80 POST \u2192 Scraper API (/jobs)",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1776,
        2720
      ],
      "notes": "Requires CDP_SCRAPER_API_BASE and CDP_API_KEY. Batches POSTs (1 per 3s) to avoid API 500 under parallel load. Retries 3\u00d7 on transient errors.",
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "// cdp_router \u2014 pass SKUs through; optional random sample via CDP_DISPATCH_SAMPLE_SIZE (0 = all).\n\nfunction env(name) {\n  try {\n    if (typeof $env !== 'undefined' && $env && $env[name]) {\n      return String($env[name]).trim();\n    }\n  } catch (e) {}\n  try {\n    if (typeof process !== 'undefined' && process.env && process.env[name]) {\n      return String(process.env[name]).trim();\n    }\n  } catch (e) {}\n  return '';\n}\nfunction envInt(name, defaultVal) {\n  const raw = env(name);\n  if (!raw) return defaultVal;\n  const n = parseInt(raw, 10);\n  return Number.isFinite(n) && n >= 0 ? n : defaultVal;\n}\n\nconst data = $input.first().json;\nconst all = Array.isArray(data.skus) ? data.skus : [];\nconst allSheetRows = Array.isArray(data.sheet_rows) ? data.sheet_rows : all;\nconst maxSkus = envInt('CDP_DISPATCH_SAMPLE_SIZE', 0);\nlet skus = all;\nlet sampled = false;\nif (maxSkus > 0 && all.length > maxSkus) {\n  const copy = [...all];\n  for (let i = copy.length - 1; i > 0; i--) {\n    const j = Math.floor(Math.random() * (i + 1));\n    const tmp = copy[i];\n    copy[i] = copy[j];\n    copy[j] = tmp;\n  }\n  skus = copy.slice(0, maxSkus);\n  sampled = true;\n}\nconst sampledSkuSet = new Set(\n  skus.map((row) => String(row?.sku || row?.SKU || row).trim().toUpperCase()).filter(Boolean)\n);\nconst sheetRows = allSheetRows.filter((row) => {\n  const sku = String(row?.sku || row?.SKU || row).trim().toUpperCase();\n  return sampledSkuSet.has(sku);\n});\n\nconst batchGroupId = 'bg-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);\ntry {\n  if (typeof $getWorkflowStaticData === 'function') {\n    const sd = $getWorkflowStaticData('global');\n    sd.cdp_last_batch_group_id = batchGroupId;\n  }\n} catch (e) {}\n\nlet commandRoute = 'analisar';\ntry {\n  const tg = $('\ud83d\udd00 Switch Comando (Telegram)').first().json;\n  if (tg && tg.route) commandRoute = String(tg.route);\n} catch (e) {}\nif (commandRoute === 'analisar') {\n  try {\n    const em = $('\ud83d\udd00 Switch Comando (Email)').first().json;\n    if (em && em.route) commandRoute = String(em.route);\n  } catch (e) {}\n}\n\nreturn [\n  {\n    json: {\n      ...data,\n      skus,\n      sheet_rows: sheetRows,\n      valid_skus: skus.length,\n      input_valid_skus: sheetRows.length,\n      dispatch_sampled: sampled,\n      dispatch_sample_limit: maxSkus,\n      dispatch_total_before_sample: all.length,\n      dispatch_sheet_rows_before_sample: allSheetRows.length,\n      batch_group_id: batchGroupId,\n      command_route: commandRoute,\n    },\n  },\n];\n",
        "mode": "runOnceForAllItems"
      },
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "name": "\ud83c\udfb2 Limitar SKUs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1440,
        2480
      ],
      "notes": "All valid SKUs. Parallel router: [0] API Diversos POST [1] Scraper POST [2] confirm. batch_group_id shared."
    },
    {
      "parameters": {
        "jsCode": "// cdp_router \u2014 Scraper POST /api/v1/jobs (force_refresh=false \u2192 Redis/PostgreSQL 24h cache).\n\nconst DEFAULT_BATCH_SIZE = 100;\nconst MAX_BATCH_SIZE = 100;\nconst DEFAULT_SITES = [\n  'gm',\n  'ml',\n  'vw',\n  'eu',\n  'pecadireta',\n  'melibox',\n  'goparts',\n  'procurapecas',\n  'ebay',\n];\nconst DEFAULT_SCRAPER_API_BASE =\n  'https://cdp-scrapers-api-prod.bravecoast-b14d791e.eastus2.azurecontainerapps.io';\nconst DEFAULT_N8N_WEBHOOK_BASE = 'https://automacao.tktechnologies.com.br';\nconst data = $input.first().json;\nconst skus = Array.isArray(data.skus) ? data.skus : [];\nconst sheetRows = Array.isArray(data.sheet_rows) ? data.sheet_rows : skus;\n\nfunction env(name) {\n  try {\n    if (typeof $env !== 'undefined' && $env && $env[name]) {\n      return String($env[name]).trim();\n    }\n  } catch (e) {}\n  try {\n    if (typeof process !== 'undefined' && process.env && process.env[name]) {\n      return String(process.env[name]).trim();\n    }\n  } catch (e) {}\n  return '';\n}\nfunction workflowName() {\n  try {\n    if (typeof $workflow !== 'undefined' && $workflow && $workflow.name) {\n      return String($workflow.name).trim();\n    }\n  } catch (e) {}\n  return '';\n}\nfunction isDevWorkflow() {\n  return /^DEV\\s*-/i.test(workflowName()) || /^dev$/i.test(env('CDP_ENV'));\n}\nfunction devEnvName(name) {\n  const map = {\n    CDP_SCRAPER_API_BASE: 'CDP_DEV_SCRAPER_API_BASE',\n    MUVSTOK_SCRAPER_API_BASE: 'CDP_DEV_SCRAPER_API_BASE',\n    CDP_API_KEY: 'CDP_DEV_API_KEY',\n    MUVSTOK_API_KEY: 'CDP_DEV_API_KEY',\n    API_KEY: 'CDP_DEV_API_KEY',\n    CDP_SCRAPER_BATCH_SIZE: 'CDP_DEV_SCRAPER_BATCH_SIZE',\n    CDP_SCRAPER_SITES: 'CDP_DEV_SCRAPER_SITES',\n    WEBHOOK_URL: 'CDP_DEV_WEBHOOK_URL',\n    CDP_N8N_WEBHOOK_URL: 'CDP_DEV_N8N_WEBHOOK_URL',\n    CDP_N8N_WEBHOOK_PATH: 'CDP_DEV_N8N_WEBHOOK_PATH',\n  };\n  return map[name] || '';\n}\nfunction envFor(name) {\n  if (!isDevWorkflow()) return env(name);\n  const mapped = devEnvName(name);\n  const value = mapped ? env(mapped) : '';\n  if (value) return value;\n  if (name === 'WEBHOOK_URL') return env('WEBHOOK_URL') || DEFAULT_N8N_WEBHOOK_BASE;\n  if (name === 'CDP_N8N_WEBHOOK_PATH') return 'webhook/dev-scraper-result';\n  return '';\n}\nfunction envList(name) {\n  const raw = envFor(name);\n  return raw ? raw.split(',').map((s) => s.trim()).filter(Boolean) : [];\n}\nfunction trimTrailingSlashes(value) {\n  let out = String(value || '').trim();\n  while (out.endsWith('/')) out = out.slice(0, -1);\n  return out;\n}\nfunction trimSlashes(value) {\n  let out = String(value || '').trim();\n  while (out.startsWith('/')) out = out.slice(1);\n  while (out.endsWith('/')) out = out.slice(0, -1);\n  return out;\n}\nfunction readStaticRequester() {\n  try {\n    if (typeof $getWorkflowStaticData === 'function') {\n      const sd = $getWorkflowStaticData('global');\n      if (sd && sd.cdp_sheet_requester) return sd.cdp_sheet_requester;\n    }\n  } catch (e) {}\n  return null;\n}\nfunction looksLikeEmail(value) {\n  return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(String(value || '').trim());\n}\nfunction skuKey(value) {\n  return String(value || '')\n    .trim()\n    .toUpperCase()\n    .replace(/[\\s\\-\\.\\\\/]/g, '');\n}\n\nconst configuredSites = envList('CDP_SCRAPER_SITES');\nconst sites = configuredSites.length ? configuredSites : DEFAULT_SITES;\nconst commandRoute = String(data.command_route || 'analisar');\nconst scraperApiBase = trimTrailingSlashes(\n  envFor('CDP_SCRAPER_API_BASE') ||\n    envFor('MUVSTOK_SCRAPER_API_BASE') ||\n    (isDevWorkflow() ? '' : DEFAULT_SCRAPER_API_BASE)\n);\nconst apiKey = envFor('CDP_API_KEY') || envFor('MUVSTOK_API_KEY') || envFor('API_KEY');\nconst batchSizeRaw = Number(envFor('CDP_SCRAPER_BATCH_SIZE') || DEFAULT_BATCH_SIZE);\nconst BATCH_SIZE = Math.max(\n  1,\n  Math.min(MAX_BATCH_SIZE, Number.isFinite(batchSizeRaw) ? batchSizeRaw : DEFAULT_BATCH_SIZE)\n);\n\nconst ctx = readStaticRequester();\nlet chatId = String(data.telegram_chat_id || data.chat_id || '').trim();\nlet emailFrom = String(data.email_from || '').trim();\nconst notifyRaw = String(data.notify || '').trim();\nlet commandOrigin = String(data.command_origin || data.origem || '').trim().toLowerCase();\nlet replyChannel = String(data.reply_channel || '').trim().toLowerCase();\nif (!emailFrom && looksLikeEmail(notifyRaw)) emailFrom = notifyRaw;\nconst dataHasChannel = Boolean(\n  replyChannel || commandOrigin === 'email' || commandOrigin === 'telegram' || emailFrom || chatId\n);\nif (ctx) {\n  if (!dataHasChannel && !replyChannel && ctx.reply_channel) {\n    replyChannel = String(ctx.reply_channel).trim().toLowerCase();\n  }\n  if (!dataHasChannel && !commandOrigin && ctx.command_origin) {\n    commandOrigin = String(ctx.command_origin).trim().toLowerCase();\n  }\n  if (!dataHasChannel && !emailFrom && ctx.email_from) emailFrom = String(ctx.email_from).trim();\n  const inputIsEmail = commandOrigin === 'email' || replyChannel === 'email' || emailFrom;\n  if (!inputIsEmail && !chatId && ctx.chat_id) chatId = String(ctx.chat_id).trim();\n}\nlet notify = 'none';\nif (!replyChannel) {\n  if (commandOrigin === 'email' || emailFrom) replyChannel = 'email';\n  else if (commandOrigin === 'telegram' || chatId) replyChannel = 'telegram';\n}\nif (!commandOrigin && replyChannel) commandOrigin = replyChannel;\nif (replyChannel === 'email') {\n  notify = 'email';\n  commandOrigin = 'email';\n  chatId = '';\n} else if (replyChannel === 'telegram') {\n  notify = 'telegram';\n  commandOrigin = 'telegram';\n  emailFrom = '';\n} else if (chatId) {\n  notify = 'telegram';\n  replyChannel = 'telegram';\n  commandOrigin = commandOrigin || 'telegram';\n} else if (emailFrom) {\n  notify = 'email';\n  replyChannel = 'email';\n  commandOrigin = commandOrigin || 'email';\n}\n\nconst batchGroupId =\n  String(data.batch_group_id || '').trim() ||\n  'bg-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);\nconst adHoc = String(commandRoute || '').startsWith('sku');\n\nlet callbackUrl = envFor('CDP_N8N_WEBHOOK_URL');\nif (!callbackUrl) {\n  const base = trimTrailingSlashes(envFor('WEBHOOK_URL'));\n  const rel = trimSlashes(envFor('CDP_N8N_WEBHOOK_PATH') || 'webhook/scraper-result');\n  if (base) callbackUrl = base + '/' + rel;\n}\nif (!callbackUrl && !isDevWorkflow()) {\n  callbackUrl = 'https://automacao.tktechnologies.com.br/webhook/scraper-result';\n}\n\nfunction encodeQueryParam(key, value) {\n  return encodeURIComponent(key) + '=' + encodeURIComponent(String(value));\n}\nfunction buildQueryString(parts) {\n  return parts.map(([k, v]) => encodeQueryParam(k, v)).join('&');\n}\nconst deliveryMode = notify === 'none' ? 'legacy' : 'aggregate';\nconst queryParts = [\n  ['notify', notify],\n  ['reply_channel', replyChannel || notify],\n  ['command_origin', commandOrigin || replyChannel || notify],\n  ['batch_group_id', batchGroupId],\n  ['dual_run', 'scraper'],\n  ['command_route', commandRoute],\n  ['delivery_mode', deliveryMode],\n];\nif (notify === 'telegram' && chatId) queryParts.push(['chat_id', chatId]);\nif (notify === 'email' && emailFrom) queryParts.push(['reply_email', emailFrom]);\nif (adHoc) queryParts.push(['ad_hoc', 'true']);\ncallbackUrl += (callbackUrl.includes('?') ? '&' : '?') + buildQueryString(queryParts);\n\nconst batches = [];\nfor (let i = 0; i < skus.length; i += BATCH_SIZE) {\n  batches.push(skus.slice(i, i + BATCH_SIZE));\n}\nconst totalBatches = batches.length;\n\nreturn batches.map((batch, index) => {\n  const batchSkuKeys = new Set(batch.map((it) => skuKey(it.sku)));\n  const batchSheetRows = sheetRows.filter((it) => batchSkuKeys.has(skuKey(it.sku || it.SKU || it)));\n  return {\n    json: {\n      callback_url: callbackUrl,\n      items: batch.map((it) => ({\n        sku: it.sku,\n        brand: it.brand || '',\n        description: it.description || '',\n      })),\n      sheet_rows: batchSheetRows.map((it) => ({\n        sku: it.sku,\n        row_number: it.row_number ?? null,\n      })),\n      sites,\n      priority: 5,\n      force_refresh: false,\n      api_jobs_url: scraperApiBase + '/api/v1/jobs',\n      api_key: apiKey,\n      batch_group_id: batchGroupId,\n      batch_index: index + 1,\n      total_batches: totalBatches,\n      batch_size: batch.length,\n      sheet_row_count: batchSheetRows.length,\n      ad_hoc: adHoc,\n      notify,\n      reply_channel: replyChannel || notify,\n      command_origin: commandOrigin || replyChannel || notify,\n      chat_id: notify === 'telegram' ? chatId : undefined,\n      reply_email: notify === 'email' ? emailFrom : '',\n      command_route: commandRoute,\n      metadata: {\n        source: 'cdp_router',\n        pipeline: 'scraper',\n        command_route: commandRoute,\n        command_origin: commandOrigin || replyChannel || notify,\n        reply_channel: replyChannel || notify,\n        notify,\n        delivery_mode: deliveryMode,\n        chat_id: notify === 'telegram' ? chatId : '',\n        reply_email: notify === 'email' ? emailFrom : '',\n        batch_group_id: batchGroupId,\n        batch_index: index + 1,\n        total_batches: totalBatches,\n        cache_policy: 'redis_24h',\n        unique_skus: batch.length,\n        sheet_rows: batchSheetRows.length,\n      },\n    },\n  };\n});\n",
        "mode": "runOnceForAllItems"
      },
      "id": "99162794-4160-4334-bdd3-885473fe4524",
      "name": "\u2699\ufe0f Formatar Payload Scraper",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1552,
        2720
      ],
      "notes": "CDP v1.0: active default sites gm,ml,vw,eu,pecadireta; optional CDP_SCRAPER_SITES env can include melibox. Sends only supported API body fields and carries requester context in callback_url query params."
    },
    {
      "parameters": {
        "jsCode": "// cdp_router \u2014 validate sheet rows, dedupe dispatch SKUs, preserve sheet rows.\n\nconst raw = $input.all();\nconst seen = new Set();\nconst uniqueBySku = new Map();\nconst sheetRows = [];\nconst dqIssues = [];\nlet skippedProcessado = 0;\n\nfunction normalizeStatus(value) {\n  return String(value || '')\n    .trim()\n    .toLowerCase()\n    .normalize('NFD')\n    .replace(/[\\u0300-\\u036f]/g, '')\n    .replace(/[^a-z0-9]+/g, ' ')\n    .trim();\n}\n\nfunction normalizeSku(value) {\n  return String(value || '')\n    .trim()\n    .toUpperCase()\n    .replace(/[\\s\\-\\.\\\\/]/g, '');\n}\n\nfor (const item of raw) {\n  const row = item.json;\n  const rawSku = row.CODIGO ?? row.SKU ?? row.sku ?? row.codigo;\n  const sku = normalizeSku(rawSku);\n\n  const processado = normalizeStatus(row.PROCESSADO);\n  if (processado === 'processado' || processado === 'sim' || processado === 'true') {\n    skippedProcessado++;\n    continue;\n  }\n\n  if (!sku) {\n    dqIssues.push({ issue: 'EMPTY_SKU', row: JSON.stringify(row) });\n    continue;\n  }\n  if (sku.length < 3) {\n    dqIssues.push({ issue: 'SHORT_SKU', sku });\n    continue;\n  }\n  if (seen.has(sku)) {\n    dqIssues.push({\n      issue: 'DUPLICATE_SKU',\n      sku,\n      row_number: row.row_number ?? null,\n      action: 'deduped_dispatch_preserved_sheet_row',\n    });\n  }\n  seen.add(sku);\n\n  const rowData = {\n    sku,\n    sku_original: rawSku ? String(rawSku).trim() : '',\n    brand: row.UNIDADE ? String(row.UNIDADE).trim() : '',\n    description: row.ITEM ? String(row.ITEM).trim() : '',\n    row_number: row.row_number ?? null,\n    notify_email: row['E-MAIL'] ? String(row['E-MAIL']).trim() : '',\n    notify_phone: row.CONTATO ? String(row.CONTATO).trim() : '',\n  };\n  sheetRows.push(rowData);\n\n  if (!uniqueBySku.has(sku)) {\n    uniqueBySku.set(sku, { ...rowData });\n  } else {\n    const existing = uniqueBySku.get(sku);\n    if (existing) {\n      if (!existing.brand && rowData.brand) existing.brand = rowData.brand;\n      if (!existing.description && rowData.description) existing.description = rowData.description;\n      if (!existing.notify_email && rowData.notify_email) existing.notify_email = rowData.notify_email;\n      if (!existing.notify_phone && rowData.notify_phone) existing.notify_phone = rowData.notify_phone;\n    }\n  }\n}\n\nconst duplicateIssues = dqIssues.filter((i) => i.issue === 'DUPLICATE_SKU');\nconst duplicateSkus = [...new Set(duplicateIssues.map((i) => i.sku).filter(Boolean))];\nconst skus = [...uniqueBySku.values()];\n\nreturn [\n  {\n    json: {\n      total_read: raw.length,\n      input_valid_skus: sheetRows.length,\n      valid_skus: skus.length,\n      unique_skus: seen.size,\n      skipped_processado: skippedProcessado,\n      duplicates: duplicateIssues.length,\n      duplicate_skus: duplicateSkus,\n      empty_skus: dqIssues.filter((i) => i.issue === 'EMPTY_SKU').length,\n      short_skus: dqIssues.filter((i) => i.issue === 'SHORT_SKU').length,\n      dq_issues: dqIssues,\n      skus,\n      sheet_rows: sheetRows,\n      dispatched_at: new Date().toISOString(),\n    },\n  },\n];\n",
        "mode": "runOnceForAllItems"
      },
      "id": "d5e14e42-868a-4b20-8743-6de4fd18ee8c",
      "name": "\ud83d\udd0d DQ: Validar & Deduplicar",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1328,
        2240
      ],
      "notes": "v0.7.0: ORIGEM removed from per-SKU. notify_email/notify_phone read from E-MAIL/CONTATO columns. Routing via ad_hoc presence of chat_id/email_from only."
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "1IGhsIhrwlnMaCduR-W-eIi9O4mMO2pPYjE-tefgIPII",
          "mode": "list",
          "cachedResultName": "cdp_skus",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IGhsIhrwlnMaCduR-W-eIi9O4mMO2pPYjE-tefgIPII/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": 843035952,
          "mode": "list",
          "cachedResultName": "SKUs",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IGhsIhrwlnMaCduR-W-eIi9O4mMO2pPYjE-tefgIPII/edit#gid=843035952"
        },
        "options": {}
      },
      "id": "51a927fa-a416-4e47-9962-5ad6da0a520d",
      "name": "\ud83d\udcca Ler CDP_SKUs",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1104,
        2240
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      },
      "id": "2d59bd66-7a09-4741-8416-97842e8854e4",
      "name": "\u23f0 Trigger Agendado (Seg 8h)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        880,
        3216
      ],
      "notes": "Runs every Monday at 8am. Adjust cron as needed."
    },
    {
      "parameters": {},
      "id": "a0cbb6d2-629d-4043-bdc1-a8fca392c3e2",
      "name": "\u25b6\ufe0f Trigger Manual",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        880,
        3024
      ]
    },
    {
      "parameters": {
        "jsCode": "// \u2500\u2500\u2500 Telegram Command Router v4 \u2014 adds .status / .andamento \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst DEFAULT_SCRAPER_API_BASE =\n  'https://cdp-scrapers-api-prod.bravecoast-b14d791e.eastus2.azurecontainerapps.io';\n\nfunction envValue(name) {\n  try {\n    if (typeof $env !== 'undefined' && $env && $env[name]) {\n      return String($env[name]).trim();\n    }\n  } catch (e) {\n    return '';\n  }\n  try {\n    if (typeof process !== 'undefined' && process.env && process.env[name]) {\n      return String(process.env[name]).trim();\n    }\n  } catch (e) {\n    return '';\n  }\n  return '';\n}\nfunction workflowName() {\n  try {\n    if (typeof $workflow !== 'undefined' && $workflow && $workflow.name) {\n      return String($workflow.name).trim();\n    }\n  } catch (e) {}\n  return '';\n}\nfunction isDevWorkflow() {\n  return /^DEV\\s*-/i.test(workflowName()) || /^dev$/i.test(envValue('CDP_ENV'));\n}\nfunction devEnvName(name) {\n  const map = {\n    CDP_SCRAPER_API_BASE: 'CDP_DEV_SCRAPER_API_BASE',\n    MUVSTOK_SCRAPER_API_BASE: 'CDP_DEV_SCRAPER_API_BASE',\n    CDP_API_KEY: 'CDP_DEV_API_KEY',\n    MUVSTOK_API_KEY: 'CDP_DEV_API_KEY',\n    API_KEY: 'CDP_DEV_API_KEY',\n    TELEGRAM_ALLOWED_CHAT_IDS: 'TELEGRAM_DEV_ALLOWED_CHAT_IDS',\n    TELEGRAM_BOT_TOKEN: 'TELEGRAM_DEV_BOT_TOKEN',\n    TELEGRAM_TOKEN: 'TELEGRAM_DEV_BOT_TOKEN',\n    TELEGRAM_API_TOKEN: 'TELEGRAM_DEV_BOT_TOKEN',\n    CDP_STATUS_COMMANDS: 'CDP_DEV_STATUS_COMMANDS',\n  };\n  return map[name] || '';\n}\nfunction envFor(name) {\n  if (!isDevWorkflow()) return envValue(name);\n  const mapped = devEnvName(name);\n  const value = mapped ? envValue(mapped) : '';\n  if (value) return value;\n  if (name === 'CDP_STATUS_COMMANDS') return envValue(name);\n  return '';\n}\n\nfunction envList(name) {\n  const raw = envFor(name);\n  return raw ? raw.split(',').map((s) => s.trim()).filter(Boolean) : [];\n}\n\nfunction statusCommands() {\n  const raw = envList('CDP_STATUS_COMMANDS');\n  if (raw.length) return raw;\n  return ['.status', '.andamento', '.progresso'];\n}\n\nfunction trimTrailingSlashes(value) {\n  let out = String(value || '').trim();\n  while (out.endsWith('/')) out = out.slice(0, -1);\n  return out;\n}\n\nfunction scraperApiBase() {\n  return trimTrailingSlashes(\n    envFor('CDP_SCRAPER_API_BASE') ||\n      envFor('MUVSTOK_SCRAPER_API_BASE') ||\n      (isDevWorkflow() ? '' : DEFAULT_SCRAPER_API_BASE)\n  );\n}\n\nfunction cdpApiKey() {\n  return envFor('CDP_API_KEY') || envFor('MUVSTOK_API_KEY') || envFor('API_KEY');\n}\n\nfunction telegramBotToken() {\n  return (\n    envFor('TELEGRAM_BOT_TOKEN') ||\n    envFor('TELEGRAM_TOKEN') ||\n    envFor('TELEGRAM_API_TOKEN')\n  );\n}\n\nconst msg = $input.first().json;\nconst chatId = String(msg.message?.chat?.id || '');\nconst username = String(msg.message?.from?.username || msg.message?.from?.first_name || '');\nconst text = String(msg.message?.text || msg.message?.caption || '').trim();\nconst doc = msg.message?.document || null;\nconst hasFile = !!doc;\n\nconst allowed = envList('TELEGRAM_ALLOWED_CHAT_IDS');\nif (allowed.length && !allowed.includes(chatId)) {\n  return [{ json: { route: 'unauthorized', chat_id: chatId } }];\n}\n\nconst lower = text.toLowerCase();\nconst isAnalisar = lower.startsWith('.analisar');\nconst isSku = lower.startsWith('.sku');\nconst isStatus = statusCommands().some((cmd) => lower.startsWith(cmd.toLowerCase()));\n\nif (isStatus) {\n  const base = scraperApiBase();\n  return [\n    {\n      json: {\n        route: 'status',\n        chat_id: chatId,\n        username,\n        origem: 'telegram',\n        notify: chatId,\n        dispatch_runs_lookup_url:\n          base + '/api/v1/dispatch-runs/active/for-chat/' + encodeURIComponent(chatId),\n        dispatch_runs_api_key: cdpApiKey(),\n      },\n    },\n  ];\n}\n\nif (isSku || hasFile) {\n  const afterCmd = isSku ? text.slice(4).trim() : '';\n  const textSkus = afterCmd\n    .split(/[\\n,;\\s]+/)\n    .map((s) => s.trim().toUpperCase())\n    .filter((s) => /^[A-Z0-9]{5,}$/.test(s))\n    .slice(0, 200);\n\n  if (hasFile && textSkus.length > 0) {\n    return [\n      {\n        json: {\n          route: 'sku_both',\n          chat_id: chatId,\n          username,\n          text_skus: textSkus,\n          file_id: doc.file_id,\n          file_name: doc.file_name || 'attachment',\n          telegram_bot_token: telegramBotToken(),\n          origem: 'telegram',\n          notify: chatId,\n        },\n      },\n    ];\n  }\n  if (hasFile && textSkus.length === 0) {\n    return [\n      {\n        json: {\n          route: 'sku_file',\n          chat_id: chatId,\n          username,\n          text_skus: [],\n          file_id: doc.file_id,\n          file_name: doc.file_name || 'attachment',\n          telegram_bot_token: telegramBotToken(),\n          origem: 'telegram',\n          notify: chatId,\n        },\n      },\n    ];\n  }\n  if (!hasFile && textSkus.length > 0) {\n    return [\n      {\n        json: {\n          route: 'sku_text',\n          chat_id: chatId,\n          username,\n          text_skus: textSkus,\n          origem: 'telegram',\n          notify: chatId,\n        },\n      },\n    ];\n  }\n  return [\n    {\n      json: {\n        route: 'sku_empty',\n        chat_id: chatId,\n        username,\n        origem: 'telegram',\n        notify: chatId,\n      },\n    },\n  ];\n}\n\nif (isAnalisar) {\n  return [\n    {\n      json: {\n        route: 'analisar',\n        chat_id: chatId,\n        username,\n        is_full_sheet_trigger: true,\n        origem: 'telegram',\n        notify: chatId,\n      },\n    },\n  ];\n}\n\nreturn [{ json: { route: 'ignore', chat_id: chatId } }];\n",
        "mode": "runOnceForAllItems"
      },
      "id": "2e1cfa31-4577-41a0-b942-ff2760cfb611",
      "name": "\ud83d\udcf1 Roteador de Comando",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -16,
        1920
      ],
      "notes": "v4: routes .analisar, .sku text, .sku file, .sku both, .sku empty, .status/.andamento, ignore"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "5d6e44e4-3ab7-42b8-874b-54b0c5ddf06b",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "analisar",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "analisar"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "b51ccc60-5c17-41e8-a06e-170e7f879fe9",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "sku_text",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "sku_text"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "3a84bad3-4c62-4abf-aa5f-5d219781e651",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "sku_file",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "sku_file"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "8d7a625e-8215-436c-99e2-360b5b9827fb",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "sku_both",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "sku_both"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "f26fec1f-3e90-477b-8a1e-c7777c1b0a49",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "sku_empty",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "sku_empty"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "775da2ca-b72f-4f4f-8ff3-f02ecdb7269c",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "ignore",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "ignore"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "130b9b9e-f4cd-4dc6-9954-ed20ef1d6d6f",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "unauthorized",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "unauthorized"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "7cdc65c2-5860-44e1-a03e-4e1553168a61",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "status",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "status"
            }
          ]
        },
        "options": {
          "fallbackOutput": "none"
        }
      },
      "id": "76aec9fb-5192-40e1-9229-62aa7f3874b8",
      "name": "\ud83d\udd00 Switch Comando (Telegram)",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3,
      "position": [
        208,
        1840
      ],
      "notes": "Routes: [0]analisar [1]sku_text [2]sku_file [3]sku_both [4]sku_empty [5]ignore [6]unauthorized [7]status"
    },
    {
      "parameters": {
        "jsCode": "\n// \u2500\u2500\u2500 Email Command Router v4 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// v4: Detects attachments in both simple mode (msg.attachments) and raw mode\n//     (msg.payload.parts[]) from Gmail Trigger with simple=false.\n//     Passes messageId for downstream attachment download.\nfunction envList(name) {\n  try {\n    const raw = (typeof process !== 'undefined' && process.env && process.env[name]) || '';\n    return raw.split(',').map(s => s.trim()).filter(Boolean);\n  } catch(e) { return []; }\n}\n\nconst msg = $input.first().json;\nlet fromEmail = '';\nif (msg.from?.value?.[0]?.address) fromEmail = String(msg.from.value[0].address).trim().toLowerCase();\nelse if (typeof msg.from === 'string') fromEmail = msg.from.trim().toLowerCase();\n\n// \u2500\u2500 Auth check \u2500\u2500\nconst allowed = envList('EMAIL_ALLOWED_SENDERS').map(s => s.toLowerCase());\nif (allowed.length && !allowed.some(a => fromEmail.includes(a))) {\n  return [{ json: { route: 'unauthorized', email_from: fromEmail } }];\n}\n\n// \u2500\u2500 Read subject + body \u2500\u2500\nconst subject = String(msg.subject || '').trim();\nconst bodyRaw = String(msg.textPlain || msg.text || msg.html || '').replace(/<[^>]+>/g, ' ').replace(/\\s+/g, ' ').trim();\n\nconst subjectL = subject.toLowerCase();\nconst bodyLines = bodyRaw.split(/\\r?\\n/).map(l => l.trim()).filter(Boolean);\nconst bodyFirstL = (bodyLines[0] || '').toLowerCase();\nlet cmdSource;\nif (subjectL.startsWith('.analisar') || subjectL.startsWith('.sku')) {\n  cmdSource = subject;\n} else if (bodyFirstL.startsWith('.analisar') || bodyFirstL.startsWith('.sku')) {\n  cmdSource = bodyRaw;\n} else {\n  cmdSource = subject || bodyRaw;\n}\nconst lower = cmdSource.toLowerCase();\n\n// \u2500\u2500 Attachment check: simple mode (msg.attachments) \u2500\u2500\nconst attachments = msg.attachments || [];\nlet xlsAttachment = attachments.find(a =>\n  /\\.(xlsx?|csv)$/i.test(a.name || a.filename || '')\n);\nlet messageId = String(msg.id || msg.messageId || '').trim();\n\n// \u2500\u2500 Attachment check: raw mode (msg.payload.parts[]) \u2500\u2500\n// Gmail Trigger with simple=false returns raw Gmail API structure\nif (!xlsAttachment && msg.payload && msg.payload.parts) {\n  const parts = msg.payload.parts;\n  // Flatten: some emails nest parts inside parts (multipart/mixed > multipart/alternative + attachment)\n  const allParts = [];\n  function collectParts(partsList) {\n    for (const p of partsList) {\n      allParts.push(p);\n      if (p.parts) collectParts(p.parts);\n    }\n  }\n  collectParts(parts);\n\n  for (const part of allParts) {\n    const fn = String(part.filename || '').trim();\n    if (/\\.(xlsx?|csv)$/i.test(fn) && part.body && part.body.attachmentId) {\n      xlsAttachment = {\n        id: part.body.attachmentId,\n        name: fn,\n        mimeType: part.mimeType || '',\n        size: part.body.size || 0\n      };\n      break;\n    }\n  }\n  // Also try to get messageId from raw payload\n  if (!messageId && msg.payload.headers) {\n    const msgIdHeader = msg.payload.headers.find(h => h.name?.toLowerCase() === 'message-id');\n    if (msgIdHeader) messageId = String(msgIdHeader.value || '').trim();\n  }\n}\n\n// Fallback messageId from threadId or any id on the message root\nif (!messageId) messageId = String(msg.threadId || '').trim();\n\nconst hasFile = !!xlsAttachment;\n\nconst isAnalisar = lower.startsWith('.analisar');\nconst isSku      = lower.startsWith('.sku');\n\nif (isSku || hasFile) {\n  const afterCmd = isSku ? cmdSource.slice(4).trim() : '';\n  const textSkus = afterCmd\n    .split(/[\\n,;\\s]+/)\n    .map(s => s.trim().toUpperCase())\n    .filter(s => /^[A-Z0-9]{5,}$/.test(s))\n    .slice(0, 200);\n\n  const baseOut = {\n    email_from: fromEmail,\n    notify: fromEmail,\n    origem: 'email',\n    message_id: messageId\n  };\n\n  if (hasFile && textSkus.length > 0) {\n    return [{ json: { route: 'sku_both', ...baseOut, text_skus: textSkus,\n      attachment_id: xlsAttachment.id || '', attachment_name: xlsAttachment.name || xlsAttachment.filename || '' }}];\n  }\n  if (hasFile && textSkus.length === 0) {\n    return [{ json: { route: 'sku_file', ...baseOut,\n      attachment_id: xlsAttachment.id || '', attachment_name: xlsAttachment.name || xlsAttachment.filename || '' }}];\n  }\n  if (!hasFile && textSkus.length > 0) {\n    return [{ json: { route: 'sku_text', ...baseOut, text_skus: textSkus }}];\n  }\n  return [{ json: { route: 'sku_empty', ...baseOut }}];\n}\n\nif (isAnalisar) {\n  return [{ json: { route: 'analisar', email_from: fromEmail, notify: fromEmail,\n    is_full_sheet_trigger: true, origem: 'email', message_id: messageId }}];\n}\n\nreturn [{ json: { route: 'ignore', email_from: fromEmail } }];\n"
      },
      "id": "0454cd9c-f0ff-462d-9d40-40eb9716f6db",
      "name": "\ud83d\udce7 Roteador de Comando",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        208,
        2624
      ],
      "notes": "v4: Detects attachments in both simple mode (msg.attachments) and raw mode (msg.payload.parts[]) from Gmail Trigger. Passes message_id for downstream attachment download."
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "407e15a9-b8e5-46f4-acc1-26a772803ad2",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "analisar",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "analisar"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "c9fe90f8-5067-47a3-b914-8ce7de00596e",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "sku_text",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "sku_text"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "5aebdb2d-220f-4e0e-8508-905ad3da95d3",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "sku_file",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "sku_file"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "9faff61b-8ca6-430e-b5ec-c1e4c719bf19",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "sku_both",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "sku_both"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "97fdbe8b-03c6-47bf-b95b-44c2fc4145f0",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "sku_empty",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "sku_empty"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "6fe15eb7-c299-44bb-a028-f66cb636de7a",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "ignore",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "ignore"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "5fdb95f4-acd8-4f0e-b366-ff544e4a3759",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "unauthorized",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "unauthorized"
            }
          ]
        },
        "options": {
          "fallbackOutput": "none"
        }
      },
      "id": "3e5775e9-f30d-4046-ab5d-953e9c1f72fc",
      "name": "\ud83d\udd00 Switch Comando (Email)",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3,
      "position": [
        432,
        2544
      ],
      "notes": "Routes: [0]analisar [1]sku_text [2]sku_file [3]sku_both [4]sku_empty [5]ignore [6]unauthorized"
    },
    {
      "parameters": {
        "jsCode": "\nconst d = $input.first().json;\nconst skus = (d.text_skus || []).map(s => ({ sku: s, brand: '', description: '' }));\nreturn [{ json: {\n  total_read: skus.length, valid_skus: skus.length,\n  skipped_processado: 0, duplicates: 0, empty_skus: 0, short_skus: 0,\n  skus, dispatched_at: new Date().toISOString(),\n  origem: d.origem || 'telegram',\n  telegram_chat_id: d.chat_id || '',\n  notify: d.notify || d.chat_id || ''\n}}];\n"
      },
      "id": "6cfbda3a-57b9-4057-aca3-0ce05766bc85",
      "name": "\ud83d\udcf1 Parsear SKUs do Telegram",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1104,
        2576
      ],
      "notes": "v3: receives pre-parsed text_skus from router"
    },
    {
      "parameters": {
        "url": "={{ 'https://api.telegram.org/bot' + $json.telegram_bot_token + '/getFile' }}",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "file_id",
              "value": "={{ $json.file_id }}"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "id": "c64c6879-39b6-4f36-9e0f-22deec414fde",
      "name": "\ud83d\udce5 Obter Path do Arquivo (Telegram)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        432,
        2160
      ],
      "continueOnFail": true,
      "notes": "v3: gets file path from Telegram API using file_id. Token fallback: TELEGRAM_BOT_TOKEN || TELEGRAM_TOKEN || TELEGRAM_API_TOKEN"
    },
    {
      "parameters": {
        "url": "={{ 'https://api.telegram.org/file/bot' + $('\ud83d\udcf1 Roteador de Comando').first().json.telegram_bot_token + '/' + $json.result.file_path }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "id": "50a29d5c-c215-4d47-8d79-9e26fb0786b5",
      "name": "\ud83d\udce5 Baixar Arquivo (Telegram)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        656,
        2160
      ],
      "continueOnFail": true,
      "notes": "v3: downloads file bytes from Telegram CDN. continueOnFail keeps sku_both text path alive if download fails."
    },
    {
      "parameters": {
        "options": {
          "headerRow": true,
          "includeEmptyCells": false
        }
      },
      "id": "79ab60cd-db25-4901-8884-e503dce33202",
      "name": "\ud83d\udcca Parsear Planilha (Telegram)",
      "type": "n8n-nodes-base.spreadsheetFile",
      "typeVersion": 2,
      "position": [
        880,
        2160
      ],
      "continueOnFail": true,
      "notes": "v3: parses XLSX/CSV binary from Telegram download into rows. continueOnFail avoids full route crash."
    },
    {
      "parameters": {
        "jsCode": "\n// Receives rows from SpreadsheetFile \u2014 find SKU-like column\nconst rows = $input.all();\nconst routerData = $('\ud83d\udd00 Switch Comando (Telegram)').first().json;\nconst chatId = String(routerData.chat_id || routerData.notify || '');\nconst textSkus = routerData.text_skus || []; // for sku_both merging\n\n// Find SKU column: prefer CODIGO, SKU, COD, CODE, first alphanumeric col\nconst sampleRow = rows[0]?.json || {};\nconst cols = Object.keys(sampleRow);\nconst skuCol = cols.find(c => /^(codigo|sku|cod|code|part|partnumber|part_number)$/i.test(c))\n  || cols.find(c => Object.values(sampleRow).some(v => /^[A-Z0-9]{5,}$/.test(String(v).trim())));\n\nconst seen = new Set(textSkus.map(s => s.toUpperCase()));\nconst fromFile = [];\nfor (const item of rows) {\n  const raw = skuCol ? String(item.json[skuCol] || '').trim().toUpperCase() : '';\n  if (/^[A-Z0-9]{5,}$/.test(raw) && !seen.has(raw)) {\n    seen.add(raw);\n    const brand = String(item.json['MARCA'] || item.json['BRAND'] || item.json['UNIDADE'] || '').trim();\n    const desc  = String(item.json['DESCRICAO'] || item.json['DESCRIPTION'] || item.json['ITEM'] || '').trim();\n    fromFile.push({ sku: raw, brand, description: desc, source: 'file' });\n  }\n}\n\n// Merge: text SKUs first, then file SKUs (deduped)\nconst textFormatted = textSkus.map(s => ({ sku: s, brand: '', description: '', source: 'text' }));\nconst allSkus = [...textFormatted, ...fromFile].slice(0, 500);\n\nreturn [{ json: {\n  total_read: allSkus.length, valid_skus: allSkus.length,\n  skipped_processado: 0, duplicates: seen.size - allSkus.length, empty_skus: 0, short_skus: 0,\n  skus: allSkus,\n  dispatched_at: new Date().toISOString(),\n  origem: 'telegram',\n  telegram_chat_id: chatId,\n  notify: chatId,\n  source_detail: `file:${fromFile.length} text:${textFormatted.length}`\n}}];\n"
      },
      "id": "15eb64a4-d461-4fdf-884e-ed984cc02904",
      "name": "\ud83d\udccb Formatar SKUs da Planilha (Telegram)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1104,
        2768
      ],
      "notes": "v3: merges text_skus + file rows, deduped. handles sku_file and sku_both"
    },
    {
      "parameters": {
        "jsCode": "\nconst d = $input.first().json;\nconst skus = (d.text_skus || []).map(s => ({ sku: s, brand: '', description: '' }));\nreturn [{ json: {\n  total_read: skus.length, valid_skus: skus.length,\n  skipped_processado: 0, duplicates: 0, empty_skus: 0, short_skus: 0,\n  skus, dispatched_at: new Date().toISOString(),\n  origem: d.origem || 'email',\n  email_from: d.email_from || '',\n  notify: d.notify || d.email_from || ''\n}}];\n"
      },
      "id": "77628e57-2796-4938-89be-1cb2dee7d6cd",
      "name": "\ud83d\udce7 Parsear SKUs do Email",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1104,
        2960
      ],
      "notes": "v3: receives pre-parsed text_skus from email router"
    },
    {
      "parameters": {
        "operation": "getAttachment"
      },
      "id": "7b804279-73ff-4227-a0cb-0b313d5733b7",
      "name": "\ud83d\udce5 Baixar Anexo (Email)",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        656,
        2832
      ],
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "notes": "v4: Uses message_id from router or Gmail Trigger id. Downloads all attachments as binary data for spreadsheet parser."
    },
    {
      "parameters": {
        "options": {
          "headerRow": true,
          "includeEmptyCells": false
        }
      },
      "id": "dfed6cd1-5eae-4476-8a57-db76ba6cf58a",
      "name": "\ud83d\udcca Parsear Planilha (Email)",
      "type": "n8n-nodes-base.spreadsheetFile",
      "typeVersion": 2,
      "position": [
        880,
        2832
      ],
      "notes": "v3: parses XLSX/CSV binary from email attachment"
    },
    {
      "parameters": {
        "jsCode": "\nconst rows = $input.all();\nconst routerData = $('\ud83d\udd00 Switch Comando (Email)').first().json;\nconst emailFrom  = String(routerData.email_from || routerData.notify || '');\nconst textSkus   = routerData.text_skus || [];\n\nconst sampleRow = rows[0]?.json || {};\nconst cols = Object.keys(sampleRow);\nconst skuCol = cols.find(c => /^(codigo|sku|cod|code|part|partnumber|part_number)$/i.test(c))\n  || cols.find(c => Object.values(sampleRow).some(v => /^[A-Z0-9]{5,}$/.test(String(v).trim())));\n\nconst seen = new Set(textSkus.map(s => s.toUpperCase()));\nconst fromFile = [];\nfor (const item of rows) {\n  const raw = skuCol ? String(item.json[skuCol] || '').trim().toUpperCase() : '';\n  if (/^[A-Z0-9]{5,}$/.test(raw) && !seen.has(raw)) {\n    seen.add(raw);\n    const brand = String(item.json['MARCA'] || item.json['BRAND'] || item.json['UNIDADE'] || '').trim();\n    const desc  = String(item.json['DESCRICAO'] || item.json['DESCRIPTION'] || item.json['ITEM'] || '').trim();\n    fromFile.push({ sku: raw, brand, description: desc, source: 'file' });\n  }\n}\n\nconst textFormatted = textSkus.map(s => ({ sku: s, brand: '', description: '', source: 'text' }));\nconst allSkus = [...textFormatted, ...fromFile].slice(0, 500);\n\nreturn [{ json: {\n  total_read: allSkus.length, valid_skus: allSkus.length,\n  skipped_processado: 0, duplicates: seen.size - allSkus.length, empty_skus: 0, short_skus: 0,\n  skus: allSkus,\n  dispatched_at: new Date().toISOString(),\n  origem: 'email',\n  email_from: emailFrom,\n  notify: emailFrom,\n  source_detail: `file:${fromFile.length} text:${textFormatted.length}`\n}}];\n"
      },
      "id": "ee3eeab5-84f1-4be2-a725-9b8475083e9b",
      "name": "\ud83d\udccb Formatar SKUs da Planilha (Email)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1104,
        3200
      ],
      "notes": "v3: merges text_skus + attachment rows, deduped"
    },
    {
      "parameters": {
        "chatId": "={{ $json.chat_id }}",
        "text": "\u26a0\ufe0f Depois de digitar o comando *.sku* coloque os c\u00f3digos ou me envie uma planilha em anexo.\n\nExemplo:\n`.sku ABC123 DEF456`\nou envie um arquivo *.xlsx* ou *.csv*",
        "additionalFields": {
          "parse_mode": "Markdown",
          "appendAttribution": false
        }
      },
      "id": "1b5130f4-57b1-45f4-9c98-fd752fd77deb",
      "name": "\ud83d\udcf1 Resposta .sku Vazio (Telegram)",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.1,
      "position": [
        432,
        2352
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "notes": "v3: replies when .sku is sent without codes or file"
    },
    {
      "parameters": {
        "operation": "send",
        "sendTo": "={{ $json.email_from }}",
        "subject": "\u26a0\ufe0f CDP: Comando .sku sem dados",
        "message": "<p>Ap\u00f3s o comando <strong>.sku</strong> coloque os c\u00f3digos separados por espa\u00e7o ou v\u00edrgula, ou envie um arquivo <strong>.xlsx</strong> ou <strong>.csv</strong> em anexo.</p><p>Exemplo: <code>.sku ABC123 DEF456 GHI789</code></p>",
        "options": {}
      },
      "id": "1d4849cd-8d06-4a2e-898c-b71986a8b819",
      "name": "\ud83d\udce7 Resposta .sku Vazio (Email)",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        656,
        3024
      ],
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "notes": "v3: replies when .sku is sent without codes or attachment"
    },
    {
      "parameters": {
        "jsCode": "// Runs after \ud83c\udfb2 Limitar SKUs \u2014 Assistente CDP (PT-BR). Single confirmation message.\nconst WEBSCRAPERS_LABEL = 'WEBSCRAPERS';\nconst ESTOQUE_LABEL = 'ESTOQUE ONLINE';\nconst dq = $input.first().json;\nconst total = dq.valid_skus || 0;\nconst sheetTotal = Number(dq.dispatch_total_before_sample || dq.total_read || 0);\nconst sampled = Boolean(dq.dispatch_sampled);\nconst skippedProcessado = Number(dq.skipped_processado || 0);\nconst skuCodes = (Array.isArray(dq.skus) ? dq.skus : [])\n  .map((s) => String((s && s.sku) || s || '').trim().toUpperCase())\n  .filter(Boolean);\nconst skuPreview = skuCodes.slice(0, 8).join(', ');\nconst skuExtra = skuCodes.length > 8 ? '\\n_\u2026e mais ' + (skuCodes.length - 8) + ' pe\u00e7as_' : '';\n\nfunction env(name) {\n  try {\n    if (typeof $env !== 'undefined' && $env && $env[name]) {\n      return String($env[name]).trim();\n    }\n  } catch (e) {}\n  try {\n    if (typeof process !== 'undefined' && process.env && process.env[name]) {\n      return String(process.env[name]).trim();\n    }\n  } catch (e) {}\n  return '';\n}\nfunction envList(name) {\n  const raw = env(name);\n  return raw ? raw.split(',').map((s) => s.trim()).filter(Boolean) : [];\n}\nfunction estimateMins(skuCount) {\n  const siteCount = envList('CDP_SCRAPER_SITES').length || 5;\n  const parallel = Math.max(1, parseInt(env('CDP_SCRAPER_PARALLEL_SITES') || '3', 10) || 3);\n  const waves = Math.ceil(siteCount / parallel);\n  const secPerWave = Number(env('CDP_ESTIMATE_SEC_PER_WAVE') || '18') || 18;\n  const interSku = Number(env('CDP_ESTIMATE_INTER_SKU_SEC') || '2.75') || 2.75;\n  const totalSec = Math.max(30, Math.ceil(Math.max(1, skuCount) * (waves * secPerWave + interSku)));\n  return Math.max(1, Math.ceil(totalSec / 60));\n}\nconst mins = estimateMins(total);\n\nfunction peca(n) {\n  const x = Number(n) || 0;\n  return x === 1 ? '1 pe\u00e7a' : x + ' pe\u00e7as';\n}\n\nlet skuLine = '\ud83d\udce6 *' + peca(total) + '* nesta rodada';\nlet skuEmail = String(total);\nif (sampled && sheetTotal > total) {\n  skuLine =\n    '\ud83d\udce6 *' +\n    peca(total) +\n    '* nesta rodada _(amostra aleat\u00f3ria de ' +\n    sheetTotal.toLocaleString('pt-BR') +\n    ' na fila)_';\n  skuEmail = total + ' (amostra de ' + sheetTotal + ' na fila)';\n}\nif (skippedProcessado > 0) {\n  skuLine += '\\n_(+' + skippedProcessado + ' j\u00e1 processadas na fila, ignoradas)_';\n  skuEmail += ' (+' + skippedProcessado + ' j\u00e1 processadas)';\n}\nif (skuPreview) {\n  skuLine += '\\n\ud83d\udd22 ' + skuPreview + skuExtra;\n}\n\nfunction looksLikeEmail(value) {\n  return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(String(value || '').trim());\n}\nfunction escapeHtml(value) {\n  return String(value ?? '')\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/\"/g, '&quot;')\n    .replace(/'/g, '&#39;');\n}\n\nlet origem = String(dq.origem || '').trim().toLowerCase() || 'auto';\nlet chatId = String(dq.telegram_chat_id || dq.chat_id || '').trim();\nlet emailFrom = String(dq.email_from || '').trim();\nconst notify = String(dq.notify || '').trim();\nlet commandOrigin = String(dq.command_origin || origem || '').trim().toLowerCase();\nlet replyChannel = String(dq.reply_channel || '').trim().toLowerCase();\n\nif (!emailFrom && looksLikeEmail(notify)) emailFrom = notify;\nif (!chatId && notify && !looksLikeEmail(notify)) chatId = notify;\nif (looksLikeEmail(chatId)) {\n  if (!emailFrom) emailFrom = chatId;\n  chatId = '';\n}\nif (!replyChannel) {\n  if (commandOrigin === 'email' || emailFrom) replyChannel = 'email';\n  else if (commandOrigin === 'telegram' || chatId) replyChannel = 'telegram';\n}\n\ntry {\n  if (typeof $getWorkflowStaticData === 'function') {\n    const sd = $getWorkflowStaticData('global');\n    const ctx = sd.cdp_sheet_requester;\n    if (ctx) {\n      if (!replyChannel && ctx.reply_channel) {\n        replyChannel = String(ctx.reply_channel).trim().toLowerCase();\n      }\n      if (!commandOrigin && ctx.command_origin) {\n        commandOrigin = String(ctx.command_origin).trim().toLowerCase();\n      }\n      if (!emailFrom) emailFrom = String(ctx.email_from || '').trim();\n      const inputIsEmail = commandOrigin === 'email' || replyChannel === 'email' || emailFrom;\n      if (!inputIsEmail && !chatId) chatId = String(ctx.chat_id || '').trim();\n      if (looksLikeEmail(chatId)) {\n        if (!emailFrom) emailFrom = chatId;\n        chatId = '';\n      }\n    }\n  }\n} catch (e) {}\n\ntry {\n  const r = $('\ud83d\udce7 Roteador de Comando').first().json;\n  if (r && r.origem === 'email') {\n    origem = 'email';\n    commandOrigin = 'email';\n    replyChannel = 'email';\n    emailFrom = String(r.email_from || r.notify || '').trim();\n    chatId = '';\n  }\n} catch (e) {}\n\nif (origem !== 'email') {\n  try {\n    const em = $('\ud83d\udd00 Switch Comando (Email)').first().json;\n    if (em && em.origem === 'email') {\n      origem = 'email';\n      commandOrigin = 'email';\n      replyChannel = 'email';\n      emailFrom = String(em.email_from || '').trim();\n      chatId = '';\n    }\n  } catch (e) {}\n}\n\nif (!emailFrom) {\n  try {\n    const gm = $('Gmail Trigger').first().json;\n    if (gm?.from?.value?.[0]?.address) {\n      emailFrom = String(gm.from.value[0].address).trim();\n      origem = 'email';\n      commandOrigin = 'email';\n      replyChannel = 'email';\n      chatId = '';\n    }\n  } catch (e) {}\n}\n\nif (origem !== 'email') {\n  try {\n    const tg = $('\ud83d\udd00 Switch Comando (Telegram)').first().json;\n    const route = String(tg?.route || '');\n    const cid = String(tg?.chat_id || tg?.notify || '').trim();\n    if (cid && !looksLikeEmail(cid)) {\n      chatId = chatId || cid;\n      origem = 'telegram';\n      commandOrigin = 'telegram';\n      replyChannel = 'telegram';\n    }\n    if (route === 'analisar') {\n      origem = 'telegram';\n      commandOrigin = 'telegram';\n      replyChannel = 'telegram';\n    }\n  } catch (e) {}\n}\n\nif (!replyChannel) {\n  if (emailFrom) replyChannel = 'email';\n  else if (chatId) replyChannel = 'telegram';\n}\nif (replyChannel === 'email') {\n  origem = 'email';\n  commandOrigin = 'email';\n  chatId = '';\n} else if (replyChannel === 'telegram') {\n  origem = 'telegram';\n  commandOrigin = 'telegram';\n  emailFrom = '';\n}\n\nconst triggered = !!(chatId || emailFrom);\nconst tempo = mins === 1 ? '~1 minuto' : '~' + mins + ' minutos';\n\nlet commandLabel = '.analisar';\ntry {\n  const route = String(dq.command_route || dq.route || '').trim();\n  if (route.startsWith('sku')) commandLabel = '.sku';\n} catch (e) {}\nif (commandLabel === '.analisar') {\n  try {\n    const tg = $('\ud83d\udd00 Switch Comando (Telegram)').first().json;\n    const r = String(tg?.route || '');\n    if (r.startsWith('sku')) commandLabel = '.sku';\n  } catch (e) {}\n}\n\nconst msgTelegram = [\n  '\ud83e\udd16 *Assistente CDP*',\n  '',\n  'Recebi sua consulta (*' + commandLabel + '*). Estamos buscando pre\u00e7os nos sites e no estoque.',\n  '',\n  skuLine,\n  '\u23f1\ufe0f Previs\u00e3o: *' + tempo + '* (sites e estoque em paralelo)',\n  '',\n  'Quando *' +\n    WEBSCRAPERS_LABEL +\n    '* e *' +\n    ESTOQUE_LABEL +\n    '* terminarem, voc\u00ea receber\u00e1 *um \u00fanico resultado final* com o link do relat\u00f3rio. \u2728',\n].join('\\n');\n\nconst statusTone = total > 1 ? 'Consultas em andamento' : 'Consulta em andamento';\nconst safeCommand = escapeHtml(commandLabel);\nconst safeSkuPreview = escapeHtml(skuPreview);\nconst safeSkuEmail = escapeHtml(skuEmail);\nconst safeTempo = escapeHtml(tempo);\nconst msgEmailHtml =\n  '<div style=\"margin:0;padding:0;background:#f6f8fb;font-family:Arial,Helvetica,sans-serif;color:#1f2937\">' +\n  '<div style=\"max-width:640px;margin:0 auto;padding:28px 18px\">' +\n  '<div style=\"background:#ffffff;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden\">' +\n  '<div style=\"padding:22px 24px;border-bottom:1px solid #eef2f7\">' +\n  '<div style=\"font-size:12px;text-transform:uppercase;letter-spacing:0;color:#64748b;font-weight:700\">' +\n  escapeHtml(statusTone) +\n  '</div>' +\n  '<h1 style=\"font-size:24px;line-height:1.25;margin:8px 0 0;color:#111827\">Consulta CDP iniciada</h1>' +\n  '</div>' +\n  '<div style=\"padding:22px 24px\">' +\n  '<p style=\"font-size:15px;line-height:1.6;margin:0 0 18px\">Recebemos o comando <strong>' +\n  safeCommand +\n  '</strong>. A busca em sites e estoque j\u00e1 est\u00e1 em andamento.</p>' +\n  '<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"border-collapse:collapse;margin:0 0 20px\">' +\n  '<tr>' +\n  '<td style=\"padding:14px 16px;background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px\">' +\n  '<div style=\"font-size:12px;color:#64748b;text-transform:uppercase;font-weight:700\">Pe\u00e7as na fila</div>' +\n  '<div style=\"font-size:26px;font-weight:700;color:#111827;margin-top:4px\">' +\n  safeSkuEmail +\n  '</div>' +\n  '</td>' +\n  '<td width=\"12\"></td>' +\n  '<td style=\"padding:14px 16px;background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px\">' +\n  '<div style=\"font-size:12px;color:#64748b;text-transform:uppercase;font-weight:700\">Previs\u00e3o</div>' +\n  '<div style=\"font-size:26px;font-weight:700;color:#111827;margin-top:4px\">' +\n  safeTempo +\n  '</div>' +\n  '</td>' +\n  '</tr>' +\n  '</table>' +\n  '<div style=\"background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;padding:14px 16px;margin:0 0 20px\">' +\n  '<div style=\"font-size:12px;color:#1d4ed8;text-transform:uppercase;font-weight:700;margin-bottom:6px\">Resultado final</div>' +\n  '<div style=\"font-size:14px;line-height:1.6;color:#1e3a8a\">Quando <strong>' +\n  escapeHtml(WEBSCRAPERS_LABEL) +\n  '</strong> e <strong>' +\n  escapeHtml(ESTOQUE_LABEL) +\n  '</strong> terminarem, enviaremos <strong>um \u00fanico e-mail</strong> com o resumo consolidado e o link do relat\u00f3rio.</div>' +\n  '</div>' +\n  (skuPreview\n    ? '<div style=\"font-size:13px;color:#64748b;text-transform:uppercase;font-weight:700;margin-bottom:6px\">C\u00f3digos</div>' +\n      '<div style=\"font-size:14px;line-height:1.5;background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:12px 14px;color:#111827\">' +\n      safeSkuPreview +\n      '</div>'\n    : '') +\n  '<p style=\"font-size:14px;line-height:1.6;color:#475569;margin:20px 0 0\">Aguarde o e-mail final antes de considerar a rodada encerrada.</p>' +\n  '</div>' +\n  '</div>' +\n  '<div style=\"font-size:12px;line-height:1.5;color:#94a3b8;text-align:center;padding:14px 0 0\">Mensagem autom\u00e1tica do Assistente CDP.</div>' +\n  '</div>' +\n  '</div>';\n\nreturn [\n  {\n    json: {\n      total,\n      mins,\n      origem,\n      command_origin: commandOrigin || origem,\n      reply_channel: replyChannel || origem,\n      triggered,\n      chat_id: chatId,\n      email_from: emailFrom,\n      notify: replyChannel === 'telegram' ? chatId : replyChannel === 'email' ? emailFrom : '',\n      msg_telegram: msgTelegram,\n      msg_email_html: msgEmailHtml,\n      msg_email_subject: 'Consulta CDP iniciada \u2014 resultado final ap\u00f3s sites e estoque (' + peca(total) + ')',\n    },\n  },\n];\n",
        "mode": "runOnceForAllItems"
      },
      "id": "d5ec2f31-5502-4b98-b9b7-dd9037150bc1",
      "name": "\ud83d\udccb Formatar Confirma\u00e7\u00e3o (Planilha)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1552,
        2144
      ],
      "notes": "Assistente CDP \u2014 \u00fanica confirma\u00e7\u00e3o (ap\u00f3s Limitar SKUs)."
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 1
          },
          "conditions": [
            {
              "id": "is-triggered",
              "leftValue": "={{ $json.triggered }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "d8e67430-53d0-4469-aed3-d0439c3e7077",
      "name": "\u2705 Foi Disparado por Comando?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1776,
        2144
      ],
      "notes": "v3: skips notification when triggered by cron or manual"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "rota-planilha-tg",
                    "leftValue": "={{ $json.origem === 'telegram' && String($json.chat_id || '').trim() !== '' }}",
                    "rightValue": true,
                    "operator": {
                      "type": "boolean",
                      "operation": "true"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "telegram"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "rota-planilha-em",
                    "leftValue": "={{ $json.origem === 'email' || (String($json.email_from || '').trim() !== '' && String($json.chat_id || '').trim() === '') }}",
                    "rightValue": true,
                    "operator": {
                      "type": "boolean",
                      "operation": "true"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "email"
            }
          ]
        },
        "options": {}
      },
      "id": "79911856-c631-46c6-a961-28f00f8cec62",
      "name": "\ud83d\udd00 Rota Confirma\u00e7\u00e3o Planilha",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3,
      "position": [
        2000,
        2144
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $json.chat_id }}",
        "text": "={{ $json.msg_telegram }}",
        "additionalFields": {
          "parse_mode": "Markdown",
          "appendAttribution": false
        }
      },
      "id": "00e06e44-2394-4598-bd20-fc141f7cf898",
      "name": "\ud83d\udcf1 Confirmar Planilha (Telegram)",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.1,
      "position": [
        2224,
        1952
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "send",
        "sendTo": "={{ $json.email_from }}",
        "subject": "={{ $json.msg_email_subject }}",
        "message": "={{ $json.msg_email_html }}",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "cca4d804-620d-4304-8a6b-9f67078a03d6",
      "name": "\ud83d\udce7 Confirmar Planilha (Email)",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        2224,
        2144
      ],
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "send",
        "sendTo": "={{ $json.email_from || '' }}",
        "subject": "={{ $json.email_subject }}",
        "message": "={{ $json.email_html }}",
        "options": {}
      },
      "id": "dcba7649-e43c-46fa-8749-9b637c05a6cb",
      "name": "\ud83d\udce7 Enviar Alerta de Erro",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        2448,
        2528
      ],
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "// cdp_router \u2014 API Diversos arm (inline POST to StokAPI). Runs for .analisar and .sku.\n\nconst DEFAULT_N8N_WEBHOOK_BASE = 'https://automacao.tktechnologies.com.br';\n\nfunction readEnv(name) {\n  try {\n    if (typeof $env !== 'undefined' && $env && $env[name]) {\n      return String($env[name]).trim();\n    }\n  } catch (e) {}\n  try {\n    if (typeof process !== 'undefined' && process.env && process.env[name]) {\n      return String(process.env[name]).trim();\n    }\n  } catch (e) {}\n  return '';\n}\nfunction workflowName() {\n  try {\n    if (typeof $workflow !== 'undefined' && $workflow && $workflow.name) {\n      return String($workflow.name).trim();\n    }\n  } catch (e) {}\n  return '';\n}\nfunction isDevWorkflow() {\n  return /^DEV\\s*-/i.test(workflowName()) || /^dev$/i.test(readEnv('CDP_ENV'));\n}\nfunction devEnvName(name) {\n  const map = {\n    CDP_MUVSTOK_API_BASE: 'CDP_DEV_MUVSTOK_API_BASE',\n    CDP_MUVSTOK_API_KEY: 'CDP_DEV_MUVSTOK_API_KEY',\n    CDP_API_KEY: 'CDP_DEV_API_KEY',\n    MUVSTOK_API_KEY: 'CDP_DEV_MUVSTOK_API_KEY',\n    API_KEY: 'CDP_DEV_API_KEY',\n    WEBHOOK_URL: 'CDP_DEV_WEBHOOK_URL',\n    CDP_MUVSTOK_N8N_WEBHOOK_URL: 'CDP_DEV_MUVSTOK_N8N_WEBHOOK_URL',\n    CDP_MUVSTOK_WEBHOOK_PATH: 'CDP_DEV_MUVSTOK_WEBHOOK_PATH',\n  };\n  return map[name] || '';\n}\nfunction envFor(name) {\n  if (!isDevWorkflow()) return readEnv(name);\n  const mapped = devEnvName(name);\n  const value = mapped ? readEnv(mapped) : '';\n  if (value) return value;\n  if (name === 'WEBHOOK_URL') return readEnv('WEBHOOK_URL') || DEFAULT_N8N_WEBHOOK_BASE;\n  if (name === 'CDP_MUVSTOK_WEBHOOK_PATH') return 'webhook/dev-muvstok-result';\n  return '';\n}\nfunction trimTrailingSlashes(value) {\n  let out = String(value || '').trim();\n  while (out.endsWith('/')) out = out.slice(0, -1);\n  return out;\n}\nfunction trimSlashes(value) {\n  let out = String(value || '').trim();\n  while (out.startsWith('/')) out = out.slice(1);\n  while (out.endsWith('/')) out = out.slice(0, -1);\n  return out;\n}\nfunction encodeQueryParam(name, value) {\n  return encodeURIComponent(name) + '=' + encodeURIComponent(String(value));\n}\nfunction looksLikeEmail(value) {\n  return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(String(value || '').trim());\n}\n\nconst dq = $input.first().json;\nlet chatId = '';\nlet emailFrom = '';\nlet notify = 'none';\nlet commandRoute = String(dq.command_route || dq.route || 'analisar');\nlet commandOrigin = String(dq.command_origin || dq.origem || '').trim().toLowerCase();\nlet replyChannel = String(dq.reply_channel || '').trim().toLowerCase();\ntry {\n  const sd = $getWorkflowStaticData('global');\n  const ctx = sd.cdp_sheet_requester || {};\n  const notifyRaw = String(dq.notify || '').trim();\n  chatId = String(dq.telegram_chat_id || dq.chat_id || '').trim();\n  emailFrom = String(dq.email_from || '').trim();\n  if (!emailFrom && looksLikeEmail(notifyRaw)) emailFrom = notifyRaw;\n  const dataHasChannel = Boolean(\n    replyChannel || commandOrigin === 'email' || commandOrigin === 'telegram' || emailFrom || chatId\n  );\n  if (!dataHasChannel && !replyChannel && ctx.reply_channel) {\n    replyChannel = String(ctx.reply_channel).trim().toLowerCase();\n  }\n  if (!dataHasChannel && !commandOrigin && ctx.command_origin) {\n    commandOrigin = String(ctx.command_origin).trim().toLowerCase();\n  }\n  if (!dataHasChannel && !emailFrom && ctx.email_from) emailFrom = String(ctx.email_from).trim();\n  const inputIsEmail = commandOrigin === 'email' || replyChannel === 'email' || emailFrom;\n  if (!inputIsEmail && !chatId && ctx.chat_id) chatId = String(ctx.chat_id).trim();\n  if (ctx.command_route) commandRoute = String(ctx.command_route);\n} catch (e) {}\nif (!replyChannel) {\n  if (commandOrigin === 'email' || emailFrom) replyChannel = 'email';\n  else if (commandOrigin === 'telegram' || chatId) replyChannel = 'telegram';\n}\nif (!commandOrigin && replyChannel) commandOrigin = replyChannel;\nif (replyChannel === 'email') {\n  notify = 'email';\n  commandOrigin = 'email';\n  chatId = '';\n} else if (replyChannel === 'telegram') {\n  notify = 'telegram';\n  commandOrigin = 'telegram';\n  emailFrom = '';\n} else if (chatId) {\n  notify = 'telegram';\n  replyChannel = 'telegram';\n  commandOrigin = commandOrigin || 'telegram';\n} else if (emailFrom) {\n  notify = 'email';\n  replyChannel = 'email';\n  commandOrigin = commandOrigin || 'email';\n}\n\nconst rawSkus = Array.isArray(dq.skus) ? dq.skus : [];\nconst rawSheetRows = Array.isArray(dq.sheet_rows) ? dq.sheet_rows : rawSkus;\nif (!dq.valid_skus || dq.valid_skus < 1 || !rawSkus.length) {\n  return [\n    {\n      json: {\n        skip_stokapi: true,\n        skip_muvstok: true,\n        reason: 'no_skus',\n        chat_id: notify === 'telegram' ? chatId : '',\n        reply_email: notify === 'email' ? emailFrom : '',\n        notify,\n        reply_channel: replyChannel || notify,\n        command_origin: commandOrigin || replyChannel || notify,\n        command_route: commandRoute,\n      },\n    },\n  ];\n}\n\nfunction skuKey(value) {\n  return String(value || '')\n    .trim()\n    .toUpperCase()\n    .replace(/[\\s\\-\\.\\\\/]/g, '');\n}\n\nconst skuRows = rawSkus\n  .map((row) => {\n    const sku = String(row.sku || row.SKU || row).trim().toUpperCase();\n    return {\n      sku,\n      row_number: row.row_number ?? null,\n      description: String(row.description || row.ITEM || '').trim(),\n    };\n  })\n  .filter((r) => r.sku.length >= 3);\nconst dispatchedSkuKeys = new Set(skuRows.map((row) => skuKey(row.sku)));\nconst sheetRows = rawSheetRows\n  .map((row) => {\n    const sku = String(row.sku || row.SKU || row).trim().toUpperCase();\n    return {\n      sku,\n      row_number: row.row_number ?? null,\n      description: String(row.description || row.ITEM || '').trim(),\n    };\n  })\n  .filter((row) => dispatchedSkuKeys.has(skuKey(row.sku)));\n\nconst batchGroupId =\n  String(dq.batch_group_id || '').trim() ||\n  'bg-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);\n\nlet callbackUrl = envFor('CDP_MUVSTOK_N8N_WEBHOOK_URL');\nif (!callbackUrl) {\n  const base = trimTrailingSlashes(envFor('WEBHOOK_URL'));\n  const rel = trimSlashes(envFor('CDP_MUVSTOK_WEBHOOK_PATH') || 'webhook/muvstok-result');\n  if (base) callbackUrl = base + '/' + rel;\n}\nif (!callbackUrl && !isDevWorkflow()) {\n  callbackUrl = 'https://automacao.tktechnologies.com.br/webhook/muvstok-result';\n}\n\nconst deliveryMode = notify === 'none' ? 'legacy' : 'aggregate';\nconst queryParts = [\n  encodeQueryParam('notify', notify),\n  encodeQueryParam('reply_channel', replyChannel || notify),\n  encodeQueryParam('command_origin', commandOrigin || replyChannel || notify),\n  encodeQueryParam('batch_group_id', batchGroupId),\n  encodeQueryParam('dual_run', 'stokapi'),\n  encodeQueryParam('command_route', commandRoute),\n  encodeQueryParam('delivery_mode', deliveryMode),\n];\nif (notify === 'telegram' && chatId) queryParts.push(encodeQueryParam('chat_id', chatId));\nif (notify === 'email' && emailFrom) queryParts.push(encodeQueryParam('reply_email', emailFrom));\ncallbackUrl += (callbackUrl.includes('?') ? '&' : '?') + queryParts.join('&');\n\nconst apiBase = trimTrailingSlashes(\n  envFor('CDP_MUVSTOK_API_BASE') ||\n    (isDevWorkflow() ? '' : 'https://cdp-muv-api.bravecoast-b14d791e.eastus2.azurecontainerapps.io')\n);\nconst apiKey =\n  envFor('CDP_MUVSTOK_API_KEY') ||\n  envFor('CDP_API_KEY') ||\n  envFor('MUVSTOK_API_KEY') ||\n  envFor('API_KEY');\n\nconst metadata = {\n  source: 'cdp_router',\n  pipeline: 'stokapi',\n  command_route: commandRoute,\n  command_origin: commandOrigin || replyChannel || notify,\n  reply_channel: replyChannel || notify,\n  notify,\n  delivery_mode: notify === 'none' ? 'legacy' : 'aggregate',\n  chat_id: notify === 'telegram' ? chatId : '',\n  reply_email: notify === 'email' ? emailFrom : '',\n  batch_index: 1,\n  total_batches: 1,\n  batch_group_id: batchGroupId,\n  dispatched_at: dq.dispatched_at || new Date().toISOString(),\n  unique_skus: skuRows.length,\n  sheet_rows: sheetRows.length,\n  duplicate_skus: Array.isArray(dq.duplicate_skus) ? dq.duplicate_skus : [],\n};\n\nreturn [\n  {\n    json: {\n      skus: skuRows.map((r) => r.sku),\n      sku_rows: sheetRows,\n      callback_url: callbackUrl,\n      api_jobs_url: apiBase + '/api/v1/muvstok/jobs',\n      api_key: apiKey,\n      metadata,\n      idempotency_key: batchGroupId + '-stokapi-1',\n      chat_id: notify === 'telegram' ? chatId : '',\n      email_from: notify === 'email' ? emailFrom : '',\n      notify,\n      reply_channel: replyChannel || notify,\n      command_origin: commandOrigin || replyChannel || notify,\n      batch_group_id: batchGroupId,\n      command_route: commandRoute,\n    },\n  },\n];\n",
        "mode": "runOnceForAllItems"
      },
      "id": "b2c3d4e5-f6a7-4890-b123-4567890abcde",
      "name": "\ud83d\udce4 Router: API Diversos",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1680,
        2720
      ],
      "notes": "Router arm B: builds API Diversos POST body + callback with requester chat_id."
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 1
          },
          "conditions": [
            {
              "id": "can-dispatch-muv",
              "leftValue": "={{ !$json.skip_stokapi && !$json.skip_muvstok && !!$json.api_jobs_url && ($json.skus || []).length > 0 }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b7c8d9e0-f1a2-4345-b678-9012345abcde",
      "name": "\u2753 Disparar API Diversos?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1840,
        2720
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $json.api_jobs_url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "X-API-Key",
              "value": "={{ $json.api_key }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ skus: $json.skus, callback_url: $json.callback_url, metadata: $json.metadata, idempotency_key: $json.idempotency_key }) }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          },
          "timeout": 90000
        }
      },
      "id": "c3d4e5f6-a7b8-4901-c234-5678901bcdef",
      "name": "\ud83d\ude80 POST API Diversos",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1920,
        2720
      ],
      "continueOnFail": true,
      "notes": "Inline POST to cdp-muv-api (avoids broken sub-workflow URL expressions on live)."
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 1
          },
          "conditions": [
            {
              "id": "muv-job-ok",
              "leftValue": "={{ !!($json.job_id || $json.body?.job_id) }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "e4f5a6b7-c8d9-4012-e345-6789012cdef3",
      "name": "\ud83d\udce6 API Diversos job aceito?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2160,
        2640
      ]
    },
    {
      "parameters": {
        "jsCode": "const resp = $input.first().json;\nlet prep = {};\ntry {\n  prep = $('\ud83d\udce4 Router: API Diversos').first().json;\n} catch (e) {}\n\nlet chatId = String(prep.chat_id || '').trim();\nlet statusCode = resp.statusCode ?? resp.status_code ?? 'N/A';\nlet errorText = resp.error || resp.message || resp.detail || '';\n\nif (resp.parallel_dispatch) {\n  const stokapi = resp.stokapi_response || {};\n  if (stokapi.accepted || stokapi.skipped) return [];\n  chatId = String(resp.chat_id || '').trim();\n  statusCode = stokapi.statusCode ?? 'N/A';\n  errorText = stokapi.error || stokapi.body?.detail || stokapi.body?.message || 'dispatch_not_accepted';\n}\n\nif (!chatId || prep.skip_stokapi || prep.skip_muvstok) return [];\nconst msg = [\n  '\ud83e\udd16 *Assistente CDP*',\n  '',\n  '\u26a0\ufe0f A consulta de estoque n\u00e3o iniciou nesta rodada.',\n  'A busca em sites continua \u2014 voc\u00ea receber\u00e1 o aviso de sites normalmente.',\n  statusCode !== 'N/A' || errorText ? 'Erro: HTTP ' + statusCode + ' \u2014 ' + String(errorText).slice(0, 160) : '',\n  '',\n  'Se o problema persistir, tente novamente em alguns minutos.',\n].filter((line) => line !== '').join('\\n');\nreturn [{ json: { chat_id: chatId, msg } }];\n",
        "mode": "runOnceForAllItems"
      },
      "id": "f5a6b7c8-d9e0-4123-f456-7890123def45",
      "name": "\ud83d\udccb Formatar erro API Diversos",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2384,
        2800
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $json.chat_id }}",
        "text": "={{ $json.msg }}",
        "additionalFields": {
          "parse_mode": "Markdown",
          "appendAttribution": false
        }
      },
      "id": "a6b7c8d9-e0f1-4234-a567-8901234ef56a",
      "name": "\ud83d\udcf1 Telegram: erro API Diversos",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.1,
      "position": [
        2608,
        2800
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "// cdp_router - persist requester channel before reading CDP_SKUs.\n\nfunction looksLikeEmail(value) {\n  return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(String(value || '').trim());\n}\n\nconst d = $input.first().json;\nlet chatId = String(d.chat_id || d.telegram_chat_id || '').trim();\nlet emailFrom = String(d.email_from || '').trim();\nconst notifyRaw = String(d.notify || '').trim();\nlet commandOrigin = String(d.command_origin || d.origem || '').trim().toLowerCase();\nlet replyChannel = String(d.reply_channel || '').trim().toLowerCase();\n\nif (!emailFrom && looksLikeEmail(notifyRaw)) emailFrom = notifyRaw;\nif (!chatId && notifyRaw && !looksLikeEmail(notifyRaw)) chatId = notifyRaw;\n\nif (!replyChannel) {\n  if (commandOrigin === 'email' || emailFrom) replyChannel = 'email';\n  else if (commandOrigin === 'telegram' || chatId) replyChannel = 'telegram';\n}\nif (!commandOrigin && replyChannel) commandOrigin = replyChannel;\n\nlet notify = 'none';\nif (replyChannel === 'email') {\n  notify = 'email';\n  commandOrigin = 'email';\n  chatId = '';\n} else if (replyChannel === 'telegram') {\n  notify = 'telegram';\n  commandOrigin = 'telegram';\n  emailFrom = '';\n} else if (chatId) {\n  replyChannel = 'telegram';\n  commandOrigin = commandOrigin || 'telegram';\n  notify = 'telegram';\n} else if (emailFrom) {\n  replyChannel = 'email';\n  commandOrigin = commandOrigin || 'email';\n  notify = 'email';\n}\n\ntry {\n  if (typeof $getWorkflowStaticData === 'function') {\n    const sd = $getWorkflowStaticData('global');\n    sd.cdp_sheet_requester = {\n      chat_id: chatId,\n      email_from: emailFrom,\n      notify,\n      reply_channel: replyChannel || notify,\n      command_origin: commandOrigin || replyChannel || notify,\n    };\n  }\n} catch (e) {}\n\nreturn [\n  {\n    json: {\n      ...d,\n      chat_id: chatId,\n      email_from: emailFrom,\n      notify,\n      reply_channel: replyChannel || notify,\n      command_origin: commandOrigin || replyChannel || notify,\n    },\n  },\n];\n",
        "mode": "runOnceForAllItems"
      },
      "id": "a94b70bc-e5ec-4851-8cbe-702d3db497c4",
      "name": "\ud83d\udcbe Salvar Contexto do Solicitante",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        896,
        1952
      ],
      "notes": "v0.8.4: Persists chat_id/email_from in workflow staticData before \ud83d\udcca Ler CDP_SKUs so \u2699\ufe0f Formatar Payload and \ud83d\udccb Formatar Confirma\u00e7\u00e3o can recover routing after the sheet read."
    },
    {
      "parameters": {
        "jsCode": "// cdp_router \u2014 persist active dual-pipeline run (staticData + dispatch_runs API).\n\nconst DEFAULT_SCRAPER_API_BASE =\n  'https://cdp-scrapers-api-prod.bravecoast-b14d791e.eastus2.azurecontainerapps.io';\n\nfunction env(name) {\n  try {\n    if (typeof $env !== 'undefined' && $env && $env[name]) {\n      return String($env[name]).trim();\n    }\n  } catch (e) {}\n  try {\n    if (typeof process !== 'undefined' && process.env && process.env[name]) {\n      return String(process.env[name]).trim();\n    }\n  } catch (e) {}\n  return '';\n}\n\nfunction workflowName() {\n  try {\n    if (typeof $workflow !== 'undefined' && $workflow && $workflow.name) {\n      return String($workflow.name);\n    }\n  } catch (e) {}\n  return '';\n}\n\nfunction isDevWorkflow() {\n  return workflowName().trim().toLowerCase().startsWith('dev -') || env('CDP_ENV').toLowerCase() === 'dev';\n}\n\nfunction devEnvName(name) {\n  return {\n    CDP_SCRAPER_API_BASE: 'CDP_DEV_SCRAPER_API_BASE',\n    MUVSTOK_SCRAPER_API_BASE: 'CDP_DEV_SCRAPER_API_BASE',\n    CDP_API_KEY: 'CDP_DEV_API_KEY',\n    MUVSTOK_API_KEY: 'CDP_DEV_API_KEY',\n    API_KEY: 'CDP_DEV_API_KEY',\n  }[name] || '';\n}\n\nfunction envFor(name, defaultVal = '') {\n  if (!isDevWorkflow()) return env(name) || defaultVal;\n  const mapped = devEnvName(name);\n  if (mapped) return env(mapped) || defaultVal;\n  return env(name) || defaultVal;\n}\n\nfunction trimTrailingSlashes(value) {\n  let out = String(value || '').trim();\n  while (out.endsWith('/')) out = out.slice(0, -1);\n  return out;\n}\n\nlet scraperJobIds = [];\ntry {\n  const scraperResponses = $('\ud83d\ude80 POST \u2192 Scraper API (/jobs)').all();\n  scraperJobIds = scraperResponses\n    .map((item) => String(item.json.job_id || item.json.body?.job_id || '').trim())\n    .filter(Boolean);\n} catch (e) {}\n\nlet stokapiJobId = '';\ntry {\n  const stokResp = $('\ud83d\ude80 POST API Diversos').first().json;\n  stokapiJobId = String(stokResp.job_id || stokResp.id || stokResp.body?.job_id || '').trim();\n} catch (e) {}\n\nlet dispatch = {};\ntry {\n  dispatch = $('\ud83c\udfb2 Limitar SKUs').first().json;\n} catch (e) {}\n\nlet confirmacao = {};\ntry {\n  confirmacao = $('\ud83d\udccb Formatar Confirma\u00e7\u00e3o (Planilha)').first().json;\n} catch (e) {}\n\nconst batchGroupId = String(dispatch.batch_group_id || '').trim();\nconst chatId = String(confirmacao.chat_id || dispatch.chat_id || dispatch.telegram_chat_id || '').trim();\nconst replyChannel = String(confirmacao.reply_channel || dispatch.reply_channel || '').trim().toLowerCase();\nconst replyEmail = String(confirmacao.email_from || dispatch.reply_email || dispatch.email_from || '').trim();\nconst commandOrigin = String(\n  confirmacao.command_origin || dispatch.command_origin || replyChannel || ''\n).trim().toLowerCase();\nconst commandRoute = String(dispatch.command_route || 'analisar');\nconst totalSkus = Number(dispatch.valid_skus || dispatch.total_skus || 0);\nconst estimatedSeconds = Number(confirmacao.mins || 0) * 60;\nconst dispatchedAt = String(dispatch.dispatched_at || new Date().toISOString());\nconst sheetRows = Array.isArray(dispatch.sheet_rows) ? dispatch.sheet_rows : [];\nconst sheetRowNumbers = [\n  ...new Set(\n    sheetRows\n      .map((row) => Number(row.row_number))\n      .filter((n) => Number.isFinite(n) && n > 0)\n  ),\n];\nconst hasRecipient =\n  (replyChannel === 'telegram' && chatId) || (replyChannel === 'email' && replyEmail) || chatId || replyEmail;\nconst deliveryMode = hasRecipient ? 'aggregate' : 'legacy';\nconst progressEnabled = deliveryMode !== 'aggregate';\n\nlet previousRun = null;\ntry {\n  if (typeof $getWorkflowStaticData === 'function') {\n    previousRun = $getWorkflowStaticData('global').cdp_active_run || null;\n  }\n} catch (e) {}\n\nif (previousRun && String(previousRun.batch_group_id || '') === batchGroupId) {\n  const mergedScraperIds = [\n    ...(Array.isArray(previousRun.scraper_job_ids) ? previousRun.scraper_job_ids : []),\n    ...scraperJobIds,\n  ]\n    .map((id) => String(id || '').trim())\n    .filter(Boolean);\n  scraperJobIds = [...new Set(mergedScraperIds)];\n  stokapiJobId = stokapiJobId || String(previousRun.stokapi_job_id || '').trim();\n}\n\nif (!scraperJobIds.length && !stokapiJobId) {\n  return [];\n}\n\nconst activeRun = {\n  batch_group_id: batchGroupId,\n  scraper_job_ids: scraperJobIds,\n  stokapi_job_id: stokapiJobId,\n  total_skus: totalSkus,\n  dispatched_at: previousRun?.batch_group_id === batchGroupId ? previousRun.dispatched_at || dispatchedAt : dispatchedAt,\n  estimated_seconds: estimatedSeconds,\n  chat_id: chatId,\n  reply_channel: replyChannel || (replyEmail ? 'email' : chatId ? 'telegram' : ''),\n  reply_email: replyEmail,\n  command_origin: commandOrigin,\n  command_route: commandRoute,\n  progress_enabled: progressEnabled,\n  delivery_mode: deliveryMode,\n  sheet_row_numbers: sheetRowNumbers,\n  scraper_completed: scraperJobIds.length < 1,\n  stokapi_completed: !stokapiJobId,\n  last_progress_notified_pct: 0,\n  progress_message_count: 0,\n};\n\ntry {\n  if (typeof $getWorkflowStaticData === 'function') {\n    const sd = $getWorkflowStaticData('global');\n    sd.cdp_active_run = activeRun;\n  }\n} catch (e) {}\n\nconst apiBase = trimTrailingSlashes(\n  envFor('CDP_SCRAPER_API_BASE') ||\n    envFor('MUVSTOK_SCRAPER_API_BASE') ||\n    (isDevWorkflow() ? '' : DEFAULT_SCRAPER_API_BASE)\n);\nconst apiKey = envFor('CDP_API_KEY') || envFor('MUVSTOK_API_KEY') || envFor('API_KEY');\n\nreturn [\n  {\n    json: {\n      registered: true,\n      ...activeRun,\n      dispatch_runs_url: apiBase ? apiBase + '/api/v1/dispatch-runs' : '',\n      dispatch_runs_api_key: apiKey,\n      reply_channel: replyChannel || (replyEmail ? 'email' : chatId ? 'telegram' : ''),\n      reply_email: replyEmail,\n      command_origin: commandOrigin,\n      progress_enabled: progressEnabled,\n      delivery_mode: deliveryMode,\n      sheet_row_numbers: sheetRowNumbers,\n    },\n  },\n];\n",
        "mode": "runOnceForAllItems"
      },
      "id": "15c1075b-12f2-475f-806b-f8aee258ba37",
      "name": "\ud83d\udcca Registrar Execu\u00e7\u00e3o",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2224,
        2528
      ],
      "notes": "Phase 2: stores active run (job IDs, metadata) in workflow staticData after both API POSTs."
    },
    {
      "parameters": {
        "jsCode": "// cdp_router \u2014 .status / .andamento: resolve active run and API poll targets.\n\nconst DEFAULT_SCRAPER_API_BASE =\n  'https://cdp-scrapers-api-prod.bravecoast-b14d791e.eastus2.azurecontainerapps.io';\n\nfunction env(name) {\n  try {\n    if (typeof $env !== 'undefined' && $env && $env[name]) {\n      return String($env[name]).trim();\n    }\n  } catch (e) {}\n  try {\n    if (typeof process !== 'undefined' && process.env && process.env[name]) {\n      return String(process.env[name]).trim();\n    }\n  } catch (e) {}\n  return '';\n}\nfunction workflowName() {\n  try {\n    if (typeof $workflow !== 'undefined' && $workflow && $workflow.name) {\n      return String($workflow.name).trim();\n    }\n  } catch (e) {}\n  return '';\n}\nfunction isDevWorkflow() {\n  return /^DEV\\s*-/i.test(workflowName()) || /^dev$/i.test(env('CDP_ENV'));\n}\nfunction devEnvName(name) {\n  const map = {\n    CDP_SCRAPER_API_BASE: 'CDP_DEV_SCRAPER_API_BASE',\n    MUVSTOK_SCRAPER_API_BASE: 'CDP_DEV_SCRAPER_API_BASE',\n    CDP_MUVSTOK_API_BASE: 'CDP_DEV_MUVSTOK_API_BASE',\n    CDP_API_KEY: 'CDP_DEV_API_KEY',\n    MUVSTOK_API_KEY: 'CDP_DEV_API_KEY',\n    API_KEY: 'CDP_DEV_API_KEY',\n    CDP_MUVSTOK_API_KEY: 'CDP_DEV_MUVSTOK_API_KEY',\n  };\n  return map[name] || '';\n}\nfunction envFor(name) {\n  if (!isDevWorkflow()) return env(name);\n  const mapped = devEnvName(name);\n  return mapped ? env(mapped) : '';\n}\n\nfunction trimTrailingSlashes(value) {\n  let out = String(value || '').trim();\n  while (out.endsWith('/')) out = out.slice(0, -1);\n  return out;\n}\n\nconst router = $input.first().json;\nconst chatId = String(router.chat_id || router.notify || '').trim();\n\nlet run = null;\ntry {\n  if (typeof $getWorkflowStaticData === 'function') {\n    run = $getWorkflowStaticData('global').cdp_active_run || null;\n  }\n} catch (e) {}\n\nif (!run || !run.batch_group_id) {\n  try {\n    const apiRun = $('\ud83d\udcca GET dispatch run (chat)').first().json;\n    if (apiRun && apiRun.batch_group_id) {\n      run = {\n        batch_group_id: apiRun.batch_group_id,\n        scraper_job_ids: apiRun.scraper_job_ids || [],\n        stokapi_job_id: apiRun.stokapi_job_id || '',\n        total_skus: Number(apiRun.total_skus || 0),\n        dispatched_at: apiRun.dispatched_at,\n        estimated_seconds: Number(apiRun.estimated_seconds || 0),\n        chat_id: apiRun.chat_id,\n      };\n    }\n  } catch (e) {}\n}\n\nconst TTL_HOURS = 24;\nif (run && run.dispatched_at) {\n  const ageMs = Date.now() - new Date(run.dispatched_at).getTime();\n  if (ageMs > TTL_HOURS * 60 * 60 * 1000) {\n    run = null;\n  }\n}\n\nif (!run || !run.batch_group_id) {\n  return [\n    {\n      json: {\n        skip_poll: true,\n        chat_id: chatId,\n        msg_telegram:\n          '\ud83e\udd16 *Assistente CDP*\\n\\nNenhuma consulta em andamento.\\nUse `.analisar` ou `.sku` para iniciar.',\n      },\n    },\n  ];\n}\n\nif (chatId && run.chat_id && chatId !== String(run.chat_id).trim()) {\n  return [\n    {\n      json: {\n        skip_poll: true,\n        chat_id: chatId,\n        msg_telegram:\n          '\ud83e\udd16 *Assistente CDP*\\n\\nN\u00e3o h\u00e1 consulta ativa para este chat.\\nUse `.analisar` ou `.sku` para iniciar.',\n      },\n    },\n  ];\n}\n\nconst scraperBase = trimTrailingSlashes(\n  envFor('CDP_SCRAPER_API_BASE') ||\n    envFor('MUVSTOK_SCRAPER_API_BASE') ||\n    (isDevWorkflow() ? '' : DEFAULT_SCRAPER_API_BASE)\n);\nconst stokapiBase = trimTrailingSlashes(\n  envFor('CDP_MUVSTOK_API_BASE') ||\n    (isDevWorkflow() ? '' : 'https://cdp-muv-api.bravecoast-b14d791e.eastus2.azurecontainerapps.io')\n);\nconst apiKey = envFor('CDP_API_KEY') || envFor('MUVSTOK_API_KEY') || envFor('API_KEY');\nconst stokapiKey = envFor('CDP_MUVSTOK_API_KEY') || apiKey;\n\nconst scraperJobIds = Array.isArray(run.scraper_job_ids) ? run.scraper_job_ids : [];\nconst primaryScraperJobId = scraperJobIds[0] || '';\n\nreturn [\n  {\n    json: {\n      skip_poll: false,\n      chat_id: chatId || run.chat_id,\n      batch_group_id: run.batch_group_id,\n      total_skus: Number(run.total_skus || 0),\n      dispatched_at: run.dispatched_at,\n      estimated_seconds: Number(run.estimated_seconds || 0),\n      scraper_job_ids: scraperJobIds,\n      stokapi_job_id: String(run.stokapi_job_id || ''),\n      scraper_job_url: primaryScraperJobId\n        ? scraperBase + '/api/v1/jobs/' + primaryScraperJobId\n        : '',\n      stokapi_job_url: run.stokapi_job_id\n        ? stokapiBase + '/api/v1/muvstok/jobs/' + run.stokapi_job_id\n        : '',\n      scraper_api_key: apiKey,\n      stokapi_api_key: stokapiKey,\n    },\n  },\n];\n",
        "mode": "runOnceForAllItems"
      },
      "id": "305278ef-2e34-445e-ab06-2b3c176ba720",
      "name": "\ud83d\udcca Status: preparar",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        432,
        1440
      ],
      "notes": "Phase 3: reads staticData active run, builds API poll URLs or 'no run' message."
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 1
          },
          "conditions": [
            {
              "id": "d8720559-0c7b-4591-b9d9-497beafe4047",
              "leftValue": "={{ !$json.skip_poll }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "1ac24e3d-a38b-4367-b37d-91977712d010",
      "name": "\u2753 Status: tem execu\u00e7\u00e3o?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        656,
        1440
      ]
    },
    {
      "parameters": {
        "url": "={{ $json.scraper_job_url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-API-Key",
              "value": "={{ $json.scraper_api_key }}"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          },
          "timeout": 15000
        }
      },
      "id": "7269b38e-3b0f-48ec-8ab0-ab1630f85577",
      "name": "\ud83d\udcca GET Scraper Job",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        880,
        1320
      ],
      "continueOnFail": true,
      "notes": "Phase 3: GET /api/v1/jobs/{id} for scraper progress."
    },
    {
      "parameters": {
        "url": "={{ $('\ud83d\udcca Status: preparar').first().json.stokapi_job_url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-API-Key",
              "value": "={{ $('\ud83d\udcca Status: preparar').first().json.stokapi_api_key }}"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          },
          "timeout": 15000
        }
      },
      "id": "2396fac2-030e-402a-ac70-7834e3afbeee",
      "name": "\ud83d\udcca GET StokAPI Job",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1104,
        1320
      ],
      "continueOnFail": true,
      "notes": "Phase 3: GET /api/v1/muvstok/jobs/{id} for StokAPI progress."
    },
    {
      "parameters": {
        "jsCode": "// cdp_router \u2014 format combined scraper + StokAPI progress for Telegram.\n\nfunction num(v, fallback) {\n  const n = Number(v);\n  return Number.isFinite(n) ? n : fallback;\n}\n\nfunction statusLabel(status) {\n  const s = String(status || '').toLowerCase();\n  if (s === 'completed' || s === 'succeeded') return 'conclu\u00eddo';\n  if (s === 'partial' || s === 'partially_succeeded') return 'parcial';\n  if (s === 'failed') return 'falhou';\n  if (s === 'running' || s === 'processing') return 'em execu\u00e7\u00e3o';\n  if (s === 'pending' || s === 'queued') return 'na fila';\n  return s || 'desconhecido';\n}\n\nfunction formatDuration(seconds) {\n  const sec = Math.max(0, Math.round(num(seconds, 0)));\n  if (sec < 60) return '~' + sec + ' s';\n  const mins = Math.ceil(sec / 60);\n  return mins === 1 ? '~1 minuto' : '~' + mins + ' minutos';\n}\n\nfunction formatClock(iso) {\n  if (!iso) return '\u2014';\n  try {\n    const d = new Date(iso);\n    return d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });\n  } catch (e) {\n    return '\u2014';\n  }\n}\n\nconst prep = $('\ud83d\udcca Status: preparar').first().json;\nif (prep.skip_poll) {\n  return [{ json: { chat_id: prep.chat_id, msg_telegram: prep.msg_telegram } }];\n}\n\nlet scraper = {};\nlet stokapi = {};\ntry {\n  scraper = $('\ud83d\udcca GET Scraper Job').first().json;\n} catch (e) {}\ntry {\n  stokapi = $('\ud83d\udcca GET StokAPI Job').first().json;\n} catch (e) {}\n\nconst total = num(prep.total_skus, num(scraper.total_items, num(stokapi.submitted_sku_count, 0)));\nconst scraperProcessed = num(\n  scraper.items_processed,\n  num(scraper.items_succeeded, 0) + num(scraper.items_failed, 0)\n);\nconst scraperPct = num(scraper.progress_pct, total > 0 ? (scraperProcessed / total) * 100 : 0);\nconst stokProcessed = num(\n  stokapi.processed_sku_count,\n  num(stokapi.succeeded_sku_count, 0) + num(stokapi.failed_sku_count, 0)\n);\nconst stokPct = num(stokapi.progress_pct, total > 0 ? (stokProcessed / total) * 100 : 0);\n\nconst dispatchedAt = prep.dispatched_at || scraper.started_at;\nconst elapsedSec = dispatchedAt\n  ? Math.max(0, Math.floor((Date.now() - new Date(dispatchedAt).getTime()) / 1000))\n  : 0;\n\nconst scraperEta = scraper.estimated_seconds_remaining;\nconst stokEta = stokapi.estimated_seconds_remaining;\nconst etaSec =\n  num(scraperEta, null) != null && scraperEta > 0\n    ? scraperEta\n    : num(stokEta, null) != null && stokEta > 0\n      ? stokEta\n      : Math.max(0, num(prep.estimated_seconds, 0) - elapsedSec);\n\nconst lines = [\n  '\ud83e\udd16 *Assistente CDP* \u2014 andamento',\n  '',\n  '\ud83d\udce6 *Sites (scraper):* ' +\n    scraperProcessed +\n    '/' +\n    (total || '?') +\n    ' pe\u00e7as (~' +\n    Math.round(scraperPct) +\n    '%) \u2014 ' +\n    statusLabel(scraper.status),\n];\n\nif (etaSec > 0 && String(scraper.status || '').toLowerCase() === 'running') {\n  lines.push('\u23f1\ufe0f ' + formatDuration(etaSec) + ' restantes (estimativa)');\n}\nlines.push(\n  '\ud83d\udd50 In\u00edcio: ' + formatClock(dispatchedAt) + ' \u00b7 J\u00e1 decorrido: ' + formatDuration(elapsedSec)\n);\nlines.push('');\nlines.push(\n  '\ud83d\udce6 *Estoque:* ' +\n    statusLabel(stokapi.status) +\n    ' (' +\n    stokProcessed +\n    '/' +\n    (total || '?') +\n    ' com dados, ~' +\n    Math.round(stokPct) +\n    '%)'\n);\n\nconst scraperTerminal = ['completed', 'partial', 'failed'].includes(\n  String(scraper.status || '').toLowerCase()\n);\nconst stokTerminal = ['succeeded', 'partially_succeeded', 'failed'].includes(\n  String(stokapi.status || '').toLowerCase()\n);\n\ntry {\n  if (typeof $getWorkflowStaticData === 'function') {\n    const sd = $getWorkflowStaticData('global');\n    if (sd.cdp_active_run) {\n      sd.cdp_active_run.scraper_completed = scraperTerminal;\n      sd.cdp_active_run.stokapi_completed = stokTerminal;\n      if (scraperTerminal && stokTerminal) {\n        delete sd.cdp_active_run;\n      }\n    }\n  }\n} catch (e) {}\n\nreturn [{ json: { chat_id: prep.chat_id, msg_telegram: lines.join('\\n') } }];\n",
        "mode": "runOnceForAllItems"
      },
      "id": "9fba51da-250e-4b5e-8502-859f11c25545",
      "name": "\ud83d\udcca Formatar Status",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1328,
        1320
      ],
      "notes": "Phase 3: combines scraper + StokAPI HTTP responses into PT-BR status message."
    },
    {
      "parameters": {
        "chatId": "={{ $json.chat_id }}",
        "text": "={{ $json.msg_telegram }}",
        "additionalFields": {
          "parse_mode": "Markdown",
          "appendAttribution": false
        }
      },
      "id": "4cac7ed4-94a1-410d-a53f-945069f781e8",
      "name": "\ud83d\udcf1 Enviar Status (Telegram)",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.1,
      "position": [
        1552,
        1440
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "continueOnFail": true,
      "notes": "Phase 3: sends .status/.andamento progress reply to Telegram."
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $json.dispatch_runs_url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "X-API-Key",
              "value": "={{ $json.dispatch_runs_api_key }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ batch_group_id: $json.batch_group_id, chat_id: $json.chat_id, command_route: $json.command_route, scraper_job_ids: $json.scraper_job_ids, stokapi_job_id: $json.stokapi_job_id, total_skus: $json.total_skus, estimated_seconds: $json.estimated_seconds, dispatched_at: $json.dispatched_at, reply_channel: $json.reply_channel, reply_email: $json.reply_email, command_origin: $json.command_origin, progress_enabled: $json.progress_enabled, delivery_mode: $json.delivery_mode, sheet_row_numbers: $json.sheet_row_numbers }) }}",
        "options": {
          "timeout": 15000
        }
      },
      "id": "e42f357d-c4b7-4048-8923-cc7e9c40b6fa",
      "name": "\ud83d\udccb POST dispatch-runs",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2448,
        2528
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "url": "={{ $json.dispatch_runs_lookup_url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-API-Key",
              "value": "={{ $json.dispatch_runs_api_key }}"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          },
          "timeout": 15000
        },
        "continueOnFail": true
      },
      "id": "09be08e5-d945-4d43-a92a-784bcf42a4db",
      "name": "\ud83d\udcca GET dispatch run (chat)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        256,
        1200
      ],
      "notes": "Phase 5: Postgres dispatch_runs fallback when staticData is empty."
    }
  ],
  "connections": {
    "Gmail Trigger": {
      "main": [
        [
          {
            "node": "\ud83d\udce7 Roteador de Comando",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcf1 Trigger Telegram": {
      "main": [
        [
          {
            "node": "\ud83d\udcf1 Roteador de Comando",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udce7 Email entrada OK?": {
      "main": [
        [
          {
            "node": "\ud83c\udfb2 Limitar SKUs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcf1 Telegram entrada OK?": {
      "main": [
        [
          {
            "node": "\ud83c\udfb2 Limitar SKUs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd17 Emparelhar SKUs \u2192 PROCESSADO": {
      "main": [
        [
          {
            "node": "\u2705 Marcar PROCESSADO \u2192 CDP_SKUs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u274c Formatar Erro de Despacho": {
      "main": [
        [
          {
            "node": "\ud83d\udcdd Erro \u2192 CDP_Resultados (Hist\u00f3rico)",
            "type": "main",
            "index": 0
          },
          {
            "node": "\ud83d\udce7 Enviar Alerta de Erro",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2705 API OK?": {
      "main": [
        [
          {
            "node": "\ud83d\udd17 Emparelhar SKUs \u2192 PROCESSADO",
            "type": "main",
            "index": 0
          },
          {
            "node": "\ud83d\udcca Registrar Execu\u00e7\u00e3o",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\u274c Formatar Erro de Despacho",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\ude80 POST \u2192 Scraper API (/jobs)": {
      "main": [
        [
          {
            "node": "\u2705 API OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2699\ufe0f Formatar Payload Scraper": {
      "main": [
        [
          {
            "node": "\ud83d\ude80 POST \u2192 Scraper API (/jobs)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd0d DQ: Validar & Deduplicar": {
      "main": [
        [
          {
            "node": "\ud83c\udfb2 Limitar SKUs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83c\udfb2 Limitar SKUs": {
      "main": [
        [
          {
            "node": "\ud83d\udce4 Router: API Diversos",
            "type": "main",
            "index": 0
          },
          {
            "node": "\u2699\ufe0f Formatar Payload Scraper",
            "type": "main",
            "index": 0
          },
          {
            "node": "\ud83d\udccb Formatar Confirma\u00e7\u00e3o (Planilha)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udce4 Router: API Diversos": {
      "main": [
        [
          {
            "node": "\u2753 Disparar API Diversos?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2753 Disparar API Diversos?": {
      "main": [
        [
          {
            "node": "\ud83d\ude80 POST API Diversos",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "\ud83d\ude80 POST API Diversos": {
      "main": [
        [
          {
            "node": "\ud83d\udce6 API Diversos job aceito?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udce6 API Diversos job aceito?": {
      "main": [
        [
          {
            "node": "\ud83d\udcca Registrar Execu\u00e7\u00e3o",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\ud83d\udccb Formatar erro API Diversos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udccb Formatar erro API Diversos": {
      "main": [
        [
          {
            "node": "\ud83d\udcf1 Telegram: erro API Diversos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcca Ler CDP_SKUs": {
      "main": [
        [
          {
            "node": "\ud83d\udd0d DQ: Validar & Deduplicar",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u23f0 Trigger Agendado (Seg 8h)": {
      "main": [
        [
          {
            "node": "\ud83d\udcca Ler CDP_SKUs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u25b6\ufe0f Trigger Manual": {
      "main": [
        [
          {
            "node": "\ud83d\udcca Ler CDP_SKUs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcf1 Roteador de Comando": {
      "main": [
        [
          {
            "node": "\ud83d\udd00 Switch Comando (Telegram)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd00 Switch Comando (Telegram)": {
      "main": [
        [
          {
            "node": "\ud83d\udcbe Salvar Contexto do Solicitante",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\ud83d\udcf1 Parsear SKUs do Telegram",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\ud83d\udce5 Obter Path do Arquivo (Telegram)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\ud83d\udce5 Obter Path do Arquivo (Telegram)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\ud83d\udcf1 Resposta .sku Vazio (Telegram)",
            "type": "main",
            "index": 0
          }
        ],
        [],
        [],
        [
          {
            "node": "\ud83d\udcca GET dispatch run (chat)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udce7 Roteador de Comando": {
      "main": [
        [
          {
            "node": "\ud83d\udd00 Switch Comando (Email)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd00 Switch Comando (Email)": {
      "main": [
        [
          {
            "node": "\ud83d\udcbe Salvar Contexto do Solicitante",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\ud83d\udce7 Parsear SKUs do Email",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\ud83d\udce5 Baixar Anexo (Email)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\ud83d\udce5 Baixar Anexo (Email)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\ud83d\udce7 Resposta .sku Vazio (Email)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcf1 Parsear SKUs do Telegram": {
      "main": [
        [
          {
            "node": "\ud83d\udcf1 Telegram entrada OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udce5 Obter Path do Arquivo (Telegram)": {
      "main": [
        [
          {
            "node": "\ud83d\udce5 Baixar Arquivo (Telegram)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udce5 Baixar Arquivo (Telegram)": {
      "main": [
        [
          {
            "node": "\ud83d\udcca Parsear Planilha (Telegram)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcca Parsear Planilha (Telegram)": {
      "main": [
        [
          {
            "node": "\ud83d\udccb Formatar SKUs da Planilha (Telegram)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udccb Formatar SKUs da Planilha (Telegram)": {
      "main": [
        [
          {
            "node": "\ud83d\udcf1 Telegram entrada OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udce7 Parsear SKUs do Email": {
      "main": [
        [
          {
            "node": "\ud83d\udce7 Email entrada OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udce5 Baixar Anexo (Email)": {
      "main": [
        [
          {
            "node": "\ud83d\udcca Parsear Planilha (Email)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcca Parsear Planilha (Email)": {
      "main": [
        [
          {
            "node": "\ud83d\udccb Formatar SKUs da Planilha (Email)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udccb Formatar SKUs da Planilha (Email)": {
      "main": [
        [
          {
            "node": "\ud83d\udce7 Email entrada OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udccb Formatar Confirma\u00e7\u00e3o (Planilha)": {
      "main": [
        [
          {
            "node": "\u2705 Foi Disparado por Comando?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2705 Foi Disparado por Comando?": {
      "main": [
        [
          {
            "node": "\ud83d\udd00 Rota Confirma\u00e7\u00e3o Planilha",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd00 Rota Confirma\u00e7\u00e3o Planilha": {
      "main": [
        [
          {
            "node": "\ud83d\udcf1 Confirmar Planilha (Telegram)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\ud83d\udce7 Confirmar Planilha (Email)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcbe Salvar Contexto do Solicitante": {
      "main": [
        [
          {
            "node": "\ud83d\udcca Ler CDP_SKUs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcca Status: preparar": {
      "main": [
        [
          {
            "node": "\u2753 Status: tem execu\u00e7\u00e3o?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2753 Status: tem execu\u00e7\u00e3o?": {
      "main": [
        [
          {
            "node": "\ud83d\udcca GET Scraper Job",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\ud83d\udcf1 Enviar Status (Telegram)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcca GET Scraper Job": {
      "main": [
        [
          {
            "node": "\ud83d\udcca GET StokAPI Job",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcca GET StokAPI Job": {
      "main": [
        [
          {
            "node": "\ud83d\udcca Formatar Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcca Formatar Status": {
      "main": [
        [
          {
            "node": "\ud83d\udcf1 Enviar Status (Telegram)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcca Registrar Execu\u00e7\u00e3o": {
      "main": [
        [
          {
            "node": "\ud83d\udccb POST dispatch-runs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcca GET dispatch run (chat)": {
      "main": [
        [
          {
            "node": "\ud83d\udcca Status: preparar",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "cdp_router"
}