{
  "name": "ALLSoft — Alerta de inventario bajo",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "triggerAtHour": 7
            }
          ]
        }
      },
      "name": "Todos los días a las 7",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        -620,
        300
      ],
      "id": "1nv3-0001-4000-8000-000000000001"
    },
    {
      "parameters": {
        "jsCode": "// Tu inventario. Sustituye por tu sistema de punto de venta, Sheets o la\n// base de datos.\nconst inventario = [\n  { producto: 'Tinte rubio ceniza',   stock: 2,  minimo: 6,  consumoSemanal: 4,  proveedor: 'Distribuidora Bella' },\n  { producto: 'Shampoo profesional',  stock: 14, minimo: 10, consumoSemanal: 5,  proveedor: 'Distribuidora Bella' },\n  { producto: 'Guantes desechables',  stock: 1,  minimo: 8,  consumoSemanal: 12, proveedor: 'Insumos MedPro' },\n  { producto: 'Toallas descartables', stock: 5,  minimo: 12, consumoSemanal: 9,  proveedor: 'Insumos MedPro' },\n  { producto: 'Esmalte semipermanente', stock: 20, minimo: 8, consumoSemanal: 3, proveedor: 'Distribuidora Bella' },\n];\n\n// TODA la aritmética se hace aquí, no en el modelo. Cuántos días aguanta el\n// stock y cuánto pedir son cuentas, no opiniones: un modelo que las improvise\n// te deja sin producto o con la bodega llena.\nconst SEMANAS_A_CUBRIR = 3;\n\nconst bajos = inventario\n  .filter((p) => p.stock <= p.minimo)\n  .map((p) => {\n    const diasRestantes = p.consumoSemanal > 0\n      ? Math.floor((p.stock / p.consumoSemanal) * 7)\n      : 999;\n    const sugerido = Math.max(\n      p.minimo - p.stock,\n      p.consumoSemanal * SEMANAS_A_CUBRIR - p.stock\n    );\n    return {\n      ...p,\n      diasRestantes,\n      sugerido,\n      urgencia: diasRestantes <= 2 ? 'critico' : diasRestantes <= 7 ? 'pronto' : 'vigilar',\n    };\n  })\n  .sort((a, b) => a.diasRestantes - b.diasRestantes);\n\nif (bajos.length === 0) {\n  return [{ json: { hay: false, cuantos: 0, listado: '', criticos: 0 } }];\n}\n\nconst listado = bajos\n  .map((p) =>\n    `- ${p.producto}: quedan ${p.stock} (minimo ${p.minimo}), alcanza ~${p.diasRestantes} dias, ` +\n    `pedir ${p.sugerido} a ${p.proveedor}`\n  )\n  .join('\\n');\n\nreturn [{\n  json: {\n    hay: true,\n    cuantos: bajos.length,\n    criticos: bajos.filter((p) => p.urgencia === 'critico').length,\n    listado,\n    detalle: bajos,\n  },\n}];\n"
      },
      "name": "Revisar inventario",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -400,
        300
      ],
      "notes": "Calcula días de stock y cuánto pedir. Toda la aritmética vive aquí, no en el modelo.",
      "id": "1nv3-0002-4000-8000-000000000002"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "version": 2
          },
          "conditions": [
            {
              "id": "i1",
              "leftValue": "={{ $json.hay }}",
              "rightValue": "={{ true }}",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "¿Hay algo bajo?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        -180,
        300
      ],
      "notes": "Si todo está bien, no manda correo. Un aviso diario que siempre dice 'todo bien' deja de leerse.",
      "id": "1nv3-0003-4000-8000-000000000003"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.groq.com/openai/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer TU_GROQ_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: 'llama-3.3-70b-versatile', temperature: 0.4, max_tokens: 220, messages: [ { role: 'system', content: 'Eres quien avisa al dueno de un negocio sobre el inventario. Recibes una lista con lo que falta y cuantos dias alcanza. Escribe 2 o 3 frases diciendo que atender primero y por que. Texto plano, sin markdown, sin listas. NO repitas las cantidades: van en una tabla aparte. Maximo 60 palabras.' }, { role: 'user', content: 'Productos por debajo del minimo: ' + $json.listado } ] }) }}",
        "options": {}
      },
      "name": "Redactar la alerta (Groq)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        60,
        200
      ],
      "onError": "continueErrorOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "notes": "Solo explica la urgencia. Las cantidades ya vienen calculadas.",
      "id": "1nv3-0004-4000-8000-000000000004"
    },
    {
      "parameters": {
        "jsCode": "// Arma la alerta de inventario.\n//\n// Las cantidades vienen calculadas del nodo anterior. El modelo solo ordena\n// el mensaje y explica la urgencia con palabras. Si le dejaras calcular\n// cuánto pedir, un día encargarías el doble de lo necesario.\nconst datos = $('Revisar inventario').first().json;\n\nlet texto;\ntry {\n  texto = $input.first().json.choices[0].message.content.trim();\n} catch (e) {\n  texto = 'Revisa el listado de abajo: hay productos por debajo del mínimo.';\n}\n\nconst filas = datos.detalle\n  .map((p) => {\n    const color = p.urgencia === 'critico' ? '#c92a2a'\n      : p.urgencia === 'pronto' ? '#e8590c' : '#5a6b80';\n    return '<tr>' +\n      '<td style=\"padding:9px 8px;border-bottom:1px solid #e6edf5\">' + p.producto + '</td>' +\n      '<td style=\"padding:9px 8px;border-bottom:1px solid #e6edf5;text-align:center\">' + p.stock + ' / ' + p.minimo + '</td>' +\n      '<td style=\"padding:9px 8px;border-bottom:1px solid #e6edf5;text-align:center;color:' + color + ';font-weight:700\">' + p.diasRestantes + ' días</td>' +\n      '<td style=\"padding:9px 8px;border-bottom:1px solid #e6edf5;text-align:center;font-weight:700\">' + p.sugerido + '</td>' +\n      '<td style=\"padding:9px 8px;border-bottom:1px solid #e6edf5;font-size:12px;color:#5a6b80\">' + p.proveedor + '</td>' +\n      '</tr>';\n  })\n  .join('');\n\nconst html =\n  '<div style=\"font-family:system-ui,Segoe UI,Arial,sans-serif;max-width:660px;margin:0 auto;color:#16202e\">' +\n  '<div style=\"background:#0f2a4a;color:#fff;padding:20px 24px;border-radius:12px 12px 0 0\">' +\n  '<div style=\"font-size:12px;letter-spacing:.12em;text-transform:uppercase;opacity:.8\">Inventario</div>' +\n  '<div style=\"font-size:20px;font-weight:700;margin-top:4px\">' +\n  datos.cuantos + ' producto(s) por debajo del mínimo</div>' +\n  '</div>' +\n  '<div style=\"border:1px solid #e6edf5;border-top:none;border-radius:0 0 12px 12px;padding:24px\">' +\n  '<p style=\"margin:0 0 18px;font-size:15px;line-height:1.6\">' + texto + '</p>' +\n  '<table style=\"width:100%;border-collapse:collapse;font-size:13px\">' +\n  '<tr style=\"background:#f4f8fc\">' +\n  '<th style=\"text-align:left;padding:9px 8px\">Producto</th>' +\n  '<th style=\"padding:9px 8px\">Stock/Mín</th>' +\n  '<th style=\"padding:9px 8px\">Alcanza</th>' +\n  '<th style=\"padding:9px 8px\">Pedir</th>' +\n  '<th style=\"text-align:left;padding:9px 8px\">Proveedor</th>' +\n  '</tr>' + filas + '</table>' +\n  '<p style=\"margin:18px 0 0;font-size:12px;color:#8595a8\">' +\n  'Las cantidades sugeridas cubren 3 semanas de consumo. Revísalas antes de ordenar.' +\n  '</p>' +\n  '</div></div>';\n\nreturn [{\n  json: {\n    asunto: (datos.criticos > 0 ? '[URGENTE] ' : '') +\n      'Inventario bajo — ' + datos.cuantos + ' producto(s)',\n    cuantos: datos.cuantos,\n    criticos: datos.criticos,\n    html,\n  },\n}];\n"
      },
      "name": "Armar la alerta",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        300,
        120
      ],
      "notes": "Tabla con stock, días restantes y cuánto pedir, todo calculado.",
      "id": "1nv3-0005-4000-8000-000000000005"
    },
    {
      "parameters": {
        "sendTo": "tu-correo@ejemplo.com",
        "subject": "={{ $json.asunto }}",
        "emailType": "html",
        "message": "={{ $json.html }}",
        "options": {}
      },
      "name": "Enviar la alerta",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        520,
        120
      ],
      "notes": "Tu propio correo: es un aviso interno.",
      "id": "1nv3-0006-4000-8000-000000000006"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "e1",
              "name": "estado",
              "value": "error_ia",
              "type": "string"
            },
            {
              "id": "e2",
              "name": "detalle",
              "value": "={{ $json.error || 'Groq no respondió' }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "name": "Avisar del fallo",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        300,
        320
      ],
      "notes": "Salida de error del nodo de IA: el flujo no se cae, avisa.",
      "id": "1nv3-0007-4000-8000-000000000007"
    }
  ],
  "connections": {
    "Todos los días a las 7": {
      "main": [
        [
          {
            "node": "Revisar inventario",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Revisar inventario": {
      "main": [
        [
          {
            "node": "¿Hay algo bajo?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "¿Hay algo bajo?": {
      "main": [
        [
          {
            "node": "Redactar la alerta (Groq)",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Redactar la alerta (Groq)": {
      "main": [
        [
          {
            "node": "Armar la alerta",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Avisar del fallo",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Armar la alerta": {
      "main": [
        [
          {
            "node": "Enviar la alerta",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "instanceId": "allsoft-academia"
  },
  "tags": []
}
