{
  "name": "Calidad de Lata \u2014 8) Simulador de ingesta (Dashboard \u2192 Cola)",
  "nodes": [
    {
      "id": "sim-wh",
      "name": "Recibir simulaci\u00f3n",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        0
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "simulador-calidad",
        "responseMode": "responseNode",
        "options": {
          "allowedOrigins": "https://sudamericana.opendraft.tech"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "sim-cfg",
      "name": "Leer config",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        224,
        0
      ],
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT clave, valor FROM config;",
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 2,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "sim-val",
      "name": "Validar y preparar",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        448,
        0
      ],
      "parameters": {
        "jsCode": "const body = $('Recibir simulaci\u00f3n').first().json.body || {};\nconst cfg = $input.all().map(i => i.json);\nconst tokenCfg = (cfg.find(x => x && x.clave === 'simulador_token') || {}).valor || null;\nconst auth_ok = !!tokenCfg && String(body.token || '') === String(tokenCfg);\nconst b64 = String(body.imagen_b64 || '').replace(/^data:[^;]+;base64,/, '');\nconst valido = b64.length > 100;\nif (!auth_ok || !valido) return [{ json: { auth_ok, valido } }];\nconst fileName = body.filename || 'foto.jpg';\nconst carpeta = String(body.carpeta || 'sin-carpeta');\nconst operario = String(body.operario || 'operario-sim');\nconst chatId = String(body.chat_id || '-1003908341093');\nconst messageId = 'sim' + Date.now() + Math.floor(Math.random() * 1000);\nconst proceso = 'calidad-lata';\nconst evidenceId = proceso + ':' + chatId + ':' + messageId;\nconst imagenRef = proceso + '/' + chatId + '/' + messageId + '.jpg';\nreturn [{ json: { auth_ok, valido, evidence_id: evidenceId, proceso, linea_fallback: carpeta, equipo: '', capturado_en: new Date().toISOString(), imagen_ref: imagenRef, origen: { chat_id: chatId, chat_title: carpeta, from_id: operario, message_id: messageId, via: 'simulador' }, caption: '', has_photo: true }, binary: { data: { data: b64, mimeType: 'image/jpeg', fileName } } }];"
      }
    },
    {
      "id": "sim-auth",
      "name": "\u00bfAutorizado?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        672,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "a1",
              "leftValue": "={{ $json.auth_ok === true && $json.valido === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "sim-rej",
      "name": "Rechazar",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        896,
        160
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ ok: false, error: $json.auth_ok ? 'imagen faltante o invalida' : 'token invalido' }) }}",
        "options": {
          "responseCode": 401,
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "https://sudamericana.opendraft.tech"
              }
            ]
          }
        }
      }
    },
    {
      "id": "sim-hash",
      "name": "Hash imagen",
      "type": "n8n-nodes-base.crypto",
      "typeVersion": 2,
      "position": [
        896,
        -96
      ],
      "parameters": {
        "binaryData": true,
        "dataPropertyName": "imagen_hash"
      }
    },
    {
      "id": "sim-rebin",
      "name": "Reasignar binario",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1120,
        -96
      ],
      "parameters": {
        "jsCode": "const item = $input.first();\nreturn [{ json: item.json, binary: $('Validar y preparar').first().binary }];"
      }
    },
    {
      "id": "sim-s3",
      "name": "Guardar imagen original",
      "type": "n8n-nodes-base.s3",
      "typeVersion": 1,
      "position": [
        1344,
        -96
      ],
      "parameters": {
        "operation": "upload",
        "bucketName": "calidad-lata",
        "fileName": "={{ $('Validar y preparar').first().json.imagen_ref }}",
        "additionalFields": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "credentials": {
        "s3": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "sim-linea",
      "name": "Buscar l\u00ednea",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1568,
        -96
      ],
      "parameters": {
        "operation": "executeQuery",
        "query": "=SELECT linea, equipo, proceso FROM lineas_grupos WHERE chat_id = '{{ $('Validar y preparar').first().json.origen.chat_id }}' LIMIT 1;",
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 2,
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "sim-dup",
      "name": "\u00bfDuplicada por hash?",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1792,
        -96
      ],
      "parameters": {
        "operation": "executeQuery",
        "query": "=SELECT evidence_id AS dup_evidence_id FROM evidencias WHERE imagen_hash = '{{ $('Hash imagen').first().json.imagen_hash }}' AND capturado_en > now() - (interval '1 hour' * COALESCE((SELECT valor::int FROM config WHERE clave='dedup_ventana_horas'), 24)) LIMIT 1;",
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 2,
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "sim-prep",
      "name": "Preparar registro",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2016,
        -96
      ],
      "parameters": {
        "jsCode": "const n = $('Validar y preparar').first().json;\nconst hash = $('Hash imagen').first().json.imagen_hash || null;\nconst lineaRows = $('Buscar l\u00ednea').all().map(i => i.json).filter(j => j && j.linea);\nconst linea = lineaRows.length ? lineaRows[0].linea : n.linea_fallback;\nconst equipo = lineaRows.length ? (lineaRows[0].equipo || '') : '';\nconst proceso = lineaRows.length ? (lineaRows[0].proceso || n.proceso) : n.proceso;\nconst dupRows = $input.all().map(i => i.json).filter(j => j && j.dup_evidence_id);\nconst estado = dupRows.length ? 'duplicada' : 'recibido';\nconst duplicada_de = dupRows.length ? dupRows[0].dup_evidence_id : null;\nreturn [{ json: { ...n, linea, equipo, proceso, imagen_hash: hash, estado, duplicada_de } }];"
      }
    },
    {
      "id": "sim-ins",
      "name": "Registrar evidencia",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        2240,
        -96
      ],
      "parameters": {
        "operation": "executeQuery",
        "query": "=INSERT INTO evidencias (evidence_id, proceso, linea, equipo, capturado_en, imagen_ref, origen, estado, imagen_hash) VALUES ('{{ $json.evidence_id }}', '{{ $json.proceso }}', '{{ ($json.linea || '').replace(/'/g, \"''\") }}', '{{ ($json.equipo || '').replace(/'/g, \"''\") }}', '{{ $json.capturado_en }}'::timestamptz, '{{ $json.imagen_ref }}', '{{ JSON.stringify($json.origen).replace(/'/g, \"''\") }}'::jsonb, '{{ $json.estado }}', '{{ $json.imagen_hash }}');",
        "options": {}
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "sim-enc",
      "name": "\u00bfEncolar?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        2464,
        -160
      ],
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "q1",
              "leftValue": "={{ $('Preparar registro').first().json.estado }}",
              "rightValue": "recibido",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "sim-rmq",
      "name": "Publicar a cola",
      "type": "n8n-nodes-base.rabbitmq",
      "typeVersion": 1.1,
      "position": [
        2688,
        -256
      ],
      "parameters": {
        "queue": "calidad.lata.procesar",
        "sendInputData": false,
        "message": "={{ JSON.stringify({ evidence_id: $('Preparar registro').first().json.evidence_id, proceso: $('Preparar registro').first().json.proceso, linea: $('Preparar registro').first().json.linea, equipo: $('Preparar registro').first().json.equipo, capturado_en: $('Preparar registro').first().json.capturado_en, imagen_ref: $('Preparar registro').first().json.imagen_ref, origen: $('Preparar registro').first().json.origen }) }}",
        "options": {
          "durable": true
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "credentials": {
        "rabbitmq": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "sim-redis",
      "name": "Contador recibidas (m\u00e9trica)",
      "type": "n8n-nodes-base.redis",
      "typeVersion": 1,
      "position": [
        2912,
        -256
      ],
      "parameters": {
        "operation": "incr",
        "key": "={{ 'metrics:recibidas:' + $('Preparar registro').first().json.linea }}"
      },
      "credentials": {
        "redis": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "sim-ok",
      "name": "Responder OK",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        3136,
        -256
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ ok: true, evidence_id: $('Preparar registro').first().json.evidence_id, estado: 'recibido', linea: $('Preparar registro').first().json.linea }) }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "https://sudamericana.opendraft.tech"
              }
            ]
          }
        }
      }
    },
    {
      "id": "sim-dupresp",
      "name": "Responder duplicada",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        2688,
        32
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ ok: true, evidence_id: $('Preparar registro').first().json.evidence_id, estado: 'duplicada', duplicada_de: $('Preparar registro').first().json.duplicada_de }) }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "https://sudamericana.opendraft.tech"
              }
            ]
          }
        }
      }
    },
    {
      "id": "sim-sticky",
      "name": "Nota-simulador",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -112,
        -400
      ],
      "parameters": {
        "content": "### 8) Simulador de ingesta (Casos de prueba del dashboard)\nRecibe una foto en base64 desde la pesta\u00f1a \u00abCasos de prueba\u00bb del dashboard y la mete al pipeline por el MISMO camino que WF1 (hash SHA-256 \u2192 dedup \u2192 MinIO \u2192 Postgres \u2192 RabbitMQ). De ah\u00ed en adelante todo corre real: WF2 visi\u00f3n IA, coherencia, alertas, dashboard.\n\n**\u00bfPor qu\u00e9 no por Telegram?** Los bots NUNCA reciben sus propios mensajes (ni de otros bots) \u2014 enviar por el bot no disparar\u00eda WF1.\n\n**Auth**: token en `config.simulador_token` (body.token). CORS restringido a sudamericana.opendraft.tech.\n\n**Marca**: `origen.via = 'simulador'` en evidencias (jsonb) \u2014 filtrable/borrable sin tocar datos reales de Telegram.\n\nBody esperado: { token, imagen_b64, filename, carpeta, operario, chat_id? }",
        "height": 360,
        "width": 720,
        "color": 6
      }
    }
  ],
  "connections": {
    "Recibir simulaci\u00f3n": {
      "main": [
        [
          {
            "node": "Leer config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Leer config": {
      "main": [
        [
          {
            "node": "Validar y preparar",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validar y preparar": {
      "main": [
        [
          {
            "node": "\u00bfAutorizado?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u00bfAutorizado?": {
      "main": [
        [
          {
            "node": "Hash imagen",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Rechazar",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hash imagen": {
      "main": [
        [
          {
            "node": "Reasignar binario",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reasignar binario": {
      "main": [
        [
          {
            "node": "Guardar imagen original",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Guardar imagen original": {
      "main": [
        [
          {
            "node": "Buscar l\u00ednea",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Buscar l\u00ednea": {
      "main": [
        [
          {
            "node": "\u00bfDuplicada por hash?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u00bfDuplicada por hash?": {
      "main": [
        [
          {
            "node": "Preparar registro",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Preparar registro": {
      "main": [
        [
          {
            "node": "Registrar evidencia",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Registrar evidencia": {
      "main": [
        [
          {
            "node": "\u00bfEncolar?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Responder duplicada",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u00bfEncolar?": {
      "main": [
        [
          {
            "node": "Publicar a cola",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Responder duplicada",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Publicar a cola": {
      "main": [
        [
          {
            "node": "Contador recibidas (m\u00e9trica)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Contador recibidas (m\u00e9trica)": {
      "main": [
        [
          {
            "node": "Responder OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "errorWorkflow": "I59vNrbU4KKGkHXF",
    "callerPolicy": "workflowsFromSameOwner"
  }
}