{
  "name": "BaseLinker AI Listing Automation",
  "nodes": [
    {
      "id": "webhook-main",
      "name": "Webhook - Odbierz Produkty",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -400,
        200
      ],
      "parameters": {
        "path": "baselinker-ai",
        "httpMethod": "POST",
        "responseMode": "responseNode",
        "options": {}
      }
    },
    {
      "id": "validate-input",
      "name": "Waliduj Input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -200,
        200
      ],
      "parameters": {
        "jsCode": "// Walidacja wej\u015bciowych danych z frontendu\nconst body = $input.first().json.body || $input.first().json;\nconst products = body.products;\n\nif (!products || !Array.isArray(products) || products.length === 0) {\n  throw new Error('BRAK_PRODUKTOW: Payload musi zawiera\u0107 tablic\u0119 \"products\" z co najmniej jednym produktem.');\n}\n\nfor (const [i, p] of products.entries()) {\n  if (!p.SKU_Szablonu) throw new Error(`BRAK_SKU: Produkt #${i+1} nie ma pola SKU_Szablonu.`);\n  if (!p.Zdjecie_Szukaj) throw new Error(`BRAK_ZDJECIA: Produkt #${i+1} nie ma pola Zdjecie_Szukaj.`);\n  if (!p.images || p.images.length === 0) throw new Error(`BRAK_OBRAZOW: Produkt #${i+1} (${p.SKU_Szablonu}) nie ma \u017cadnych zdj\u0119\u0107.`);\n  if (!p.Stare_Slowo || !p.Nowe_Slowo) throw new Error(`BRAK_SLOW: Produkt #${i+1} (${p.SKU_Szablonu}) nie ma Stare_Slowo / Nowe_Slowo.`);\n}\n\nreturn products.map(p => ({ json: p }));"
      }
    },
    {
      "id": "loop-products",
      "name": "Petla Produktow",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        0,
        200
      ],
      "parameters": {
        "batchSize": 1,
        "options": {}
      }
    },
    {
      "id": "set-config",
      "name": "Konfiguracja API",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3,
      "position": [
        200,
        200
      ],
      "parameters": {
        "mode": "manual",
        "assignments": {
          "assignments": [
            {
              "id": "cfg-bl-token",
              "name": "BL_TOKEN",
              "value": "={{ $env.BASELINKER_TOKEN }}",
              "type": "string"
            },
            {
              "id": "cfg-inv-id",
              "name": "INVENTORY_ID",
              "value": "={{ $env.BASELINKER_INVENTORY_ID }}",
              "type": "string"
            },
            {
              "id": "cfg-gemini",
              "name": "GEMINI_KEY",
              "value": "={{ $env.GEMINI_API_KEY }}",
              "type": "string"
            },
            {
              "id": "cfg-price-group",
              "name": "PRICE_GROUP_ID",
              "value": "={{ $env.BASELINKER_PRICE_GROUP_ID }}",
              "type": "string"
            },
            {
              "id": "cfg-warehouse",
              "name": "WAREHOUSE_ID",
              "value": "={{ $env.BASELINKER_WAREHOUSE_ID }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true
      }
    },
    {
      "id": "extract-prefix",
      "name": "Oblicz Prefix SKU",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        400,
        200
      ],
      "parameters": {
        "jsCode": "const item = $input.item.json;\nconst sku = item.SKU_Szablonu || '';\n\n// Wyci\u0105gamy prefix literowy np. 'DN-' z 'DN-0101'\nconst match = sku.match(/^([A-Za-z]+-?)/);\nconst prefix = match ? match[1] : sku.substring(0, 3);\n\nreturn {\n  json: {\n    ...item,\n    _prefix: prefix,\n    _templateSku: sku\n  }\n};"
      }
    },
    {
      "id": "bl-list-by-prefix",
      "name": "BL: Pobierz produkty wg prefixu",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [
        600,
        200
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.baselinker.com/connector.php",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-BLToken",
              "value": "={{ $('Konfiguracja API').item.json.BL_TOKEN }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "form-urlencoded",
        "bodyParameters": {
          "parameters": [
            {
              "name": "method",
              "value": "getInventoryProductsList"
            },
            {
              "name": "parameters",
              "value": "={{ JSON.stringify({ inventory_id: $('Konfiguracja API').item.json.INVENTORY_ID, filter_sku: $('Oblicz Prefix SKU').item.json._prefix, page: 1 }) }}"
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "find-template-and-next-sku",
      "name": "Znajdz Szablon i Oblicz Nastepne SKU",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        800,
        200
      ],
      "parameters": {
        "jsCode": "const blResp = $input.item.json;\nconst prefix = $('Oblicz Prefix SKU').item.json._prefix;\nconst templateSku = $('Oblicz Prefix SKU').item.json._templateSku;\nconst products = blResp.products || {};\n\nif (blResp.status !== 'SUCCESS' && !blResp.products) {\n  throw new Error(`BL_ERROR: getInventoryProductsList zwr\u00f3ci\u0142 b\u0142\u0105d: ${JSON.stringify(blResp)}`);\n}\n\n// Szukamy product_id dla template SKU (np. DN-0101)\nlet templateProductId = null;\nlet maxNum = 0;\n\nfor (const [pid, pdata] of Object.entries(products)) {\n  const sku = pdata.sku || '';\n  if (sku === templateSku) {\n    templateProductId = pid;\n  }\n  // Liczymy MAX numer dla tego prefixu\n  const numStr = sku.replace(prefix, '');\n  const num = parseInt(numStr, 10);\n  if (!isNaN(num) && num > maxNum) maxNum = num;\n}\n\nif (!templateProductId) {\n  throw new Error(`BRAK_SZABLONU: Nie znaleziono produktu o SKU '${templateSku}' w BaseLinker. Sprawd\u017a czy SKU_Szablonu w CSV jest poprawny.`);\n}\n\nconst nextNum = maxNum + 1;\nconst newSku = prefix + nextNum.toString().padStart(4, '0');\n\nreturn {\n  json: {\n    ...$('Oblicz Prefix SKU').item.json,\n    _templateProductId: templateProductId,\n    _newSku: newSku,\n    _maxFound: maxNum\n  }\n};"
      }
    },
    {
      "id": "bl-get-template-data",
      "name": "BL: Pobierz Dane Szablonu",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [
        1000,
        200
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.baselinker.com/connector.php",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-BLToken",
              "value": "={{ $('Konfiguracja API').item.json.BL_TOKEN }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "form-urlencoded",
        "bodyParameters": {
          "parameters": [
            {
              "name": "method",
              "value": "getInventoryProductsData"
            },
            {
              "name": "parameters",
              "value": "={{ JSON.stringify({ inventory_id: $('Konfiguracja API').item.json.INVENTORY_ID, products: [ $('Znajdz Szablon i Oblicz Nastepne SKU').item.json._templateProductId ] }) }}"
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "prep-gemini",
      "name": "Przygotuj Prompt Gemini",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        200
      ],
      "parameters": {
        "jsCode": "const blData = $input.item.json;\nconst ctx = $('Znajdz Szablon i Oblicz Nastepne SKU').item.json;\nconst templateId = ctx._templateProductId;\n\nif (blData.status !== 'SUCCESS') {\n  throw new Error(`BL_TEMPLATE_ERROR: ${JSON.stringify(blData)}`);\n}\n\nconst template = blData.products[templateId];\nif (!template) {\n  throw new Error(`BRAK_DANYCH_SZABLONU: Nie ma danych dla product_id=${templateId}`);\n}\n\nconst templateName = template.text_fields?.name || template.name || '';\nconst templateDesc = template.text_fields?.description || template.description || '';\nconst templateDesc1 = template.text_fields?.description_extra1 || '';\nconst templateDesc2 = template.text_fields?.description_extra2 || '';\nconst templateDesc3 = template.text_fields?.description_extra3 || '';\nconst templateDesc4 = template.text_fields?.description_extra4 || '';\n\nconst stareSlowo = ctx.Stare_Slowo || '';\nconst noweSlowo = ctx.Nowe_Slowo || '';\nconst audience = ctx.Audience || 'unknown';\nconst firstImageB64 = ctx.images[0];\n\nconst prompt = `Jeste\u015b copywriterem SEO dla e-commerce (Allegro/marketplace PL). Twoje zadanie to przepisanie karty produktu naklejek \u015bciennych.\n\nSZABLON TYTU\u0141U: ${templateName}\nSZABLON OPISU (HTML): ${templateDesc}\n\nSTARE S\u0141OWO (do usuni\u0119cia): ${stareSlowo}\nNOWE S\u0141OWO (do wprowadzenia): ${noweSlowo}\nP\u0141E\u0106 odbiorcy (z pliku): ${audience} \u2014 je\u015bli 'unknown', wywnioskuj ze zdj\u0119cia.\n\nZASADY:\n1. Zachowaj DOK\u0141ADNIE t\u0119 sam\u0105 struktur\u0119 HTML \u2014 nie zmieniaj tag\u00f3w, klas, sekcji.\n2. Zamie\u0144 KA\u017bD\u0104 form\u0119 gramatyczn\u0105 starego s\u0142owa na odpowiedni\u0105 form\u0119 nowego s\u0142owa (odmiana przez przypadki PL!).\n3. W tytule: zachowaj wzorzec 'Naklejki na \u015acian\u0119 dla Dzieci [Ch\u0142opca/Dziewczynki] \u015acienne Samoprzylepne Naklejka [Nowe Slowo]'.\n4. W opisie: zamie\u0144 'z ${stareSlowo.toLowerCase()}ami', 'z ${stareSlowo.toLowerCase()}iem' itp. na poprawn\u0105 form\u0119 '${noweSlowo}'.\n5. Dopasuj zwroty dla/ch\u0142opca/dziewczynki je\u015bli audience to nie 'unknown'.\n6. NIE zmieniaj parametr\u00f3w technicznych (wymiary, materia\u0142, itp.).\n7. Dla description_extra1-4: przepisz analogicznie zachowuj\u0105c HTML.\n\nODPOWIED\u0179: Tylko czysty JSON (bez markdown, bez ```json):\n{\"title\": \"...\", \"description\": \"...\", \"description_extra1\": \"...\", \"description_extra2\": \"...\", \"description_extra3\": \"...\", \"description_extra4\": \"...\"}`;\n\nreturn {\n  json: {\n    ...ctx,\n    _templateWeight: template.weight || 2,\n    _templateTaxRate: template.tax_rate || 23,\n    _templateCategoryId: template.category_id || '',\n    _templatePrice: template.prices ? Object.values(template.prices)[0] : 47.99,\n    _templateStock: template.stock ? Object.values(template.stock)[0] : 100,\n    _templateDesc1: templateDesc1,\n    _templateDesc2: templateDesc2,\n    _templateDesc3: templateDesc3,\n    _templateDesc4: templateDesc4,\n    _geminiPrompt: prompt,\n    _firstImageB64: firstImageB64\n  }\n};"
      }
    },
    {
      "id": "gemini-vision",
      "name": "Gemini Vision - Generuj Tytul i Opis",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [
        1400,
        200
      ],
      "parameters": {
        "method": "POST",
        "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "x-goog-api-key",
              "value": "={{ $('Konfiguracja API').item.json.GEMINI_KEY }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ contents: [{ parts: [{ text: $('Przygotuj Prompt Gemini').item.json._geminiPrompt }, { inline_data: { mime_type: 'image/jpeg', data: $('Przygotuj Prompt Gemini').item.json._firstImageB64 } }] }], generationConfig: { temperature: 0.3, maxOutputTokens: 4096 } }) }}",
        "options": {}
      }
    },
    {
      "id": "parse-gemini-response",
      "name": "Parsuj Odpowiedz Gemini",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1600,
        200
      ],
      "parameters": {
        "jsCode": "const geminiResp = $input.item.json;\nconst ctx = $('Przygotuj Prompt Gemini').item.json;\n\nif (!geminiResp.candidates || geminiResp.candidates.length === 0) {\n  throw new Error(`GEMINI_ERROR: Brak odpowiedzi od Gemini. ${JSON.stringify(geminiResp)}`);\n}\n\nlet text = geminiResp.candidates[0]?.content?.parts?.[0]?.text || '';\n\n// Wyczy\u015b\u0107 potencjalny markdown\ntext = text.replace(/```json\\s*/gi, '').replace(/```\\s*/g, '').trim();\n\nlet parsed;\ntry {\n  parsed = JSON.parse(text);\n} catch(e) {\n  // Spr\u00f3buj wyci\u0105gn\u0105\u0107 JSON z tekstu\n  const match = text.match(/\\{[\\s\\S]*\\}/);\n  if (match) {\n    parsed = JSON.parse(match[0]);\n  } else {\n    throw new Error(`GEMINI_PARSE_ERROR: Nie mo\u017cna sparsowa\u0107 JSON z Gemini. Odpowied\u017a: ${text.substring(0, 500)}`);\n  }\n}\n\nif (!parsed.title || !parsed.description) {\n  throw new Error(`GEMINI_INCOMPLETE: Brak title lub description w odpowiedzi Gemini.`);\n}\n\nreturn {\n  json: {\n    ...ctx,\n    _aiTitle: parsed.title,\n    _aiDesc: parsed.description,\n    _aiDesc1: parsed.description_extra1 || ctx._templateDesc1,\n    _aiDesc2: parsed.description_extra2 || ctx._templateDesc2,\n    _aiDesc3: parsed.description_extra3 || ctx._templateDesc3,\n    _aiDesc4: parsed.description_extra4 || ctx._templateDesc4\n  }\n};"
      }
    },
    {
      "id": "bl-add-product",
      "name": "BL: Dodaj Produkt",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [
        1800,
        200
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.baselinker.com/connector.php",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-BLToken",
              "value": "={{ $('Konfiguracja API').item.json.BL_TOKEN }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "form-urlencoded",
        "bodyParameters": {
          "parameters": [
            {
              "name": "method",
              "value": "addInventoryProduct"
            },
            {
              "name": "parameters",
              "value": "={{ JSON.stringify({\n  inventory_id: $('Konfiguracja API').item.json.INVENTORY_ID,\n  sku: $('Parsuj Odpowiedz Gemini').item.json._newSku,\n  ean: '',\n  weight: $('Parsuj Odpowiedz Gemini').item.json._templateWeight,\n  tax_rate: $('Parsuj Odpowiedz Gemini').item.json._templateTaxRate,\n  category_id: $('Parsuj Odpowiedz Gemini').item.json._templateCategoryId,\n  text_fields: {\n    name: $('Parsuj Odpowiedz Gemini').item.json._aiTitle,\n    description: $('Parsuj Odpowiedz Gemini').item.json._aiDesc,\n    description_extra1: $('Parsuj Odpowiedz Gemini').item.json._aiDesc1,\n    description_extra2: $('Parsuj Odpowiedz Gemini').item.json._aiDesc2,\n    description_extra3: $('Parsuj Odpowiedz Gemini').item.json._aiDesc3,\n    description_extra4: $('Parsuj Odpowiedz Gemini').item.json._aiDesc4\n  },\n  prices: { [String($('Konfiguracja API').item.json.PRICE_GROUP_ID)]: Number($('Parsuj Odpowiedz Gemini').item.json.Cena) || Number($('Parsuj Odpowiedz Gemini').item.json._templatePrice) || 47.99 },\n  stock: { [String($('Konfiguracja API').item.json.WAREHOUSE_ID)]: Number($('Parsuj Odpowiedz Gemini').item.json.Ilosc) || Number($('Parsuj Odpowiedz Gemini').item.json._templateStock) || 100 },\n  images: $('Parsuj Odpowiedz Gemini').item.json.images.map(b64 => 'data:image/jpeg;base64,' + b64)\n}) }}"
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "check-product-added",
      "name": "Sprawdz Wynik Dodania",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2000,
        200
      ],
      "parameters": {
        "jsCode": "const blResp = $input.item.json;\nconst ctx = $('Parsuj Odpowiedz Gemini').item.json;\n\nif (blResp.status !== 'SUCCESS' || !blResp.product_id) {\n  throw new Error(`BL_ADD_ERROR: Nie uda\u0142o si\u0119 doda\u0107 produktu '${ctx._newSku}'. B\u0142\u0105d BaseLinker: ${JSON.stringify(blResp)}`);\n}\n\nconst newProductId = blResp.product_id;\n\nreturn {\n  json: {\n    _newSku: ctx._newSku,\n    _newProductId: newProductId,\n    _aiTitle: ctx._aiTitle,\n    _audience: ctx.Audience,\n    _imagesCount: ctx.images.length,\n    status: 'SUCCESS',\n    message: `Produkt '${ctx._newSku}' (ID: ${newProductId}) dodany pomy\u015blnie z ${ctx.images.length} zdj\u0119ciami. Tytu\u0142: ${ctx._aiTitle}`\n  }\n};"
      }
    },
    {
      "id": "rate-limit-wait",
      "name": "Poczekaj 2s (Rate Limit)",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1,
      "position": [
        2200,
        200
      ],
      "parameters": {
        "amount": 2,
        "unit": "seconds"
      }
    },
    {
      "id": "collect-results",
      "name": "Zbierz Wyniki",
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        2400,
        200
      ],
      "parameters": {
        "aggregate": "aggregateAllItemData",
        "options": {}
      }
    },
    {
      "id": "respond-success",
      "name": "Odpowiedz Sukces",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        2600,
        200
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: true, processed: $input.all().length, results: $input.all().map(i => i.json) }) }}",
        "options": {
          "responseCode": 200
        }
      }
    },
    {
      "id": "error-handler",
      "name": "Odpowiedz Blad",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        600,
        450
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: false, error: $input.first().json.message || 'Nieznany b\u0142\u0105d', step: $input.first().json.node || 'workflow' }) }}",
        "options": {
          "responseCode": 400
        }
      }
    }
  ],
  "connections": {
    "Webhook - Odbierz Produkty": {
      "main": [
        [
          {
            "node": "Waliduj Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Waliduj Input": {
      "main": [
        [
          {
            "node": "Petla Produktow",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Petla Produktow": {
      "main": [
        [
          {
            "node": "Konfiguracja API",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Zbierz Wyniki",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Konfiguracja API": {
      "main": [
        [
          {
            "node": "Oblicz Prefix SKU",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Oblicz Prefix SKU": {
      "main": [
        [
          {
            "node": "BL: Pobierz produkty wg prefixu",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "BL: Pobierz produkty wg prefixu": {
      "main": [
        [
          {
            "node": "Znajdz Szablon i Oblicz Nastepne SKU",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Znajdz Szablon i Oblicz Nastepne SKU": {
      "main": [
        [
          {
            "node": "BL: Pobierz Dane Szablonu",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "BL: Pobierz Dane Szablonu": {
      "main": [
        [
          {
            "node": "Przygotuj Prompt Gemini",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Przygotuj Prompt Gemini": {
      "main": [
        [
          {
            "node": "Gemini Vision - Generuj Tytul i Opis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini Vision - Generuj Tytul i Opis": {
      "main": [
        [
          {
            "node": "Parsuj Odpowiedz Gemini",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parsuj Odpowiedz Gemini": {
      "main": [
        [
          {
            "node": "BL: Dodaj Produkt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "BL: Dodaj Produkt": {
      "main": [
        [
          {
            "node": "Sprawdz Wynik Dodania",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sprawdz Wynik Dodania": {
      "main": [
        [
          {
            "node": "Poczekaj 2s (Rate Limit)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Poczekaj 2s (Rate Limit)": {
      "main": [
        [
          {
            "node": "Petla Produktow",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Zbierz Wyniki": {
      "main": [
        [
          {
            "node": "Odpowiedz Sukces",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "errorWorkflow": "",
    "saveManualExecutions": true,
    "callerPolicy": "workflowsFromAList"
  }
}