{
  "name": "ezGPT - HTML Product Cards",
  "active": false,
  "nodes": [
    {
      "id": "trigger",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        240,
        300
      ],
      "parameters": {}
    },
    {
      "id": "sample",
      "name": "Sample HTML",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const html = `<!doctype html>\n<html lang=\"ru\"><body>\n<main class=\"catalog\">\n  <article class=\"product-card\"><a class=\"product-link\" data-id=\"001\" href=\"/products/001\"><span class=\"product-name\">Чай &amp; кофе</span></a><span class=\"product-price\" data-price-minor=\"150000\">1 500 рублей</span></article>\n  <article class=\"product-card\"><a class=\"product-link\" data-id=\"002\" href=\"/products/002\"><span class=\"product-name\">Блокнот</span></a><span class=\"product-price\" data-price-minor=\"0\">0 рублей</span></article>\n  <article class=\"product-card\"><a class=\"product-link\" data-id=\"003\" href=\"/products/003\"><span class=\"product-name\">Лампа</span></a><span class=\"product-price\" data-price-minor=\"49900\">499 рублей</span></article>\n</main>\n</body></html>`;\nreturn [{ json: { html } }];"
      }
    },
    {
      "id": "validate",
      "name": "Check HTML Text",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const items = $input.all();\nif (items.length !== 1) throw new Error('one_html_item_required');\nconst html = items[0].json.html;\nif (typeof html !== 'string' || !html.trim()) throw new Error('html_text_required');\nif (Buffer.byteLength(html, 'utf8') > 16384) throw new Error('html_too_large');\nreturn [{ json: { html } }];"
      }
    },
    {
      "id": "extract-cards",
      "name": "Extract Cards",
      "type": "n8n-nodes-base.html",
      "typeVersion": 1.2,
      "position": [
        900,
        300
      ],
      "parameters": {
        "operation": "extractHtmlContent",
        "sourceData": "json",
        "dataPropertyName": "html",
        "extractionValues": {
          "values": [
            {
              "key": "cards",
              "cssSelector": ".product-card",
              "returnValue": "html",
              "returnArray": true
            }
          ]
        },
        "options": {
          "trimValues": true,
          "cleanUpText": true
        }
      }
    },
    {
      "id": "list",
      "name": "Check Card List",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1120,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const items = $input.all();\nif (items.length !== 1 || !Array.isArray(items[0].json.cards)) {\n  throw new Error('cards_array_required');\n}\nconst cards = items[0].json.cards;\nif (!cards.length) throw new Error('cards_empty');\nif (cards.length > 20) throw new Error('too_many_cards');\nif (cards.some(card => typeof card !== 'string' || !card.trim())) {\n  throw new Error('card_html_required');\n}\nreturn [{ json: { cards } }];"
      }
    },
    {
      "id": "split",
      "name": "Split Cards",
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        1340,
        300
      ],
      "parameters": {
        "fieldToSplitOut": "cards",
        "include": "noOtherFields",
        "options": {
          "destinationFieldName": "card_html"
        }
      }
    },
    {
      "id": "extract-fields",
      "name": "Extract Fields",
      "type": "n8n-nodes-base.html",
      "typeVersion": 1.2,
      "position": [
        1560,
        300
      ],
      "parameters": {
        "operation": "extractHtmlContent",
        "sourceData": "json",
        "dataPropertyName": "card_html",
        "extractionValues": {
          "values": [
            {
              "key": "id",
              "cssSelector": ".product-link",
              "returnValue": "attribute",
              "attribute": "data-id",
              "returnArray": true
            },
            {
              "key": "name",
              "cssSelector": ".product-name",
              "returnValue": "text",
              "returnArray": true
            },
            {
              "key": "price",
              "cssSelector": ".product-price",
              "returnValue": "attribute",
              "attribute": "data-price-minor",
              "returnArray": true
            },
            {
              "key": "href",
              "cssSelector": ".product-link",
              "returnValue": "attribute",
              "attribute": "href",
              "returnArray": true
            }
          ]
        },
        "options": {
          "trimValues": true,
          "cleanUpText": true
        }
      }
    },
    {
      "id": "check",
      "name": "Check Products",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1780,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const items = $input.all();\nconst expectedCount = $('Check Card List').first().json.cards.length;\nif (items.length !== expectedCount) throw new Error('product_count_mismatch');\nconst ids = new Set();\nconst single = (row, key) => {\n  const value = row[key];\n  if (!Array.isArray(value) || value.length !== 1 || typeof value[0] !== 'string' || !value[0].trim()) {\n    throw new Error(key + '_single_value_required');\n  }\n  return value[0];\n};\nconst products = items.map(item => {\n  const row = item.json;\n  if (Object.keys(row).sort().join('|') !== 'href|id|name|price') {\n    throw new Error('product_fields_mismatch');\n  }\n  const id = single(row, 'id');\n  const name = single(row, 'name');\n  const rawPrice = single(row, 'price');\n  const href = single(row, 'href');\n  if (!/^\\d{3}$/.test(id) || ids.has(id)) throw new Error('product_id_invalid_or_duplicate');\n  if (!/^\\d+$/.test(rawPrice) || !Number.isSafeInteger(Number(rawPrice))) {\n    throw new Error('price_minor_invalid');\n  }\n  if (href !== '/products/' + id) throw new Error('product_href_mismatch');\n  ids.add(id);\n  return { product_id: id, name, price_minor: Number(rawPrice), href };\n});\nreturn [{ json: {\n  products,\n  count: products.length,\n  action_performed: false,\n} }];"
      }
    }
  ],
  "connections": {
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Sample HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sample HTML": {
      "main": [
        [
          {
            "node": "Check HTML Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check HTML Text": {
      "main": [
        [
          {
            "node": "Extract Cards",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Cards": {
      "main": [
        [
          {
            "node": "Check Card List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Card List": {
      "main": [
        [
          {
            "node": "Split Cards",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Cards": {
      "main": [
        [
          {
            "node": "Extract Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Fields": {
      "main": [
        [
          {
            "node": "Check Products",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveDataSuccessExecution": "all",
    "saveDataErrorExecution": "all"
  },
  "pinData": {}
}
