# workflow_versions

workflow_id: HMS43ySZOcEfGSyX
workflow_name: BaseLinker to Synerise CSV Export - Complete MVP
trigger: partial_update
created_at: 2025-11-14 15:47:04
workflow_snapshot:
  {"updatedAt":"2025-11-14T15:41:47.621Z","createdAt":"2025-09-25T14:46:56.372Z","id":"HMS43ySZOcEfGSyX","name":"BaseLinker to Synerise CSV Export - Complete MVP","active":false,"isArchived":false,"nodes":[{"parameters":{"rule":{"interval":[{"field":"minutes","minutesInterval":15}]}},"id":"schedule_trigger","name":"Schedule Trigger - Every 15 min","type":"n8n-nodes-base.scheduleTrigger","position":[-1264,-272],"typeVersion":1.2},{"parameters":{},"id":"manual_trigger","name":"Manual Trigger","type":"n8n-nodes-base.manualTrigger","position":[-1264,-80],"typeVersion":1},{"parameters":{"jsCode":"// Transform BaseLinker orders to Synerise CSV format\n// Mode: Run Once for All Items\n// FIXED: orderId now uses shop_order_id (short format) instead of order_id\n// FIXED: recordedAt now uses Europe/Warsaw timezone (+2h) instead of UTC\n\nconst items = $input.all();\nconst results = [];\n\nconsole.log(`Processing ${items.length} orders for Synerise export`);\n\n// Map status using the enriched status_name\nconst statusMapping = {\n  'ZREALIZOWANE': 'completed',\n  'ANULOWANE': 'cancelled',\n  'ZWROT': 'returned',\n  'WYSŁANE': 'shipped'\n};\n\n// Process each order\nitems.forEach((item, index) => {\n  try {\n    const order = item.json;\n    \n    // Generate eventSalt\n    const eventSalt = `${order.order_id}_${order.date_add || Date.now()}`;\n    \n    // BUGFIX #2: Convert Unix timestamp to Europe/Warsaw timezone\n    // Add 2 hours offset for Europe/Warsaw (UTC+2 in summer, UTC+1 in winter)\n    let recordedAt;\n    if (order.date_confirmed) {\n      const timestamp = parseInt(order.date_confirmed);\n      const utcDate = new Date(timestamp * 1000);\n      // Add 2 hours (7200000 milliseconds)\n      const warsawDate = new Date(utcDate.getTime() + (2 * 60 * 60 * 1000));\n      recordedAt = warsawDate.toISOString();\n      \n      // Debug log for first order\n      if (index === 0) {\n        console.log(`Timezone fix example:`);\n        console.log(`  Original UTC: ${utcDate.toISOString()}`);\n        console.log(`  Warsaw (+2h): ${recordedAt}`);\n      }\n    } else {\n      recordedAt = new Date().toISOString();\n    }\n    \n    // Find appropriate status based on name\n    let orderStatus = 'unknown';\n    const statusNameUpper = (order.status_name || '').toUpperCase();\n    for (const [key, value] of Object.entries(statusMapping)) {\n      if (statusNameUpper.includes(key)) {\n        orderStatus = value;\n        break;\n      }\n    }\n    \n    // Determine payment type\n    const paymentType = order.payment_method_cod === '1' ? 'Pobranie' : 'Przelew';\n    \n    // Safe parseFloat\n    const safeParseFloat = (value) => {\n      const parsed = parseFloat(value);\n      return isNaN(parsed) ? 0 : parsed;\n    };\n    \n    // Extract client name with fallbacks\n    const clientName = order.delivery_fullname || \n                      order.invoice_fullname || \n                      order.delivery_company ||\n                      `${order.delivery_name || ''} ${order.delivery_surname || ''}`.trim() ||\n                      'Unknown Customer';\n    \n    // BUGFIX #1: Use shop_order_id (short format) instead of order_id\n    // shop_order_id examples: 184031, 184042 (human-readable short numbers)\n    // order_id examples: 790964970, 790932859 (internal BaseLinker IDs)\n    const orderId = order.shop_order_id || order.order_id; // Fallback to order_id if shop_order_id is missing\n    \n    // Debug log for first order to verify fix\n    if (index === 0) {\n      console.log(`Order ID fix example:`);\n      console.log(`  OLD (order_id): ${order.order_id}`);\n      console.log(`  NEW (shop_order_id): ${order.shop_order_id}`);\n      console.log(`  Using: ${orderId}`);\n    }\n    \n    // Add transformed order to results\n    results.push({\n      json: {\n        eventSalt: eventSalt,\n        orderId: orderId, // FIXED: Now uses shop_order_id\n        recordedAt: recordedAt, // FIXED: Now uses Europe/Warsaw timezone\n        clientEmail: order.email || '',\n        clientName: clientName,\n        clientPhone: order.phone || '',\n        status: orderStatus,\n        statusName: order.status_name || '',\n        totalAmount: safeParseFloat(order.payment_done),\n        currency: order.currency || 'PLN',\n        paymentMethod: order.payment_method || '',\n        paymentType: paymentType,\n        deliveryMethod: order.delivery_method || '',\n        deliveryPrice: safeParseFloat(order.delivery_price),\n        products: order.products || [],\n        source: order.order_source || 'BaseLinker',\n        notes: order.admin_comments || ''\n      }\n    });\n    \n  } catch (error) {\n    console.error(`Error processing order at index ${index}:`, error);\n    console.error(`Order data:`, JSON.stringify(item.json).substring(0, 500));\n    \n    // Add failed order with error info\n    results.push({\n      json: {\n        error: true,\n        errorMessage: error.message,\n        orderId: item.json?.order_id || 'UNKNOWN',\n        originalData: item.json\n      }\n    });\n  }\n});\n\nconsole.log(`Successfully transformed ${results.length} orders`);\nconsole.log(`Sample order IDs: ${results.slice(0, 5).map(r => r.json.orderId).join(', ')}`);\n\n// Return all transformed items\nreturn results;"},"id":"transform_data","name":"Transform to Synerise Format","type":"n8n-nodes-base.code","position":[2768,-440],"typeVersion":2},{"parameters":{"jsCode":"// Flatten nested product data for CSV\n// UPDATED: 16 columns (removed: eventSalt, clientName, clientPhone, productCategory, productEan, source, notes)\nconst items = $input.all();\nconst csvRows = [];\n\nitems.forEach(item => {\n  const order = item.json;\n  \n  // If order has products, create a row for each product\n  if (order.products && order.products.length > 0) {\n    order.products.forEach(product => {\n      csvRows.push({\n        orderId: order.orderId,\n        recordedAt: order.recordedAt,\n        clientEmail: order.clientEmail,\n        status: order.status,\n        statusName: order.statusName,\n        totalAmount: order.totalAmount,\n        currency: order.currency,\n        productSku: product.sku || product.product_id,\n        productName: product.name,\n        productBrand: product.brand || 'Unknown',\n        productPrice: product.price_brutto,\n        productQuantity: product.quantity,\n        paymentMethod: order.paymentMethod,\n        paymentType: order.paymentType,\n        deliveryMethod: order.deliveryMethod,\n        deliveryPrice: order.deliveryPrice\n      });\n    });\n  } else {\n    // Order without products\n    csvRows.push({\n      orderId: order.orderId,\n      recordedAt: order.recordedAt,\n      clientEmail: order.clientEmail,\n      status: order.status,\n      statusName: order.statusName,\n      totalAmount: order.totalAmount,\n      currency: order.currency,\n      productSku: '',\n      productName: '',\n      productBrand: '',\n      productPrice: '',\n      productQuantity: '',\n      paymentMethod: order.paymentMethod,\n      paymentType: order.paymentType,\n      deliveryMethod: order.deliveryMethod,\n      deliveryPrice: order.deliveryPrice\n    });\n  }\n});\n\n// Generate filename with timestamp\nconst now = new Date();\nconst filename = `synerise_export_${now.toISOString().slice(0,10)}_${now.toTimeString().slice(0,5).replace(':','')}.csv`;\n\nreturn [{\n  json: {\n    filename: filename,\n    rowCount: csvRows.length,\n    exportTime: now.toISOString(),\n    data: csvRows\n  }\n}];"},"id":"prepare_csv","name":"Prepare CSV Data","type":"n8n-nodes-base.code","position":[2992,-440],"typeVersion":2},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getOrders"},{"name":"parameters","value":"={{ JSON.stringify({ order_id: parseInt($json.json.orderId), get_unconfirmed_orders: false }) }}"}]},"options":{"timeout":30000}},"id":"get_orders_batch","name":"Get Orders by IDs","type":"n8n-nodes-base.httpRequest","position":[1200,-32],"typeVersion":4.2,"retryOnFail":true,"maxTries":5,"waitBetweenTries":5000},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":1},"conditions":[{"leftValue":"={{ $json.logs?.length }}","rightValue":0,"operator":{"type":"number","operation":"gt"},"id":"fb5b9b41-36c6-408f-a7ce-41f8c97e3146"}],"combinator":"and"},"options":{}},"id":"check_changes","name":"Check If Changes Exist","type":"n8n-nodes-base.if","position":[304,-176],"typeVersion":2},{"parameters":{},"id":"no_changes","name":"No Changes - Stop","type":"n8n-nodes-base.noOp","position":[528,112],"typeVersion":1},{"parameters":{"jsCode":"// Extract Product IDs - WERSJA Z JEDNYM OUTPUTEM I RÓŻNYMI TYPAMI\nconst items = $input.all();\nconst allProductIds = new Set();\nconst allOrders = [];\n\n// Aggregate node zwraca wszystko w polu \"data\"\nconst aggregatedData = items[0]?.json?.data;\n\nif (aggregatedData && Array.isArray(aggregatedData)) {\n  // Sprawdź strukturę danych z Aggregate\n  aggregatedData.forEach(item => {\n    // Jeśli każdy item ma pole \"orders\" (batche z BaseLinker)\n    if (item.orders && Array.isArray(item.orders)) {\n      allOrders.push(...item.orders);\n    } \n    // Jeśli to bezpośrednio zamówienia (spłaszczone)\n    else if (item.order_id) {\n      allOrders.push(item);\n    }\n    // Jeśli to zagnieżdżone w json\n    else if (item.json?.orders) {\n      allOrders.push(...item.json.orders);\n    }\n    else if (item.json?.order_id) {\n      allOrders.push(item.json);\n    }\n  });\n} \n// Fallback - może dane są bezpośrednio w items\nelse {\n  items.forEach(item => {\n    if (item.json?.orders) {\n      allOrders.push(...item.json.orders);\n    } else if (item.json?.order_id) {\n      allOrders.push(item.json);\n    }\n  });\n}\n\n// Zbierz wszystkie unikalne product IDs\nallOrders.forEach(order => {\n  if (order.products && Array.isArray(order.products)) {\n    order.products.forEach(product => {\n      if (product.product_id) {\n        allProductIds.add(parseInt(product.product_id));\n      }\n    });\n  }\n});\n\nconst productIdsArray = Array.from(allProductIds);\n\n// Podziel produkty na batche po 100\nconst BATCH_SIZE = 100;\nconst productBatches = [];\nfor (let i = 0; i < productIdsArray.length; i += BATCH_SIZE) {\n  productBatches.push(productIdsArray.slice(i, i + BATCH_SIZE));\n}\n\nconsole.log('=== EXTRACT PRODUCT IDS ===');\nconsole.log(`Total orders: ${allOrders.length}`);\nconsole.log(`Unique products: ${productIdsArray.length}`);\nconsole.log(`Product batches: ${productBatches.length} (${BATCH_SIZE} per batch)`);\nproductBatches.forEach((batch, index) => {\n  console.log(`  Batch ${index + 1}: ${batch.length} products`);\n});\n\n// Przygotuj output items\nconst outputItems = [];\n\n// PIERWSZY ITEM: Zamówienia (dla Merge)\noutputItems.push({\n  json: {\n    type: 'ordersData',  // Unikalny typ dla Switch\n    orders: allOrders,\n    stats: {\n      batchOrderCount: allOrders.length,\n      uniqueProductCount: allProductIds.size,\n      totalBatches: productBatches.length,\n      avgProductsPerOrder: (allOrders.reduce((sum, o) => sum + (o.products?.length || 0), 0) / allOrders.length).toFixed(2)\n    }\n  }\n});\n\n// KOLEJNE ITEMY: Każdy batch produktów (dla Loop/Get Product Brands)\nproductBatches.forEach((batch, index) => {\n  outputItems.push({\n    json: {\n      type: 'productBatch',  // Unikalny typ dla Switch\n      batchIndex: index + 1,\n      totalBatches: productBatches.length,\n      productIdsToEnrich: batch,\n      batchSize: batch.length\n    }\n  });\n});\n\nconsole.log(`Returning ${outputItems.length} items: 1 ordersData + ${productBatches.length} productBatch(es)`);\n\nreturn outputItems;"},"id":"extract_product_ids","name":"Extract Product IDs","type":"n8n-nodes-base.code","typeVersion":2,"position":[1200,-272]},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getInventoryProductsData"},{"name":"parameters","value":"={{ JSON.stringify({ \n  inventory_id: 758, \n  products: $json.productIdsToEnrich || [] \n}) }}"}]},"options":{"timeout":30000}},"id":"get_product_brands","name":"Get Product Brands","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1872,-56],"retryOnFail":true},{"parameters":{"jsCode":"// FIXED: Merge Product Data - Correctly extracts data from Merge node\n// This version properly handles the 4 inputs from the Merge node\n\nconst allInputs = $input.all();\nconsole.log(`=== MERGE PRODUCT DATA - DEBUGGING ===`);\nconsole.log(`Total inputs received: ${allInputs.length}`);\n\n// Initialize data storage\nlet manufacturersData = [];\nlet ordersData = [];\nlet productsData = {};\nlet statusesData = [];\n\n// The Merge node combines 4 inputs:\n// Input 0: Product Manufacturers\n// Input 1: Orders (from Switch - ordersData branch)  \n// Input 2: Aggregated Products\n// Input 3: Order Status List\n\n// Process each input and extract the correct data\nallInputs.forEach((item, index) => {\n  const data = item.json;\n  \n  console.log(`\\nInput ${index} analysis:`, {\n    hasManufacturers: !!(data.manufacturers || data.manufacturer),\n    hasOrders: !!(data.orders || data.type === 'ordersData'),\n    hasProducts: !!(data.products || data.data),\n    hasStatuses: !!(data.statuses || data.status),\n    topKeys: Object.keys(data).slice(0, 5),\n    dataType: data.type || 'unknown'\n  });\n  \n  // Extract manufacturers (usually first input from Get Product Manufacturers)\n  if (data.manufacturers && Array.isArray(data.manufacturers)) {\n    manufacturersData = data.manufacturers;\n    console.log(`✓ Found ${manufacturersData.length} manufacturers`);\n  }\n  \n  // Extract orders (from ordersData type)\n  if (data.type === 'ordersData' && data.orders) {\n    ordersData = data.orders;\n    console.log(`✓ Found ${ordersData.length} orders from ordersData`);\n  } else if (data.orders && Array.isArray(data.orders)) {\n    ordersData = data.orders;\n    console.log(`✓ Found ${ordersData.length} orders`);\n  }\n  \n  // Extract products from aggregated data\n  if (data.data && Array.isArray(data.data)) {\n    // This is the aggregated products output\n    console.log(`✓ Found aggregated data with ${data.data.length} items`);\n    data.data.forEach(item => {\n      if (item.products && typeof item.products === 'object') {\n        Object.assign(productsData, item.products);\n      }\n    });\n  } else if (data.products && typeof data.products === 'object') {\n    // Direct products object\n    Object.assign(productsData, data.products);\n  }\n  \n  // Extract statuses\n  if (data.statuses && Array.isArray(data.statuses)) {\n    statusesData = data.statuses;\n    console.log(`✓ Found ${statusesData.length} order statuses`);\n  }\n});\n\n// Create manufacturer mapping (ID → Name)\nconst manufacturerMap = {};\nmanufacturersData.forEach(manufacturer => {\n  const id = String(manufacturer.manufacturer_id);\n  manufacturerMap[id] = manufacturer.name;\n});\n\nconsole.log(`\\n=== DATA SUMMARY ===`);\nconsole.log(`Manufacturers: ${Object.keys(manufacturerMap).length}`);\nconsole.log(`Sample manufacturers:`, Object.entries(manufacturerMap).slice(0, 5));\nconsole.log(`Products with brand data: ${Object.keys(productsData).length}`);\nconsole.log(`Orders to process: ${ordersData.length}`);\nconsole.log(`Statuses: ${statusesData.length}`);\n\n// Create status mapping\nconst statusMap = {};\nstatusesData.forEach(status => {\n  statusMap[String(status.id)] = status.name;\n});\n\n// Statistics\nlet stats = {\n  totalProducts: 0,\n  productsWithBrand: 0,\n  productsWithoutBrand: 0,\n  missingProductData: 0,\n  uniqueBrands: new Set(),\n  missingProductIds: new Set()\n};\n\n// Process and enrich orders\nconst enrichedOrders = ordersData.map((order, orderIndex) => {\n  // Clone the order to avoid mutation\n  const enrichedOrder = JSON.parse(JSON.stringify(order));\n  \n  // Add status name\n  enrichedOrder.status_name = statusMap[String(order.order_status_id)] || 'Unknown';\n  \n  // Process products in the order\n  if (enrichedOrder.products && Array.isArray(enrichedOrder.products)) {\n    enrichedOrder.products = enrichedOrder.products.map((product, productIndex) => {\n      stats.totalProducts++;\n      \n      const productIdStr = String(product.product_id);\n      const productData = productsData[productIdStr];\n      \n      // Initialize brand as Unknown\n      let brandName = 'Unknown';\n      let categoryName = '';\n      let ean = product.ean || '';\n      \n      if (productData) {\n        // Get manufacturer/brand name\n        if (productData.manufacturer_id) {\n          const manufacturerIdStr = String(productData.manufacturer_id);\n          const manufacturer = manufacturerMap[manufacturerIdStr];\n          \n          if (manufacturer) {\n            brandName = manufacturer;\n            stats.uniqueBrands.add(brandName);\n            stats.productsWithBrand++;\n          } else {\n            console.warn(`Manufacturer ID ${manufacturerIdStr} not found in map for product ${productIdStr}`);\n            stats.productsWithoutBrand++;\n          }\n        } else {\n          stats.productsWithoutBrand++;\n        }\n        \n        // Get category from text_fields\n        if (productData.text_fields) {\n          categoryName = productData.text_fields.extra_field_851 || '';\n        }\n        \n        // Get EAN\n        ean = productData.ean || product.ean || '';\n        \n      } else {\n        stats.missingProductData++;\n        stats.missingProductIds.add(productIdStr);\n        \n        if (orderIndex < 3 && productIndex < 2) {\n          console.warn(`No product data found for ID: ${productIdStr}`);\n        }\n      }\n      \n      // Debug first few products\n      if (orderIndex === 0 && productIndex < 3) {\n        console.log(`\\nSample enrichment - Product ${productIdStr}:`, {\n          originalName: product.name,\n          brand: brandName,\n          hasProductData: !!productData,\n          manufacturerId: productData?.manufacturer_id,\n          category: categoryName ? categoryName.substring(0, 50) + '...' : 'None'\n        });\n      }\n      \n      // Return enriched product\n      return {\n        ...product,\n        brand: brandName,\n        category_name: categoryName,\n        ean: ean\n      };\n    });\n  }\n  \n  return enrichedOrder;\n});\n\n// Calculate coverage\nconst coverage = stats.totalProducts > 0 \n  ? ((stats.productsWithBrand / stats.totalProducts) * 100).toFixed(1)\n  : 0;\n\nconsole.log(`\\n=== ENRICHMENT RESULTS ===`);\nconsole.log(`Total products: ${stats.totalProducts}`);\nconsole.log(`Products with brands: ${stats.productsWithBrand} (${coverage}%)`);\nconsole.log(`Products without brands: ${stats.productsWithoutBrand}`);\nconsole.log(`Products missing data: ${stats.missingProductData}`);\nconsole.log(`Unique brands found: ${stats.uniqueBrands.size}`);\nconsole.log(`Brand names:`, Array.from(stats.uniqueBrands).slice(0, 10).join(', '));\n\nif (stats.missingProductData > 0) {\n  console.warn(`\\n⚠️ Missing product IDs (first 10):`, \n    Array.from(stats.missingProductIds).slice(0, 10).join(', '));\n}\n\n// Return results\nconst output = [];\n\n// Add statistics as first item\noutput.push({\n  json: {\n    _type: 'statistics',\n    _timestamp: new Date().toISOString(),\n    coverage: {\n      total_products: stats.totalProducts,\n      with_brand: stats.productsWithBrand,\n      without_brand: stats.productsWithoutBrand,\n      missing_data: stats.missingProductData,\n      coverage_percent: `${coverage}%`,\n      unique_brands: stats.uniqueBrands.size\n    },\n    brands: Array.from(stats.uniqueBrands),\n    missing_ids: Array.from(stats.missingProductIds).slice(0, 20)\n  }\n});\n\n// Add enriched orders\nenrichedOrders.forEach(order => {\n  output.push({ json: order });\n});\n\nreturn output;"},"id":"merge_product_data","name":"Merge Product Data","type":"n8n-nodes-base.code","typeVersion":2,"position":[2320,-440]},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getInventoryManufacturers"},{"name":"parameters","value":"{\"inventory_id\":758}"}]},"options":{"timeout":30000}},"id":"c5dda8bd-6bde-4b69-951b-1be765ced825","name":"Get Product Manufacturers","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1872,-704],"retryOnFail":true},{"parameters":{"mode":"combine","combineBy":"combineByPosition","numberInputs":4,"options":{}},"type":"n8n-nodes-base.merge","typeVersion":3.2,"position":[2096,-472],"id":"02667abe-006e-4583-8357-2c78e454af1e","name":"Merge"},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getOrderStatusList"},{"name":"parameters","value":"{}"}]},"options":{}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1872,256],"id":"1f5c5d4d-7fb4-4810-97f5-aba0bf10d275","name":"Get Order Status List","retryOnFail":true},{"parameters":{"jsCode":"// Konfiguracja dozwolonych statusów - zgodnie z wymaganiami klienta\n// Tylko statusy końcowe: zrealizowane, anulowane, zwroty\n\nconst allowedStatuses = [\n  // === WYSŁANE/ZREALIZOWANE/DOSTARCZONE ===\n  300027, // WYSŁANE // .PL\n  300028, // WYSŁANE // _PL\n  361867, // DE // Wysłane\n  386333, // SM // WYSŁANE\n  354789, // Dostarczone\n  354788, // Paczka w punkcie\n  300828, // OO // Odebrane\n  \n  // === ANULOWANE ===\n  301588, // Anulowane (przez klienta)\n  302472, // Anulowane (brak wpłaty)\n  302473, // Anulowane (zwrot paczki)\n  302475, // Anulowane (brak towaru)\n  303313, // Anulowane STA\n  304745, // Anulacja przed wysyłką\n  316385, // Anulacje (testowe)\n  359429, // WMS Anulowane\n  302981, // [Optima] Anulacja RO\n  \n  // === ZWROTY ===\n  300346, // ZWR // Przedpłacone\n  303161, // ZWR // Deklaracja zwrotu\n  311537, // SOK // Zwroty paczek\n  354792, // Zwrot kurierski\n  372310, // MolZwrotPrzyjęty [sok] zwrot$\n  374054, // Zwroty MADS (Hebe i Mitago_EU)\n  374055, // Zwroty MAGU Kau+Pigu\n  356655, // [MOL] Zwroty częściowe\n  356674, // EXC zwrócone\n  \n  // === NIEDOSTARCZONE (może być traktowane jako anulowane) ===\n  354791  // Niedostarczone\n];\n\n// Mapowanie kategorii statusów dla lepszego raportowania\nconst statusCategories = {\n  'ZREALIZOWANE': [300027, 300028, 361867, 386333, 354789, 354788, 300828],\n  'ANULOWANE': [301588, 302472, 302473, 302475, 303313, 304745, 316385, 359429, 302981],\n  'ZWROTY': [300346, 303161, 311537, 354792, 372310, 374054, 374055, 356655, 356674],\n  'NIEDOSTARCZONE': [354791]\n};\n\n// Pobierz wszystkie zamówienia\nconst items = $input.all();\n\nconsole.log(\"=== FILTER ORDER STATUS ===\");\nconsole.log(`Total orders received: ${items.length}`);\n\n// Zbierz statystyki\nconst statusStats = {};\nconst categoryStats = {\n  'ZREALIZOWANE': 0,\n  'ANULOWANE': 0,\n  'ZWROTY': 0,\n  'NIEDOSTARCZONE': 0,\n  'OTHER': 0\n};\n\nlet passedCount = 0;\nlet filteredCount = 0;\n\n// Filtruj zamówienia\nconst filteredOrders = items.filter(item => {\n  const order = item.json;\n  const statusId = parseInt(order.order_status_id);\n  const statusName = order.status_name || 'Unknown';\n  \n  // Zbierz statystyki szczegółowe\n  const key = `${statusId} - ${statusName}`;\n  statusStats[key] = (statusStats[key] || 0) + 1;\n  \n  // Określ kategorię statusu\n  let category = 'OTHER';\n  for (const [cat, ids] of Object.entries(statusCategories)) {\n    if (ids.includes(statusId)) {\n      category = cat;\n      break;\n    }\n  }\n  categoryStats[category]++;\n  \n  // Sprawdź czy status jest dozwolony\n  const isAllowed = allowedStatuses.includes(statusId);\n  \n  if (isAllowed) {\n    passedCount++;\n    console.log(`  ✅ Order ${order.order_id}: Status ${statusId} (${statusName}) - ${category}`);\n  } else {\n    filteredCount++;\n    if (filteredCount <= 5) { // Pokaż tylko pierwsze 5 odrzuconych\n      console.log(`  ❌ Order ${order.order_id}: Status ${statusId} (${statusName}) - FILTERED OUT`);\n    }\n  }\n  \n  return isAllowed;\n});\n\n// Raport podsumowujący\nconsole.log(\"\\n=== STATUS CATEGORY SUMMARY ===\");\nconsole.log(`📦 Zrealizowane: ${categoryStats.ZREALIZOWANE} orders`);\nconsole.log(`❌ Anulowane: ${categoryStats.ANULOWANE} orders`);\nconsole.log(`↩️ Zwroty: ${categoryStats.ZWROTY} orders`);\nconsole.log(`⚠️ Niedostarczone: ${categoryStats.NIEDOSTARCZONE} orders`);\nconsole.log(`❓ Inne (odrzucone): ${categoryStats.OTHER} orders`);\n\nconsole.log(\"\\n=== DETAILED STATUS DISTRIBUTION ===\");\n// Sortuj statusy według liczby zamówień\nconst sortedStats = Object.entries(statusStats)\n  .sort((a, b) => b[1] - a[1])\n  .slice(0, 10); // Pokaż top 10\n\nsortedStats.forEach(([status, count]) => {\n  const statusId = parseInt(status.split(' ')[0]);\n  const allowed = allowedStatuses.includes(statusId) ? '✅' : '❌';\n  \n  // Znajdź kategorię\n  let category = 'OTHER';\n  for (const [cat, ids] of Object.entries(statusCategories)) {\n    if (ids.includes(statusId)) {\n      category = cat;\n      break;\n    }\n  }\n  \n  console.log(`  ${allowed} ${status}: ${count} orders [${category}]`);\n});\n\nconsole.log(`\\n=== FINAL SUMMARY ===`);\nconsole.log(`✅ Passed to Synerise: ${passedCount} orders`);\nconsole.log(`❌ Filtered out: ${filteredCount} orders`);\nconsole.log(`📊 Filter efficiency: ${((passedCount/items.length)*100).toFixed(1)}%`);\n\n// Ostrzeżenie jeśli wszystko odrzucone\nif (passedCount === 0 && items.length > 0) {\n  console.error(\"⚠️ WARNING: All orders were filtered out! Check status configuration.\");\n}\n\n// WAŻNE: Zwróć przefiltrowane zamówienia jako items\nreturn filteredOrders;"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[2544,-440],"id":"b86b7a29-5237-4232-af23-adac098b11e6","name":"Filter Order Status"},{"parameters":{"jsCode":"// Generate CSV with semicolon delimiter (16 columns)\n// UPDATED: Changed from 23 columns (comma) to 16 columns (semicolon)\nconst input = $input.all()[0].json;\nconst csvRows = input.data;\n\n// 16 columns (removed: eventSalt, clientName, clientPhone, productCategory, productEan, source, notes)\nconst headers = [\n  'orderId',\n  'recordedAt',\n  'clientEmail',\n  'status',\n  'statusName',\n  'totalAmount',\n  'currency',\n  'productSku',\n  'productName',\n  'productBrand',\n  'productPrice',\n  'productQuantity',\n  'paymentMethod',\n  'paymentType',\n  'deliveryMethod',\n  'deliveryPrice'\n];\n\n// Function to escape CSV values for semicolon delimiter\nfunction escapeCsvValue(value) {\n  if (value === null || value === undefined) return '';\n  const stringValue = String(value);\n  \n  // If contains semicolon, double quote, or newline, wrap in quotes\n  if (stringValue.includes(';') || stringValue.includes('\"') || stringValue.includes('\\n')) {\n    return `\"${stringValue.replace(/\"/g, '\"\"')}\"`;\n  }\n  return stringValue;\n}\n\n// Generate CSV with SEMICOLON delimiter\nlet csvContent = headers.join(';') + '\\n';\n\n// Add data rows\ncsvRows.forEach(row => {\n  const values = headers.map(header => escapeCsvValue(row[header]));\n  csvContent += values.join(';') + '\\n';\n});\n\nconsole.log(`Generated CSV: ${csvRows.length} rows, 16 columns, semicolon delimiter`);\n\n// Return as binary data\nreturn {\n  json: {\n    filename: input.filename,\n    rowCount: csvRows.length,\n    columnCount: headers.length,\n    delimiter: 'semicolon'\n  },\n  binary: {\n    data: {\n      data: Buffer.from(csvContent, 'utf8').toString('base64'),\n      mimeType: 'text/csv',\n      fileName: input.filename\n    }\n  }\n};"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[3216,-440],"id":"f78311b1-4880-45f1-83cb-b134a3eea978","name":"Code in JavaScript"},{"parameters":{"jsCode":"// Extract unique order IDs from journal\nconst items = $input.all();\nconst logs = items[0]?.json?.logs || [];\n\n// Get unique order IDs\nconst orderIds = [...new Set(logs.map(log => log.order_id).filter(id => id))];\n\n// Przygotuj outputy\nlet output0 = []; // Order IDs do przetworzenia\nlet output1 = []; // Dane do Google Sheets update\n\nif (logs.length > 0) {\n  // Znajdź log z najwyższą datą (najświeższy)\n  const newestLog = logs.reduce((newest, current) => {\n    return current.date > newest.date ? current : newest;\n  }, logs[0]);\n  \n  const newLastLogId = newestLog.log_id;\n  const newestDate = new Date(newestLog.date * 1000).toISOString();\n  \n  console.log(`Found ${logs.length} logs, ${orderIds.length} unique orders`);\n  console.log(`Newest log_id: ${newLastLogId} from date: ${newestDate}`);\n  console.log(`Newest log details:`, newestLog);\n  \n  // Output 0: Order IDs do przetworzenia\n  output0 = orderIds.map((orderId, index) => ({\n    json: {\n      orderId: orderId,\n      totalCount: orderIds.length,\n      currentIndex: index + 1\n    }\n  }));\n  \n  // Output 1: Dane do update Google Sheets (4 wiersze - dodałem newest_date)\n  output1 = [\n    {\n      json: {\n        key: 'last_log_id',\n        value: newLastLogId.toString(),\n        row_number: 2\n      }\n    },\n    {\n      json: {\n        key: 'last_sync',\n        value: new Date().toISOString(),\n        row_number: 3\n      }\n    },\n    {\n      json: {\n        key: 'total_orders',\n        value: orderIds.length.toString(),\n        row_number: 4\n      }\n    },\n    {\n      json: {\n        key: 'newest_log_date',\n        value: newestDate,\n        row_number: 5\n      }\n    }\n  ];\n} else {\n  console.log(\"No new orders to process\");\n}\n\n// Zwróć dane dla obu outputów\nreturn {\n  0: output0,  // Idzie do Loop Over Items\n  1: output1   // Idzie do Google Sheets Update\n};"},"id":"extract_order_ids","name":"Extract Order IDs & Prepare Update","type":"n8n-nodes-base.code","position":[528,-272],"typeVersion":2},{"parameters":{"fieldToSplitOut":"[\"0\"]","options":{}},"type":"n8n-nodes-base.splitOut","typeVersion":1,"position":[752,-80],"id":"bd9dc77a-8626-42cb-8d89-7029d5957980","name":"Split Out1"},{"parameters":{"assignments":{"assignments":[{"id":"89673c3e-0d1b-4e40-a3b7-87bc1d14ac25","name":"1","value":"={{ $json['1'] }}","type":"array"}]},"options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[752,-992],"id":"ab3d3a59-bf28-44ff-9bf3-2b2385af2e63","name":"Edit Fields"},{"parameters":{"options":{}},"type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[976,-80],"id":"35f888c4-1fd5-4f9f-83b0-f4b5d86f2f2b","name":"Loop Over Items"},{"parameters":{"jsCode":"// Inicjalizacja pętli\nconst staticData = $getWorkflowStaticData('global');\nconst lastLogId = $input.first().json.value || null;\nconst maxIterations = 50; // Zabezpieczenie przed nieskończoną pętlą\n\nreturn [{\n  json: {\n    lastLogId: lastLogId,\n    iteration: 0,\n    maxIterations: maxIterations,\n    continue: true,\n    cutoffTimestamp: Math.floor(Date.now() / 1000) - (24 * 60 * 60) // 24h temu\n  }\n}];"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[-816,-176],"id":"5f9815c0-2159-4208-adf0-078fde06e7be","name":"Code in JavaScript2"},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getJournalList"},{"name":"parameters","value":"={{ JSON.stringify({ logs_types: [1, 18], last_log_id: $json.lastLogId }) }}"}]},"options":{"timeout":30000}},"id":"8936a624-f84b-49a6-a2ab-149c91788642","name":"Get Journal Changes2","type":"n8n-nodes-base.httpRequest","position":[-592,-176],"typeVersion":4.2},{"parameters":{"jsCode":"// Sprawdź czy dotarliśmy do świeżych logów\nconst logs = $json.logs || [];\nconst iteration = $json.iteration || 0;\nconst maxIterations = $json.maxIterations || 50;\n\n// ZMIANA: Ustaw cutoffTimestamp na 1 godzinę temu\nconst cutoffTimestamp = $json.cutoffTimestamp || (Math.floor(Date.now() / 1000) - (360 * 60)); // 1h temu\n\nif (logs.length === 0) {\n  // Brak więcej logów - zakończ\n  return [{\n    json: {\n      continue: false,\n      reason: \"No more logs\",\n      ordersToProcess: [],\n      lastProcessedDate: null,\n      lastLogId: null\n    }\n  }];\n}\n\n// Bezpieczne znajdowanie min/max z walidacją\nconst validLogs = logs.filter(log => \n  log && \n  log.date && !isNaN(log.date) && \n  log.log_id && !isNaN(log.log_id)\n);\n\nif (validLogs.length === 0) {\n  console.error(\"No valid logs found:\", logs);\n  return [{\n    json: {\n      continue: false,\n      reason: \"Invalid log data\",\n      ordersToProcess: [],\n      error: \"No valid logs with date and log_id\"\n    }\n  }];\n}\n\n// Znajdź log z najnowszą datą (największy timestamp)\nconst newestLog = validLogs.reduce((newest, current) => {\n  return current.date > newest.date ? current : newest;\n}, validLogs[0]);\n\n// Znajdź log z najstarszą datą \nconst oldestLog = validLogs.reduce((oldest, current) => {\n  return current.date < oldest.date ? current : oldest;\n}, validLogs[0]);\n\nconst oldestTimestamp = oldestLog.date;\nconst newestTimestamp = newestLog.date;\nconst newestLogId = newestLog.log_id;\n\n// Konwersja na daty\nconst oldestDate = new Date(oldestTimestamp * 1000);\nconst newestDate = new Date(newestTimestamp * 1000);\nconst cutoffDate = new Date(cutoffTimestamp * 1000);\nconst currentDate = new Date();\n\n// KLUCZOWA LOGIKA: Sprawdź czy najstarszy log jest w ciągu ostatniej godziny\nconst isRecent = oldestTimestamp >= cutoffTimestamp;\n\n// Filtruj tylko logi z ostatniej godziny\nconst recentLogs = validLogs.filter(log => log.date >= cutoffTimestamp);\nconst orderIds = [...new Set(recentLogs.map(log => log.order_id).filter(id => id))];\n\n// Oblicz różnicę czasu dla lepszego debugowania\nconst timeDiff = (currentDate.getTime() - oldestDate.getTime()) / 1000; // w sekundach\nconst timeDiffMinutes = Math.round(timeDiff / 60);\nconst timeDiffFormatted = timeDiffMinutes < 60 ? \n  `${timeDiffMinutes} minut temu` : \n  `${Math.round(timeDiffMinutes / 60)} godzin ${timeDiffMinutes % 60} minut temu`;\n\n// Debug info\nconsole.log({\n  iteration: iteration,\n  logsCount: logs.length,\n  validLogsCount: validLogs.length,\n  currentTime: currentDate.toISOString(),\n  oldestDate: oldestDate.toISOString(),\n  newestDate: newestDate.toISOString(),\n  cutoffDate: cutoffDate.toISOString(),\n  oldestLogAge: timeDiffFormatted,\n  newestLogId: newestLogId,\n  isRecent: isRecent,\n  recentLogsCount: recentLogs.length,\n  uniqueOrdersCount: orderIds.length,\n  shouldContinue: !isRecent && iteration < maxIterations\n});\n\n// LOGIKA DECYZYJNA - szukamy danych z ostatniej GODZINY\nif (isRecent || iteration >= maxIterations) {\n  // ZAKOŃCZ tylko gdy:\n  // 1. Znaleźliśmy dane z ostatniej godziny (isRecent = true)\n  // 2. LUB osiągnęliśmy limit iteracji\n  \n  console.log(`STOPPING: isRecent=${isRecent}, iteration=${iteration}/${maxIterations}`);\n  console.log(`Found ${recentLogs.length} logs from last hour with ${orderIds.length} unique orders`);\n  \n  // Zapisz stan\n  const staticData = $getWorkflowStaticData('global');\n  staticData.lastLogId = newestLogId;\n  staticData.lastProcessedTimestamp = newestTimestamp;\n  staticData.lastProcessedDate = newestDate.toISOString();\n  \n  return [{\n    json: {\n      continue: false, // STOP\n      ordersToProcess: orderIds,\n      logsProcessed: recentLogs.length,\n      lastLogId: newestLogId,\n      lastProcessedTimestamp: newestTimestamp,\n      lastProcessedDate: newestDate.toISOString(),\n      oldestDateInBatch: oldestDate.toISOString(),\n      newestDateInBatch: newestDate.toISOString(),\n      timePeriod: \"Last 1 hour\", // Informacja o okresie czasu\n      newestLogDetails: {\n        log_id: newestLog.log_id,\n        order_id: newestLog.order_id,\n        log_type: newestLog.log_type,\n        date_timestamp: newestLog.date,\n        date_iso: newestDate.toISOString()\n      },\n      reachedLimit: iteration >= maxIterations,\n      totalIterations: iteration,\n      cutoffUsed: cutoffDate.toISOString(),\n      isRecent: isRecent,\n      stopReason: isRecent ? \"Found logs from last hour\" : \"Reached iteration limit\",\n      summary: `Processed ${recentLogs.length} logs from last hour containing ${orderIds.length} unique orders`\n    }\n  }];\n} else {\n  // KONTYNUUJ - nie znaleźliśmy jeszcze danych z ostatniej godziny\n  console.log(`CONTINUING: Looking for logs after ${cutoffDate.toISOString()} (last hour)`);\n  console.log(`Current oldest: ${oldestDate.toISOString()} (${timeDiffFormatted}), need to go deeper...`);\n  \n  return [{\n    json: {\n      continue: true, // KONTYNUUJ PĘTLĘ\n      lastLogId: newestLogId, // Użyj najnowszy log_id jako punkt startowy dla następnej iteracji\n      iteration: iteration + 1,\n      maxIterations: maxIterations,\n      cutoffTimestamp: cutoffTimestamp,\n      partialOrders: orderIds, // Przekaż częściowe wyniki\n      dateRange: {\n        oldest: oldestDate.toISOString(),\n        newest: newestDate.toISOString(),\n        cutoff: cutoffDate.toISOString()\n      },\n      timePeriod: \"Last 1 hour\",\n      message: `Iteration ${iteration + 1}: Found ${logs.length} logs, oldest is ${timeDiffFormatted}, need logs from last hour`\n    }\n  }];\n}"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[-368,-248],"id":"129494d7-d610-4501-aaee-83fb1b699587","name":"Code in JavaScript3"},{"parameters":{"conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"conditions":[{"id":"fbeec7a0-b97b-4e8c-b5e4-057af2b9e4c7","leftValue":"={{ $json.continue }}","rightValue":"","operator":{"type":"boolean","operation":"true","singleValue":true}}],"combinator":"and"},"options":{}},"type":"n8n-nodes-base.if","typeVersion":2.2,"position":[-144,-176],"id":"3372a372-6500-4dbb-af4b-1dc8d2697902","name":"If"},{"parameters":{"options":{}},"type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[1648,-200],"id":"a153f10d-55bb-49a6-9c18-7b087b9bbc27","name":"Loop Over Items1"},{"parameters":{"rules":{"values":[{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"leftValue":"={{ $json.type }}","rightValue":"ordersData","operator":{"type":"string","operation":"equals"},"id":"16455973-160a-451f-9ce5-d3e00d4ec21c"}],"combinator":"and"},"renameOutput":true,"outputKey":"orders"},{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"id":"d0ff60de-b3c8-45e4-8d2e-b00c0acc1685","leftValue":"={{ $json.type }}","rightValue":"productBatch","operator":{"type":"string","operation":"equals","name":"filter.operator.equals"}}],"combinator":"and"},"renameOutput":true,"outputKey":"product"}]},"options":{}},"type":"n8n-nodes-base.switch","typeVersion":3.3,"position":[1424,-272],"id":"2eeef40d-3217-4583-9238-e45cdf92fd64","name":"Switch"},{"parameters":{"jsCode":"// Transform If Node Output to Journal Format\n// This replaces both \"Edit Fields1\" and \"Get Journal Changes\" nodes\n// Converts ordersToProcess array into a logs format expected by downstream nodes\n\nconst input = $input.first().json;\n\n// Extract data from the If node\nconst orderIds = input.ordersToProcess || [];\nconst lastLogId = input.lastLogId;\nconst lastProcessedTimestamp = input.lastProcessedTimestamp;\nconst lastProcessedDate = input.lastProcessedDate;\nconst logsProcessed = input.logsProcessed;\n\nconsole.log(\"=== TRANSFORM IF OUTPUT TO JOURNAL FORMAT ===\");\nconsole.log(`Converting ${orderIds.length} order IDs to logs format`);\nconsole.log(`Last Log ID: ${lastLogId}`);\nconsole.log(`Logs originally processed: ${logsProcessed}`);\n\n// Create synthetic logs from the order IDs\n// We need to create a logs array that looks like what the API would return\n// but with ALL the orders from the last hour, not just new ones\nconst logs = [];\n\n// Generate log entries for each order\n// We'll create one log entry per order (log_type 1 = order created/modified)\norderIds.forEach((orderId, index) => {\n  logs.push({\n    log_id: lastLogId - (orderIds.length - index - 1), // Create sequential log IDs\n    log_type: 1, // Type 1 = order event\n    order_id: orderId,\n    object_id: 0,\n    date: lastProcessedTimestamp // Use the timestamp from processing\n  });\n});\n\nconsole.log(`✅ Created ${logs.length} log entries from ${orderIds.length} orders`);\n\n// Create output in the same format as \"Get Journal Changes\" would return\nconst output = {\n  status: \"SUCCESS\",\n  logs: logs,\n  // Add metadata for tracking\n  _metadata: {\n    source: \"If_node_transformation\",\n    originalOrderCount: orderIds.length,\n    lastLogId: lastLogId,\n    lastProcessedDate: lastProcessedDate,\n    processedAt: new Date().toISOString()\n  }\n};\n\n// Log sample for debugging\nif (logs.length > 0) {\n  console.log(\"\\nSample log entries (first 3):\");\n  logs.slice(0, 3).forEach(log => {\n    console.log(`  Order ${log.order_id}, Log ID: ${log.log_id}`);\n  });\n  if (logs.length > 3) {\n    console.log(`  ... and ${logs.length - 3} more`);\n  }\n}\n\n// Return in the format expected by \"Check If Changes Exist\"\nreturn [{ json: output }];"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[80,-176],"id":"7ef1fd1a-8d85-4bea-ac79-b6f3ed3e3cde","name":"Code in JavaScript4"},{"parameters":{"aggregate":"aggregateAllItemData","options":{}},"type":"n8n-nodes-base.aggregate","typeVersion":1,"position":[1872,-248],"id":"494369fd-2f92-487d-914b-f62b31de2e2e","name":"Aggregate"},{"parameters":{"jsCode":"// Filter out marketplace orders - keep ONLY online shop orders\n// Marketplace IDs to EXCLUDE: 15156 (Allegro), 15157 (Erli), 15158 (eMag)\n\nconst MARKETPLACE_IDS_TO_EXCLUDE = [15156, 15157, 15158];\n\nconst items = $input.all();\nconst orders = items[0]?.json?.orders || [];\n\nconsole.log('=== FILTER ONLINE SHOP ORDERS ===');\nconsole.log(`Total orders before filtering: ${orders.length}`);\n\n// Statistics\nconst stats = {\n  total: orders.length,\n  onlineShop: 0,\n  marketplace: 0,\n  bySource: {}\n};\n\n// Filter orders\nconst filteredOrders = orders.filter(order => {\n  const orderSourceId = parseInt(order.order_source_id);\n  \n  // Track statistics\n  const sourceName = order.order_source || 'Unknown';\n  stats.bySource[sourceName] = (stats.bySource[sourceName] || 0) + 1;\n  \n  const isMarketplace = MARKETPLACE_IDS_TO_EXCLUDE.includes(orderSourceId);\n  \n  if (isMarketplace) {\n    stats.marketplace++;\n    console.log(`  ❌ EXCLUDED: Order ${order.order_id} from ${sourceName} (ID: ${orderSourceId})`);\n    return false;\n  } else {\n    stats.onlineShop++;\n    return true;\n  }\n});\n\nconsole.log('\\n=== FILTERING RESULTS ===');\nconsole.log(`✅ Online shop orders: ${stats.onlineShop}`);\nconsole.log(`❌ Marketplace orders (excluded): ${stats.marketplace}`);\nconsole.log(`📊 Filter efficiency: ${((stats.onlineShop/stats.total)*100).toFixed(1)}% kept`);\n\nconsole.log('\\n=== ORDERS BY SOURCE ===');\nObject.entries(stats.bySource).forEach(([source, count]) => {\n  console.log(`  ${source}: ${count} orders`);\n});\n\n// Return filtered orders in the same structure\nreturn [{\n  json: {\n    orders: filteredOrders,\n    _filterStats: stats,\n    _timestamp: new Date().toISOString()\n  }\n}];"},"id":"filter_shop_orders","name":"Filter Online Shop Orders","type":"n8n-nodes-base.code","typeVersion":2,"position":[1424,16]},{"parameters":{"operation":"get","dataTableId":{"__rl":true,"value":"ZLqPZuPvd8alE2nF","mode":"list","cachedResultName":"workflow_state_synerise","cachedResultUrl":"/projects/jlSr7IBJt4siEqiM/datatables/ZLqPZuPvd8alE2nF"},"returnAll":true},"type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[-1040,-176],"id":"07b192e4-4094-4e70-82da-6f63b22ed42d","name":"get state"},{"parameters":{"operation":"update","dataTableId":{"__rl":true,"value":"ZLqPZuPvd8alE2nF","mode":"list","cachedResultName":"workflow_state_synerise","cachedResultUrl":"/projects/jlSr7IBJt4siEqiM/datatables/ZLqPZuPvd8alE2nF"},"filters":{"conditions":[{"keyName":"key","keyValue":"={{ $json['1'][0].json.key }}"}]},"columns":{"mappingMode":"defineBelow","value":{"value":"={{ $json['1'][0].json.value }}"},"matchingColumns":[],"schema":[{"id":"key","displayName":"key","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":true},{"id":"value","displayName":"value","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":false}],"attemptToConvertTypes":false,"convertFieldsToString":false},"options":{}},"type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[976,-992],"id":"141ccff1-591f-4c54-b04a-bec2a0fea7dd","name":"Update row(s)"},{"parameters":{"operation":"update","dataTableId":{"__rl":true,"value":"ZLqPZuPvd8alE2nF","mode":"list","cachedResultName":"workflow_state_synerise","cachedResultUrl":"/projects/jlSr7IBJt4siEqiM/datatables/ZLqPZuPvd8alE2nF"},"filters":{"conditions":[{"keyName":"key","keyValue":"={{ $('Edit Fields').item.json['1'][1].json.key }}"}]},"columns":{"mappingMode":"defineBelow","value":{"value":"={{ $('Edit Fields').item.json['1'][1].json.value }}"},"matchingColumns":[],"schema":[{"id":"key","displayName":"key","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":true},{"id":"value","displayName":"value","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":false}],"attemptToConvertTypes":false,"convertFieldsToString":false},"options":{}},"type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[1200,-992],"id":"6cf4c1c6-bc18-4477-804e-565104921745","name":"Update row(s)1"},{"parameters":{"operation":"update","dataTableId":{"__rl":true,"value":"ZLqPZuPvd8alE2nF","mode":"list","cachedResultName":"workflow_state_synerise","cachedResultUrl":"/projects/jlSr7IBJt4siEqiM/datatables/ZLqPZuPvd8alE2nF"},"filters":{"conditions":[{"keyName":"key","keyValue":"={{ $('Edit Fields').item.json['1'][2].json.key }}"}]},"columns":{"mappingMode":"defineBelow","value":{"value":"={{ $('Edit Fields').item.json['1'][2].json.value }}"},"matchingColumns":[],"schema":[{"id":"key","displayName":"key","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":true},{"id":"value","displayName":"value","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":false}],"attemptToConvertTypes":false,"convertFieldsToString":false},"options":{}},"type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[1424,-992],"id":"2cb43758-e040-4b61-8195-459ec5a4cd2a","name":"Update row(s)2"},{"parameters":{"operation":"update","dataTableId":{"__rl":true,"value":"ZLqPZuPvd8alE2nF","mode":"list","cachedResultName":"workflow_state_synerise","cachedResultUrl":"/projects/jlSr7IBJt4siEqiM/datatables/ZLqPZuPvd8alE2nF"},"filters":{"conditions":[{"keyName":"key","keyValue":"={{ $('Edit Fields').item.json['1'][3].json.key }}"}]},"columns":{"mappingMode":"defineBelow","value":{"value":"={{ $('Edit Fields').item.json['1'][3].json.value }}"},"matchingColumns":[],"schema":[{"id":"key","displayName":"key","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":true},{"id":"value","displayName":"value","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":false}],"attemptToConvertTypes":false,"convertFieldsToString":false},"options":{}},"type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[1648,-992],"id":"57f5a418-5443-4c70-b546-65333a3b54e1","name":"Update row(s)3"}],"connections":{"Manual Trigger":{"main":[[{"node":"get state","type":"main","index":0}]]},"Schedule Trigger - Every 15 min":{"main":[[{"node":"get state","type":"main","index":0}]]},"Transform to Synerise Format":{"main":[[{"node":"Prepare CSV Data","type":"main","index":0}]]},"Prepare CSV Data":{"main":[[{"node":"Code in JavaScript","type":"main","index":0}]]},"Check If Changes Exist":{"main":[[{"node":"Extract Order IDs & Prepare Update","type":"main","index":0},{"node":"Get Product Manufacturers","type":"main","index":0},{"node":"Get Order Status List","type":"main","index":0}],[{"node":"No Changes - Stop","type":"main","index":0}]]},"Extract Product IDs":{"main":[[{"node":"Switch","type":"main","index":0}]]},"Get Product Brands":{"main":[[{"node":"Loop Over Items1","type":"main","index":0}]]},"Merge Product Data":{"main":[[{"node":"Filter Order Status","type":"main","index":0}]]},"Get Product Manufacturers":{"main":[[{"node":"Merge","type":"main","index":0}]]},"Get Order Status List":{"main":[[{"node":"Merge","type":"main","index":3}]]},"Merge":{"main":[[{"node":"Merge Product Data","type":"main","index":0}]]},"Filter Order Status":{"main":[[{"node":"Transform to Synerise Format","type":"main","index":0}]]},"Extract Order IDs & Prepare Update":{"main":[[{"node":"Split Out1","type":"main","index":0},{"node":"Edit Fields","type":"main","index":0}]]},"Split Out1":{"main":[[{"node":"Loop Over Items","type":"main","index":0}]]},"Edit Fields":{"main":[[{"node":"Update row(s)","type":"main","index":0}]]},"Loop Over Items":{"main":[[{"node":"Extract Product IDs","type":"main","index":0}],[{"node":"Get Orders by IDs","type":"main","index":0}]]},"Code in JavaScript2":{"main":[[{"node":"Get Journal Changes2","type":"main","index":0}]]},"Get Journal Changes2":{"main":[[{"node":"Code in JavaScript3","type":"main","index":0}]]},"Code in JavaScript3":{"main":[[{"node":"If","type":"main","index":0}]]},"If":{"main":[[{"node":"Get Journal Changes2","type":"main","index":0}],[{"node":"Code in JavaScript4","type":"main","index":0}]]},"Loop Over Items1":{"main":[[{"node":"Aggregate","type":"main","index":0}],[{"node":"Get Product Brands","type":"main","index":0}]]},"Switch":{"main":[[{"node":"Merge","type":"main","index":1}],[{"node":"Loop Over Items1","type":"main","index":0}]]},"Code in JavaScript4":{"main":[[{"node":"Check If Changes Exist","type":"main","index":0}]]},"Aggregate":{"main":[[{"node":"Merge","type":"main","index":2}]]},"Get Orders by IDs":{"main":[[{"node":"Filter Online Shop Orders","type":"main","index":0}]]},"Filter Online Shop Orders":{"main":[[{"node":"Loop Over Items","type":"main","index":0}]]},"get state":{"main":[[{"node":"Code in JavaScript2","type":"main","index":0}]]},"Update row(s)":{"main":[[{"node":"Update row(s)1","type":"main","index":0}]]},"Update row(s)1":{"main":[[{"node":"Update row(s)2","type":"main","index":0}]]},"Update row(s)2":{"main":[[{"node":"Update row(s)3","type":"main","index":0}]]}},"settings":{"saveManualExecutions":true,"executionOrder":"v1"},"staticData":{"node:Schedule Trigger - Every 15 min":{"recurrenceRules":[]}},"meta":{"templateCredsSetupCompleted":true},"pinData":{},"versionId":"c430f332-84b8-4d61-8788-4ec9029c1bda","versionCounter":3,"triggerCount":1,"shared":[{"updatedAt":"2025-09-25T14:46:56.372Z","createdAt":"2025-09-25T14:46:56.372Z","role":"workflow:owner","workflowId":"HMS43ySZOcEfGSyX","projectId":"jlSr7IBJt4siEqiM","project":{"updatedAt":"2025-01-31T18:30:45.852Z","createdAt":"2025-01-31T18:13:38.763Z","id":"jlSr7IBJt4siEqiM","name":"N8N Estyl <n8n@estyl.pl>","type":"personal","icon":null,"description":null,"projectRelations":[{"updatedAt":"2025-01-31T18:13:38.763Z","createdAt":"2025-01-31T18:13:38.763Z","userId":"5089e5a8-9975-48ff-8451-f5c3fb3e78ac","projectId":"jlSr7IBJt4siEqiM","user":{"updatedAt":"2025-11-13T23:00:06.270Z","createdAt":"2025-01-31T18:13:37.839Z","id":"5089e5a8-9975-48ff-8451-f5c3fb3e78ac","email":"n8n@estyl.pl","firstName":"N8N","lastName":"Estyl","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2025-01-31T18:30:49.691Z","personalization_survey_n8n_version":"1.76.1"},"settings":{"userActivated":true,"firstSuccessfulWorkflowId":"xbn9AqbzxO9Xbruw","userActivatedAt":1739378108329,"easyAIWorkflowOnboarded":true,"npsSurvey":{"responded":true,"lastShownAt":1758812569793}},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2025-11-13","isPending":false}}]}}],"tags":[]}

operations:
  [{"type":"updateNode","nodeId":"5f9815c0-2159-4208-adf0-078fde06e7be","updates":{"parameters":{"jsCode":"// Inicjalizacja pętli - FIXED for Data Table compatibility\nconst staticData = $getWorkflowStaticData('global');\n\n// Data Table returns rows with {key, value, id, createdAt, updatedAt}\n// We need to find the specific row where key === 'last_log_id'\nconst items = $input.all();\nconst lastLogIdRow = items.find(item => item.json.key === 'last_log_id');\nconst lastLogId = lastLogIdRow ? lastLogIdRow.json.value : null;\n\nconst maxIterations = 50; // Zabezpieczenie przed nieskończoną pętlą\n\nconsole.log('=== STATE INITIALIZATION ===');\nconsole.log(`Found last_log_id row:`, lastLogIdRow ? 'YES' : 'NO');\nconsole.log(`Last log ID value: ${lastLogId}`);\nconsole.log(`Total state rows: ${items.length}`);\n\nreturn [{\n  json: {\n    lastLogId: lastLogId,\n    iteration: 0,\n    maxIterations: maxIterations,\n    continue: true,\n    cutoffTimestamp: Math.floor(Date.now() / 1000) - (24 * 60 * 60) // 24h temu\n  }\n}];"}}}]


workflow_id: HMS43ySZOcEfGSyX
workflow_name: BaseLinker to Synerise CSV Export - Complete MVP
trigger: partial_update
created_at: 2025-11-14 16:07:39
workflow_snapshot:
  {"updatedAt":"2025-11-14T15:47:05.143Z","createdAt":"2025-09-25T14:46:56.372Z","id":"HMS43ySZOcEfGSyX","name":"BaseLinker to Synerise CSV Export - Complete MVP","active":false,"isArchived":false,"nodes":[{"parameters":{"rule":{"interval":[{"field":"minutes","minutesInterval":15}]}},"id":"schedule_trigger","name":"Schedule Trigger - Every 15 min","type":"n8n-nodes-base.scheduleTrigger","position":[-1264,-272],"typeVersion":1.2},{"parameters":{},"id":"manual_trigger","name":"Manual Trigger","type":"n8n-nodes-base.manualTrigger","position":[-1264,-80],"typeVersion":1},{"parameters":{"jsCode":"// Transform BaseLinker orders to Synerise CSV format\n// Mode: Run Once for All Items\n// FIXED: orderId now uses shop_order_id (short format) instead of order_id\n// FIXED: recordedAt now uses Europe/Warsaw timezone (+2h) instead of UTC\n\nconst items = $input.all();\nconst results = [];\n\nconsole.log(`Processing ${items.length} orders for Synerise export`);\n\n// Map status using the enriched status_name\nconst statusMapping = {\n  'ZREALIZOWANE': 'completed',\n  'ANULOWANE': 'cancelled',\n  'ZWROT': 'returned',\n  'WYSŁANE': 'shipped'\n};\n\n// Process each order\nitems.forEach((item, index) => {\n  try {\n    const order = item.json;\n    \n    // Generate eventSalt\n    const eventSalt = `${order.order_id}_${order.date_add || Date.now()}`;\n    \n    // BUGFIX #2: Convert Unix timestamp to Europe/Warsaw timezone\n    // Add 2 hours offset for Europe/Warsaw (UTC+2 in summer, UTC+1 in winter)\n    let recordedAt;\n    if (order.date_confirmed) {\n      const timestamp = parseInt(order.date_confirmed);\n      const utcDate = new Date(timestamp * 1000);\n      // Add 2 hours (7200000 milliseconds)\n      const warsawDate = new Date(utcDate.getTime() + (2 * 60 * 60 * 1000));\n      recordedAt = warsawDate.toISOString();\n      \n      // Debug log for first order\n      if (index === 0) {\n        console.log(`Timezone fix example:`);\n        console.log(`  Original UTC: ${utcDate.toISOString()}`);\n        console.log(`  Warsaw (+2h): ${recordedAt}`);\n      }\n    } else {\n      recordedAt = new Date().toISOString();\n    }\n    \n    // Find appropriate status based on name\n    let orderStatus = 'unknown';\n    const statusNameUpper = (order.status_name || '').toUpperCase();\n    for (const [key, value] of Object.entries(statusMapping)) {\n      if (statusNameUpper.includes(key)) {\n        orderStatus = value;\n        break;\n      }\n    }\n    \n    // Determine payment type\n    const paymentType = order.payment_method_cod === '1' ? 'Pobranie' : 'Przelew';\n    \n    // Safe parseFloat\n    const safeParseFloat = (value) => {\n      const parsed = parseFloat(value);\n      return isNaN(parsed) ? 0 : parsed;\n    };\n    \n    // Extract client name with fallbacks\n    const clientName = order.delivery_fullname || \n                      order.invoice_fullname || \n                      order.delivery_company ||\n                      `${order.delivery_name || ''} ${order.delivery_surname || ''}`.trim() ||\n                      'Unknown Customer';\n    \n    // BUGFIX #1: Use shop_order_id (short format) instead of order_id\n    // shop_order_id examples: 184031, 184042 (human-readable short numbers)\n    // order_id examples: 790964970, 790932859 (internal BaseLinker IDs)\n    const orderId = order.shop_order_id || order.order_id; // Fallback to order_id if shop_order_id is missing\n    \n    // Debug log for first order to verify fix\n    if (index === 0) {\n      console.log(`Order ID fix example:`);\n      console.log(`  OLD (order_id): ${order.order_id}`);\n      console.log(`  NEW (shop_order_id): ${order.shop_order_id}`);\n      console.log(`  Using: ${orderId}`);\n    }\n    \n    // Add transformed order to results\n    results.push({\n      json: {\n        eventSalt: eventSalt,\n        orderId: orderId, // FIXED: Now uses shop_order_id\n        recordedAt: recordedAt, // FIXED: Now uses Europe/Warsaw timezone\n        clientEmail: order.email || '',\n        clientName: clientName,\n        clientPhone: order.phone || '',\n        status: orderStatus,\n        statusName: order.status_name || '',\n        totalAmount: safeParseFloat(order.payment_done),\n        currency: order.currency || 'PLN',\n        paymentMethod: order.payment_method || '',\n        paymentType: paymentType,\n        deliveryMethod: order.delivery_method || '',\n        deliveryPrice: safeParseFloat(order.delivery_price),\n        products: order.products || [],\n        source: order.order_source || 'BaseLinker',\n        notes: order.admin_comments || ''\n      }\n    });\n    \n  } catch (error) {\n    console.error(`Error processing order at index ${index}:`, error);\n    console.error(`Order data:`, JSON.stringify(item.json).substring(0, 500));\n    \n    // Add failed order with error info\n    results.push({\n      json: {\n        error: true,\n        errorMessage: error.message,\n        orderId: item.json?.order_id || 'UNKNOWN',\n        originalData: item.json\n      }\n    });\n  }\n});\n\nconsole.log(`Successfully transformed ${results.length} orders`);\nconsole.log(`Sample order IDs: ${results.slice(0, 5).map(r => r.json.orderId).join(', ')}`);\n\n// Return all transformed items\nreturn results;"},"id":"transform_data","name":"Transform to Synerise Format","type":"n8n-nodes-base.code","position":[2768,-440],"typeVersion":2},{"parameters":{"jsCode":"// Flatten nested product data for CSV\n// UPDATED: 16 columns (removed: eventSalt, clientName, clientPhone, productCategory, productEan, source, notes)\nconst items = $input.all();\nconst csvRows = [];\n\nitems.forEach(item => {\n  const order = item.json;\n  \n  // If order has products, create a row for each product\n  if (order.products && order.products.length > 0) {\n    order.products.forEach(product => {\n      csvRows.push({\n        orderId: order.orderId,\n        recordedAt: order.recordedAt,\n        clientEmail: order.clientEmail,\n        status: order.status,\n        statusName: order.statusName,\n        totalAmount: order.totalAmount,\n        currency: order.currency,\n        productSku: product.sku || product.product_id,\n        productName: product.name,\n        productBrand: product.brand || 'Unknown',\n        productPrice: product.price_brutto,\n        productQuantity: product.quantity,\n        paymentMethod: order.paymentMethod,\n        paymentType: order.paymentType,\n        deliveryMethod: order.deliveryMethod,\n        deliveryPrice: order.deliveryPrice\n      });\n    });\n  } else {\n    // Order without products\n    csvRows.push({\n      orderId: order.orderId,\n      recordedAt: order.recordedAt,\n      clientEmail: order.clientEmail,\n      status: order.status,\n      statusName: order.statusName,\n      totalAmount: order.totalAmount,\n      currency: order.currency,\n      productSku: '',\n      productName: '',\n      productBrand: '',\n      productPrice: '',\n      productQuantity: '',\n      paymentMethod: order.paymentMethod,\n      paymentType: order.paymentType,\n      deliveryMethod: order.deliveryMethod,\n      deliveryPrice: order.deliveryPrice\n    });\n  }\n});\n\n// Generate filename with timestamp\nconst now = new Date();\nconst filename = `synerise_export_${now.toISOString().slice(0,10)}_${now.toTimeString().slice(0,5).replace(':','')}.csv`;\n\nreturn [{\n  json: {\n    filename: filename,\n    rowCount: csvRows.length,\n    exportTime: now.toISOString(),\n    data: csvRows\n  }\n}];"},"id":"prepare_csv","name":"Prepare CSV Data","type":"n8n-nodes-base.code","position":[2992,-440],"typeVersion":2},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getOrders"},{"name":"parameters","value":"={{ JSON.stringify({ order_id: parseInt($json.json.orderId), get_unconfirmed_orders: false }) }}"}]},"options":{"timeout":30000}},"id":"get_orders_batch","name":"Get Orders by IDs","type":"n8n-nodes-base.httpRequest","position":[1200,-32],"typeVersion":4.2,"retryOnFail":true,"maxTries":5,"waitBetweenTries":5000},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":1},"conditions":[{"leftValue":"={{ $json.logs?.length }}","rightValue":0,"operator":{"type":"number","operation":"gt"},"id":"fb5b9b41-36c6-408f-a7ce-41f8c97e3146"}],"combinator":"and"},"options":{}},"id":"check_changes","name":"Check If Changes Exist","type":"n8n-nodes-base.if","position":[304,-176],"typeVersion":2},{"parameters":{},"id":"no_changes","name":"No Changes - Stop","type":"n8n-nodes-base.noOp","position":[528,112],"typeVersion":1},{"parameters":{"jsCode":"// Extract Product IDs - WERSJA Z JEDNYM OUTPUTEM I RÓŻNYMI TYPAMI\nconst items = $input.all();\nconst allProductIds = new Set();\nconst allOrders = [];\n\n// Aggregate node zwraca wszystko w polu \"data\"\nconst aggregatedData = items[0]?.json?.data;\n\nif (aggregatedData && Array.isArray(aggregatedData)) {\n  // Sprawdź strukturę danych z Aggregate\n  aggregatedData.forEach(item => {\n    // Jeśli każdy item ma pole \"orders\" (batche z BaseLinker)\n    if (item.orders && Array.isArray(item.orders)) {\n      allOrders.push(...item.orders);\n    } \n    // Jeśli to bezpośrednio zamówienia (spłaszczone)\n    else if (item.order_id) {\n      allOrders.push(item);\n    }\n    // Jeśli to zagnieżdżone w json\n    else if (item.json?.orders) {\n      allOrders.push(...item.json.orders);\n    }\n    else if (item.json?.order_id) {\n      allOrders.push(item.json);\n    }\n  });\n} \n// Fallback - może dane są bezpośrednio w items\nelse {\n  items.forEach(item => {\n    if (item.json?.orders) {\n      allOrders.push(...item.json.orders);\n    } else if (item.json?.order_id) {\n      allOrders.push(item.json);\n    }\n  });\n}\n\n// Zbierz wszystkie unikalne product IDs\nallOrders.forEach(order => {\n  if (order.products && Array.isArray(order.products)) {\n    order.products.forEach(product => {\n      if (product.product_id) {\n        allProductIds.add(parseInt(product.product_id));\n      }\n    });\n  }\n});\n\nconst productIdsArray = Array.from(allProductIds);\n\n// Podziel produkty na batche po 100\nconst BATCH_SIZE = 100;\nconst productBatches = [];\nfor (let i = 0; i < productIdsArray.length; i += BATCH_SIZE) {\n  productBatches.push(productIdsArray.slice(i, i + BATCH_SIZE));\n}\n\nconsole.log('=== EXTRACT PRODUCT IDS ===');\nconsole.log(`Total orders: ${allOrders.length}`);\nconsole.log(`Unique products: ${productIdsArray.length}`);\nconsole.log(`Product batches: ${productBatches.length} (${BATCH_SIZE} per batch)`);\nproductBatches.forEach((batch, index) => {\n  console.log(`  Batch ${index + 1}: ${batch.length} products`);\n});\n\n// Przygotuj output items\nconst outputItems = [];\n\n// PIERWSZY ITEM: Zamówienia (dla Merge)\noutputItems.push({\n  json: {\n    type: 'ordersData',  // Unikalny typ dla Switch\n    orders: allOrders,\n    stats: {\n      batchOrderCount: allOrders.length,\n      uniqueProductCount: allProductIds.size,\n      totalBatches: productBatches.length,\n      avgProductsPerOrder: (allOrders.reduce((sum, o) => sum + (o.products?.length || 0), 0) / allOrders.length).toFixed(2)\n    }\n  }\n});\n\n// KOLEJNE ITEMY: Każdy batch produktów (dla Loop/Get Product Brands)\nproductBatches.forEach((batch, index) => {\n  outputItems.push({\n    json: {\n      type: 'productBatch',  // Unikalny typ dla Switch\n      batchIndex: index + 1,\n      totalBatches: productBatches.length,\n      productIdsToEnrich: batch,\n      batchSize: batch.length\n    }\n  });\n});\n\nconsole.log(`Returning ${outputItems.length} items: 1 ordersData + ${productBatches.length} productBatch(es)`);\n\nreturn outputItems;"},"id":"extract_product_ids","name":"Extract Product IDs","type":"n8n-nodes-base.code","typeVersion":2,"position":[1200,-272]},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getInventoryProductsData"},{"name":"parameters","value":"={{ JSON.stringify({ \n  inventory_id: 758, \n  products: $json.productIdsToEnrich || [] \n}) }}"}]},"options":{"timeout":30000}},"id":"get_product_brands","name":"Get Product Brands","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1872,-56],"retryOnFail":true},{"parameters":{"jsCode":"// FIXED: Merge Product Data - Correctly extracts data from Merge node\n// This version properly handles the 4 inputs from the Merge node\n\nconst allInputs = $input.all();\nconsole.log(`=== MERGE PRODUCT DATA - DEBUGGING ===`);\nconsole.log(`Total inputs received: ${allInputs.length}`);\n\n// Initialize data storage\nlet manufacturersData = [];\nlet ordersData = [];\nlet productsData = {};\nlet statusesData = [];\n\n// The Merge node combines 4 inputs:\n// Input 0: Product Manufacturers\n// Input 1: Orders (from Switch - ordersData branch)  \n// Input 2: Aggregated Products\n// Input 3: Order Status List\n\n// Process each input and extract the correct data\nallInputs.forEach((item, index) => {\n  const data = item.json;\n  \n  console.log(`\\nInput ${index} analysis:`, {\n    hasManufacturers: !!(data.manufacturers || data.manufacturer),\n    hasOrders: !!(data.orders || data.type === 'ordersData'),\n    hasProducts: !!(data.products || data.data),\n    hasStatuses: !!(data.statuses || data.status),\n    topKeys: Object.keys(data).slice(0, 5),\n    dataType: data.type || 'unknown'\n  });\n  \n  // Extract manufacturers (usually first input from Get Product Manufacturers)\n  if (data.manufacturers && Array.isArray(data.manufacturers)) {\n    manufacturersData = data.manufacturers;\n    console.log(`✓ Found ${manufacturersData.length} manufacturers`);\n  }\n  \n  // Extract orders (from ordersData type)\n  if (data.type === 'ordersData' && data.orders) {\n    ordersData = data.orders;\n    console.log(`✓ Found ${ordersData.length} orders from ordersData`);\n  } else if (data.orders && Array.isArray(data.orders)) {\n    ordersData = data.orders;\n    console.log(`✓ Found ${ordersData.length} orders`);\n  }\n  \n  // Extract products from aggregated data\n  if (data.data && Array.isArray(data.data)) {\n    // This is the aggregated products output\n    console.log(`✓ Found aggregated data with ${data.data.length} items`);\n    data.data.forEach(item => {\n      if (item.products && typeof item.products === 'object') {\n        Object.assign(productsData, item.products);\n      }\n    });\n  } else if (data.products && typeof data.products === 'object') {\n    // Direct products object\n    Object.assign(productsData, data.products);\n  }\n  \n  // Extract statuses\n  if (data.statuses && Array.isArray(data.statuses)) {\n    statusesData = data.statuses;\n    console.log(`✓ Found ${statusesData.length} order statuses`);\n  }\n});\n\n// Create manufacturer mapping (ID → Name)\nconst manufacturerMap = {};\nmanufacturersData.forEach(manufacturer => {\n  const id = String(manufacturer.manufacturer_id);\n  manufacturerMap[id] = manufacturer.name;\n});\n\nconsole.log(`\\n=== DATA SUMMARY ===`);\nconsole.log(`Manufacturers: ${Object.keys(manufacturerMap).length}`);\nconsole.log(`Sample manufacturers:`, Object.entries(manufacturerMap).slice(0, 5));\nconsole.log(`Products with brand data: ${Object.keys(productsData).length}`);\nconsole.log(`Orders to process: ${ordersData.length}`);\nconsole.log(`Statuses: ${statusesData.length}`);\n\n// Create status mapping\nconst statusMap = {};\nstatusesData.forEach(status => {\n  statusMap[String(status.id)] = status.name;\n});\n\n// Statistics\nlet stats = {\n  totalProducts: 0,\n  productsWithBrand: 0,\n  productsWithoutBrand: 0,\n  missingProductData: 0,\n  uniqueBrands: new Set(),\n  missingProductIds: new Set()\n};\n\n// Process and enrich orders\nconst enrichedOrders = ordersData.map((order, orderIndex) => {\n  // Clone the order to avoid mutation\n  const enrichedOrder = JSON.parse(JSON.stringify(order));\n  \n  // Add status name\n  enrichedOrder.status_name = statusMap[String(order.order_status_id)] || 'Unknown';\n  \n  // Process products in the order\n  if (enrichedOrder.products && Array.isArray(enrichedOrder.products)) {\n    enrichedOrder.products = enrichedOrder.products.map((product, productIndex) => {\n      stats.totalProducts++;\n      \n      const productIdStr = String(product.product_id);\n      const productData = productsData[productIdStr];\n      \n      // Initialize brand as Unknown\n      let brandName = 'Unknown';\n      let categoryName = '';\n      let ean = product.ean || '';\n      \n      if (productData) {\n        // Get manufacturer/brand name\n        if (productData.manufacturer_id) {\n          const manufacturerIdStr = String(productData.manufacturer_id);\n          const manufacturer = manufacturerMap[manufacturerIdStr];\n          \n          if (manufacturer) {\n            brandName = manufacturer;\n            stats.uniqueBrands.add(brandName);\n            stats.productsWithBrand++;\n          } else {\n            console.warn(`Manufacturer ID ${manufacturerIdStr} not found in map for product ${productIdStr}`);\n            stats.productsWithoutBrand++;\n          }\n        } else {\n          stats.productsWithoutBrand++;\n        }\n        \n        // Get category from text_fields\n        if (productData.text_fields) {\n          categoryName = productData.text_fields.extra_field_851 || '';\n        }\n        \n        // Get EAN\n        ean = productData.ean || product.ean || '';\n        \n      } else {\n        stats.missingProductData++;\n        stats.missingProductIds.add(productIdStr);\n        \n        if (orderIndex < 3 && productIndex < 2) {\n          console.warn(`No product data found for ID: ${productIdStr}`);\n        }\n      }\n      \n      // Debug first few products\n      if (orderIndex === 0 && productIndex < 3) {\n        console.log(`\\nSample enrichment - Product ${productIdStr}:`, {\n          originalName: product.name,\n          brand: brandName,\n          hasProductData: !!productData,\n          manufacturerId: productData?.manufacturer_id,\n          category: categoryName ? categoryName.substring(0, 50) + '...' : 'None'\n        });\n      }\n      \n      // Return enriched product\n      return {\n        ...product,\n        brand: brandName,\n        category_name: categoryName,\n        ean: ean\n      };\n    });\n  }\n  \n  return enrichedOrder;\n});\n\n// Calculate coverage\nconst coverage = stats.totalProducts > 0 \n  ? ((stats.productsWithBrand / stats.totalProducts) * 100).toFixed(1)\n  : 0;\n\nconsole.log(`\\n=== ENRICHMENT RESULTS ===`);\nconsole.log(`Total products: ${stats.totalProducts}`);\nconsole.log(`Products with brands: ${stats.productsWithBrand} (${coverage}%)`);\nconsole.log(`Products without brands: ${stats.productsWithoutBrand}`);\nconsole.log(`Products missing data: ${stats.missingProductData}`);\nconsole.log(`Unique brands found: ${stats.uniqueBrands.size}`);\nconsole.log(`Brand names:`, Array.from(stats.uniqueBrands).slice(0, 10).join(', '));\n\nif (stats.missingProductData > 0) {\n  console.warn(`\\n⚠️ Missing product IDs (first 10):`, \n    Array.from(stats.missingProductIds).slice(0, 10).join(', '));\n}\n\n// Return results\nconst output = [];\n\n// Add statistics as first item\noutput.push({\n  json: {\n    _type: 'statistics',\n    _timestamp: new Date().toISOString(),\n    coverage: {\n      total_products: stats.totalProducts,\n      with_brand: stats.productsWithBrand,\n      without_brand: stats.productsWithoutBrand,\n      missing_data: stats.missingProductData,\n      coverage_percent: `${coverage}%`,\n      unique_brands: stats.uniqueBrands.size\n    },\n    brands: Array.from(stats.uniqueBrands),\n    missing_ids: Array.from(stats.missingProductIds).slice(0, 20)\n  }\n});\n\n// Add enriched orders\nenrichedOrders.forEach(order => {\n  output.push({ json: order });\n});\n\nreturn output;"},"id":"merge_product_data","name":"Merge Product Data","type":"n8n-nodes-base.code","typeVersion":2,"position":[2320,-440]},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getInventoryManufacturers"},{"name":"parameters","value":"{\"inventory_id\":758}"}]},"options":{"timeout":30000}},"id":"c5dda8bd-6bde-4b69-951b-1be765ced825","name":"Get Product Manufacturers","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1872,-704],"retryOnFail":true},{"parameters":{"mode":"combine","combineBy":"combineByPosition","numberInputs":4,"options":{}},"type":"n8n-nodes-base.merge","typeVersion":3.2,"position":[2096,-472],"id":"02667abe-006e-4583-8357-2c78e454af1e","name":"Merge"},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getOrderStatusList"},{"name":"parameters","value":"{}"}]},"options":{}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1872,256],"id":"1f5c5d4d-7fb4-4810-97f5-aba0bf10d275","name":"Get Order Status List","retryOnFail":true},{"parameters":{"jsCode":"// Konfiguracja dozwolonych statusów - zgodnie z wymaganiami klienta\n// Tylko statusy końcowe: zrealizowane, anulowane, zwroty\n\nconst allowedStatuses = [\n  // === WYSŁANE/ZREALIZOWANE/DOSTARCZONE ===\n  300027, // WYSŁANE // .PL\n  300028, // WYSŁANE // _PL\n  361867, // DE // Wysłane\n  386333, // SM // WYSŁANE\n  354789, // Dostarczone\n  354788, // Paczka w punkcie\n  300828, // OO // Odebrane\n  \n  // === ANULOWANE ===\n  301588, // Anulowane (przez klienta)\n  302472, // Anulowane (brak wpłaty)\n  302473, // Anulowane (zwrot paczki)\n  302475, // Anulowane (brak towaru)\n  303313, // Anulowane STA\n  304745, // Anulacja przed wysyłką\n  316385, // Anulacje (testowe)\n  359429, // WMS Anulowane\n  302981, // [Optima] Anulacja RO\n  \n  // === ZWROTY ===\n  300346, // ZWR // Przedpłacone\n  303161, // ZWR // Deklaracja zwrotu\n  311537, // SOK // Zwroty paczek\n  354792, // Zwrot kurierski\n  372310, // MolZwrotPrzyjęty [sok] zwrot$\n  374054, // Zwroty MADS (Hebe i Mitago_EU)\n  374055, // Zwroty MAGU Kau+Pigu\n  356655, // [MOL] Zwroty częściowe\n  356674, // EXC zwrócone\n  \n  // === NIEDOSTARCZONE (może być traktowane jako anulowane) ===\n  354791  // Niedostarczone\n];\n\n// Mapowanie kategorii statusów dla lepszego raportowania\nconst statusCategories = {\n  'ZREALIZOWANE': [300027, 300028, 361867, 386333, 354789, 354788, 300828],\n  'ANULOWANE': [301588, 302472, 302473, 302475, 303313, 304745, 316385, 359429, 302981],\n  'ZWROTY': [300346, 303161, 311537, 354792, 372310, 374054, 374055, 356655, 356674],\n  'NIEDOSTARCZONE': [354791]\n};\n\n// Pobierz wszystkie zamówienia\nconst items = $input.all();\n\nconsole.log(\"=== FILTER ORDER STATUS ===\");\nconsole.log(`Total orders received: ${items.length}`);\n\n// Zbierz statystyki\nconst statusStats = {};\nconst categoryStats = {\n  'ZREALIZOWANE': 0,\n  'ANULOWANE': 0,\n  'ZWROTY': 0,\n  'NIEDOSTARCZONE': 0,\n  'OTHER': 0\n};\n\nlet passedCount = 0;\nlet filteredCount = 0;\n\n// Filtruj zamówienia\nconst filteredOrders = items.filter(item => {\n  const order = item.json;\n  const statusId = parseInt(order.order_status_id);\n  const statusName = order.status_name || 'Unknown';\n  \n  // Zbierz statystyki szczegółowe\n  const key = `${statusId} - ${statusName}`;\n  statusStats[key] = (statusStats[key] || 0) + 1;\n  \n  // Określ kategorię statusu\n  let category = 'OTHER';\n  for (const [cat, ids] of Object.entries(statusCategories)) {\n    if (ids.includes(statusId)) {\n      category = cat;\n      break;\n    }\n  }\n  categoryStats[category]++;\n  \n  // Sprawdź czy status jest dozwolony\n  const isAllowed = allowedStatuses.includes(statusId);\n  \n  if (isAllowed) {\n    passedCount++;\n    console.log(`  ✅ Order ${order.order_id}: Status ${statusId} (${statusName}) - ${category}`);\n  } else {\n    filteredCount++;\n    if (filteredCount <= 5) { // Pokaż tylko pierwsze 5 odrzuconych\n      console.log(`  ❌ Order ${order.order_id}: Status ${statusId} (${statusName}) - FILTERED OUT`);\n    }\n  }\n  \n  return isAllowed;\n});\n\n// Raport podsumowujący\nconsole.log(\"\\n=== STATUS CATEGORY SUMMARY ===\");\nconsole.log(`📦 Zrealizowane: ${categoryStats.ZREALIZOWANE} orders`);\nconsole.log(`❌ Anulowane: ${categoryStats.ANULOWANE} orders`);\nconsole.log(`↩️ Zwroty: ${categoryStats.ZWROTY} orders`);\nconsole.log(`⚠️ Niedostarczone: ${categoryStats.NIEDOSTARCZONE} orders`);\nconsole.log(`❓ Inne (odrzucone): ${categoryStats.OTHER} orders`);\n\nconsole.log(\"\\n=== DETAILED STATUS DISTRIBUTION ===\");\n// Sortuj statusy według liczby zamówień\nconst sortedStats = Object.entries(statusStats)\n  .sort((a, b) => b[1] - a[1])\n  .slice(0, 10); // Pokaż top 10\n\nsortedStats.forEach(([status, count]) => {\n  const statusId = parseInt(status.split(' ')[0]);\n  const allowed = allowedStatuses.includes(statusId) ? '✅' : '❌';\n  \n  // Znajdź kategorię\n  let category = 'OTHER';\n  for (const [cat, ids] of Object.entries(statusCategories)) {\n    if (ids.includes(statusId)) {\n      category = cat;\n      break;\n    }\n  }\n  \n  console.log(`  ${allowed} ${status}: ${count} orders [${category}]`);\n});\n\nconsole.log(`\\n=== FINAL SUMMARY ===`);\nconsole.log(`✅ Passed to Synerise: ${passedCount} orders`);\nconsole.log(`❌ Filtered out: ${filteredCount} orders`);\nconsole.log(`📊 Filter efficiency: ${((passedCount/items.length)*100).toFixed(1)}%`);\n\n// Ostrzeżenie jeśli wszystko odrzucone\nif (passedCount === 0 && items.length > 0) {\n  console.error(\"⚠️ WARNING: All orders were filtered out! Check status configuration.\");\n}\n\n// WAŻNE: Zwróć przefiltrowane zamówienia jako items\nreturn filteredOrders;"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[2544,-440],"id":"b86b7a29-5237-4232-af23-adac098b11e6","name":"Filter Order Status"},{"parameters":{"jsCode":"// Generate CSV with semicolon delimiter (16 columns)\n// UPDATED: Changed from 23 columns (comma) to 16 columns (semicolon)\nconst input = $input.all()[0].json;\nconst csvRows = input.data;\n\n// 16 columns (removed: eventSalt, clientName, clientPhone, productCategory, productEan, source, notes)\nconst headers = [\n  'orderId',\n  'recordedAt',\n  'clientEmail',\n  'status',\n  'statusName',\n  'totalAmount',\n  'currency',\n  'productSku',\n  'productName',\n  'productBrand',\n  'productPrice',\n  'productQuantity',\n  'paymentMethod',\n  'paymentType',\n  'deliveryMethod',\n  'deliveryPrice'\n];\n\n// Function to escape CSV values for semicolon delimiter\nfunction escapeCsvValue(value) {\n  if (value === null || value === undefined) return '';\n  const stringValue = String(value);\n  \n  // If contains semicolon, double quote, or newline, wrap in quotes\n  if (stringValue.includes(';') || stringValue.includes('\"') || stringValue.includes('\\n')) {\n    return `\"${stringValue.replace(/\"/g, '\"\"')}\"`;\n  }\n  return stringValue;\n}\n\n// Generate CSV with SEMICOLON delimiter\nlet csvContent = headers.join(';') + '\\n';\n\n// Add data rows\ncsvRows.forEach(row => {\n  const values = headers.map(header => escapeCsvValue(row[header]));\n  csvContent += values.join(';') + '\\n';\n});\n\nconsole.log(`Generated CSV: ${csvRows.length} rows, 16 columns, semicolon delimiter`);\n\n// Return as binary data\nreturn {\n  json: {\n    filename: input.filename,\n    rowCount: csvRows.length,\n    columnCount: headers.length,\n    delimiter: 'semicolon'\n  },\n  binary: {\n    data: {\n      data: Buffer.from(csvContent, 'utf8').toString('base64'),\n      mimeType: 'text/csv',\n      fileName: input.filename\n    }\n  }\n};"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[3216,-440],"id":"f78311b1-4880-45f1-83cb-b134a3eea978","name":"Code in JavaScript"},{"parameters":{"jsCode":"// Extract unique order IDs from journal\nconst items = $input.all();\nconst logs = items[0]?.json?.logs || [];\n\n// Get unique order IDs\nconst orderIds = [...new Set(logs.map(log => log.order_id).filter(id => id))];\n\n// Przygotuj outputy\nlet output0 = []; // Order IDs do przetworzenia\nlet output1 = []; // Dane do Google Sheets update\n\nif (logs.length > 0) {\n  // Znajdź log z najwyższą datą (najświeższy)\n  const newestLog = logs.reduce((newest, current) => {\n    return current.date > newest.date ? current : newest;\n  }, logs[0]);\n  \n  const newLastLogId = newestLog.log_id;\n  const newestDate = new Date(newestLog.date * 1000).toISOString();\n  \n  console.log(`Found ${logs.length} logs, ${orderIds.length} unique orders`);\n  console.log(`Newest log_id: ${newLastLogId} from date: ${newestDate}`);\n  console.log(`Newest log details:`, newestLog);\n  \n  // Output 0: Order IDs do przetworzenia\n  output0 = orderIds.map((orderId, index) => ({\n    json: {\n      orderId: orderId,\n      totalCount: orderIds.length,\n      currentIndex: index + 1\n    }\n  }));\n  \n  // Output 1: Dane do update Google Sheets (4 wiersze - dodałem newest_date)\n  output1 = [\n    {\n      json: {\n        key: 'last_log_id',\n        value: newLastLogId.toString(),\n        row_number: 2\n      }\n    },\n    {\n      json: {\n        key: 'last_sync',\n        value: new Date().toISOString(),\n        row_number: 3\n      }\n    },\n    {\n      json: {\n        key: 'total_orders',\n        value: orderIds.length.toString(),\n        row_number: 4\n      }\n    },\n    {\n      json: {\n        key: 'newest_log_date',\n        value: newestDate,\n        row_number: 5\n      }\n    }\n  ];\n} else {\n  console.log(\"No new orders to process\");\n}\n\n// Zwróć dane dla obu outputów\nreturn {\n  0: output0,  // Idzie do Loop Over Items\n  1: output1   // Idzie do Google Sheets Update\n};"},"id":"extract_order_ids","name":"Extract Order IDs & Prepare Update","type":"n8n-nodes-base.code","position":[528,-272],"typeVersion":2},{"parameters":{"fieldToSplitOut":"[\"0\"]","options":{}},"type":"n8n-nodes-base.splitOut","typeVersion":1,"position":[752,-80],"id":"bd9dc77a-8626-42cb-8d89-7029d5957980","name":"Split Out1"},{"parameters":{"assignments":{"assignments":[{"id":"89673c3e-0d1b-4e40-a3b7-87bc1d14ac25","name":"1","value":"={{ $json['1'] }}","type":"array"}]},"options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[752,-992],"id":"ab3d3a59-bf28-44ff-9bf3-2b2385af2e63","name":"Edit Fields"},{"parameters":{"options":{}},"type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[976,-80],"id":"35f888c4-1fd5-4f9f-83b0-f4b5d86f2f2b","name":"Loop Over Items"},{"parameters":{"jsCode":"// Inicjalizacja pętli - FIXED for Data Table compatibility\nconst staticData = $getWorkflowStaticData('global');\n\n// Data Table returns rows with {key, value, id, createdAt, updatedAt}\n// We need to find the specific row where key === 'last_log_id'\nconst items = $input.all();\nconst lastLogIdRow = items.find(item => item.json.key === 'last_log_id');\nconst lastLogId = lastLogIdRow ? lastLogIdRow.json.value : null;\n\nconst maxIterations = 50; // Zabezpieczenie przed nieskończoną pętlą\n\nconsole.log('=== STATE INITIALIZATION ===');\nconsole.log(`Found last_log_id row:`, lastLogIdRow ? 'YES' : 'NO');\nconsole.log(`Last log ID value: ${lastLogId}`);\nconsole.log(`Total state rows: ${items.length}`);\n\nreturn [{\n  json: {\n    lastLogId: lastLogId,\n    iteration: 0,\n    maxIterations: maxIterations,\n    continue: true,\n    cutoffTimestamp: Math.floor(Date.now() / 1000) - (24 * 60 * 60) // 24h temu\n  }\n}];"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[-816,-176],"id":"5f9815c0-2159-4208-adf0-078fde06e7be","name":"Code in JavaScript2"},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getJournalList"},{"name":"parameters","value":"={{ JSON.stringify({ logs_types: [1, 18], last_log_id: $json.lastLogId }) }}"}]},"options":{"timeout":30000}},"id":"8936a624-f84b-49a6-a2ab-149c91788642","name":"Get Journal Changes2","type":"n8n-nodes-base.httpRequest","position":[-592,-176],"typeVersion":4.2},{"parameters":{"jsCode":"// Sprawdź czy dotarliśmy do świeżych logów\nconst logs = $json.logs || [];\nconst iteration = $json.iteration || 0;\nconst maxIterations = $json.maxIterations || 50;\n\n// ZMIANA: Ustaw cutoffTimestamp na 1 godzinę temu\nconst cutoffTimestamp = $json.cutoffTimestamp || (Math.floor(Date.now() / 1000) - (360 * 60)); // 1h temu\n\nif (logs.length === 0) {\n  // Brak więcej logów - zakończ\n  return [{\n    json: {\n      continue: false,\n      reason: \"No more logs\",\n      ordersToProcess: [],\n      lastProcessedDate: null,\n      lastLogId: null\n    }\n  }];\n}\n\n// Bezpieczne znajdowanie min/max z walidacją\nconst validLogs = logs.filter(log => \n  log && \n  log.date && !isNaN(log.date) && \n  log.log_id && !isNaN(log.log_id)\n);\n\nif (validLogs.length === 0) {\n  console.error(\"No valid logs found:\", logs);\n  return [{\n    json: {\n      continue: false,\n      reason: \"Invalid log data\",\n      ordersToProcess: [],\n      error: \"No valid logs with date and log_id\"\n    }\n  }];\n}\n\n// Znajdź log z najnowszą datą (największy timestamp)\nconst newestLog = validLogs.reduce((newest, current) => {\n  return current.date > newest.date ? current : newest;\n}, validLogs[0]);\n\n// Znajdź log z najstarszą datą \nconst oldestLog = validLogs.reduce((oldest, current) => {\n  return current.date < oldest.date ? current : oldest;\n}, validLogs[0]);\n\nconst oldestTimestamp = oldestLog.date;\nconst newestTimestamp = newestLog.date;\nconst newestLogId = newestLog.log_id;\n\n// Konwersja na daty\nconst oldestDate = new Date(oldestTimestamp * 1000);\nconst newestDate = new Date(newestTimestamp * 1000);\nconst cutoffDate = new Date(cutoffTimestamp * 1000);\nconst currentDate = new Date();\n\n// KLUCZOWA LOGIKA: Sprawdź czy najstarszy log jest w ciągu ostatniej godziny\nconst isRecent = oldestTimestamp >= cutoffTimestamp;\n\n// Filtruj tylko logi z ostatniej godziny\nconst recentLogs = validLogs.filter(log => log.date >= cutoffTimestamp);\nconst orderIds = [...new Set(recentLogs.map(log => log.order_id).filter(id => id))];\n\n// Oblicz różnicę czasu dla lepszego debugowania\nconst timeDiff = (currentDate.getTime() - oldestDate.getTime()) / 1000; // w sekundach\nconst timeDiffMinutes = Math.round(timeDiff / 60);\nconst timeDiffFormatted = timeDiffMinutes < 60 ? \n  `${timeDiffMinutes} minut temu` : \n  `${Math.round(timeDiffMinutes / 60)} godzin ${timeDiffMinutes % 60} minut temu`;\n\n// Debug info\nconsole.log({\n  iteration: iteration,\n  logsCount: logs.length,\n  validLogsCount: validLogs.length,\n  currentTime: currentDate.toISOString(),\n  oldestDate: oldestDate.toISOString(),\n  newestDate: newestDate.toISOString(),\n  cutoffDate: cutoffDate.toISOString(),\n  oldestLogAge: timeDiffFormatted,\n  newestLogId: newestLogId,\n  isRecent: isRecent,\n  recentLogsCount: recentLogs.length,\n  uniqueOrdersCount: orderIds.length,\n  shouldContinue: !isRecent && iteration < maxIterations\n});\n\n// LOGIKA DECYZYJNA - szukamy danych z ostatniej GODZINY\nif (isRecent || iteration >= maxIterations) {\n  // ZAKOŃCZ tylko gdy:\n  // 1. Znaleźliśmy dane z ostatniej godziny (isRecent = true)\n  // 2. LUB osiągnęliśmy limit iteracji\n  \n  console.log(`STOPPING: isRecent=${isRecent}, iteration=${iteration}/${maxIterations}`);\n  console.log(`Found ${recentLogs.length} logs from last hour with ${orderIds.length} unique orders`);\n  \n  // Zapisz stan\n  const staticData = $getWorkflowStaticData('global');\n  staticData.lastLogId = newestLogId;\n  staticData.lastProcessedTimestamp = newestTimestamp;\n  staticData.lastProcessedDate = newestDate.toISOString();\n  \n  return [{\n    json: {\n      continue: false, // STOP\n      ordersToProcess: orderIds,\n      logsProcessed: recentLogs.length,\n      lastLogId: newestLogId,\n      lastProcessedTimestamp: newestTimestamp,\n      lastProcessedDate: newestDate.toISOString(),\n      oldestDateInBatch: oldestDate.toISOString(),\n      newestDateInBatch: newestDate.toISOString(),\n      timePeriod: \"Last 1 hour\", // Informacja o okresie czasu\n      newestLogDetails: {\n        log_id: newestLog.log_id,\n        order_id: newestLog.order_id,\n        log_type: newestLog.log_type,\n        date_timestamp: newestLog.date,\n        date_iso: newestDate.toISOString()\n      },\n      reachedLimit: iteration >= maxIterations,\n      totalIterations: iteration,\n      cutoffUsed: cutoffDate.toISOString(),\n      isRecent: isRecent,\n      stopReason: isRecent ? \"Found logs from last hour\" : \"Reached iteration limit\",\n      summary: `Processed ${recentLogs.length} logs from last hour containing ${orderIds.length} unique orders`\n    }\n  }];\n} else {\n  // KONTYNUUJ - nie znaleźliśmy jeszcze danych z ostatniej godziny\n  console.log(`CONTINUING: Looking for logs after ${cutoffDate.toISOString()} (last hour)`);\n  console.log(`Current oldest: ${oldestDate.toISOString()} (${timeDiffFormatted}), need to go deeper...`);\n  \n  return [{\n    json: {\n      continue: true, // KONTYNUUJ PĘTLĘ\n      lastLogId: newestLogId, // Użyj najnowszy log_id jako punkt startowy dla następnej iteracji\n      iteration: iteration + 1,\n      maxIterations: maxIterations,\n      cutoffTimestamp: cutoffTimestamp,\n      partialOrders: orderIds, // Przekaż częściowe wyniki\n      dateRange: {\n        oldest: oldestDate.toISOString(),\n        newest: newestDate.toISOString(),\n        cutoff: cutoffDate.toISOString()\n      },\n      timePeriod: \"Last 1 hour\",\n      message: `Iteration ${iteration + 1}: Found ${logs.length} logs, oldest is ${timeDiffFormatted}, need logs from last hour`\n    }\n  }];\n}"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[-368,-248],"id":"129494d7-d610-4501-aaee-83fb1b699587","name":"Code in JavaScript3"},{"parameters":{"conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"conditions":[{"id":"fbeec7a0-b97b-4e8c-b5e4-057af2b9e4c7","leftValue":"={{ $json.continue }}","rightValue":"","operator":{"type":"boolean","operation":"true","singleValue":true}}],"combinator":"and"},"options":{}},"type":"n8n-nodes-base.if","typeVersion":2.2,"position":[-144,-176],"id":"3372a372-6500-4dbb-af4b-1dc8d2697902","name":"If"},{"parameters":{"options":{}},"type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[1648,-200],"id":"a153f10d-55bb-49a6-9c18-7b087b9bbc27","name":"Loop Over Items1"},{"parameters":{"rules":{"values":[{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"leftValue":"={{ $json.type }}","rightValue":"ordersData","operator":{"type":"string","operation":"equals"},"id":"16455973-160a-451f-9ce5-d3e00d4ec21c"}],"combinator":"and"},"renameOutput":true,"outputKey":"orders"},{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"id":"d0ff60de-b3c8-45e4-8d2e-b00c0acc1685","leftValue":"={{ $json.type }}","rightValue":"productBatch","operator":{"type":"string","operation":"equals","name":"filter.operator.equals"}}],"combinator":"and"},"renameOutput":true,"outputKey":"product"}]},"options":{}},"type":"n8n-nodes-base.switch","typeVersion":3.3,"position":[1424,-272],"id":"2eeef40d-3217-4583-9238-e45cdf92fd64","name":"Switch"},{"parameters":{"jsCode":"// Transform If Node Output to Journal Format\n// This replaces both \"Edit Fields1\" and \"Get Journal Changes\" nodes\n// Converts ordersToProcess array into a logs format expected by downstream nodes\n\nconst input = $input.first().json;\n\n// Extract data from the If node\nconst orderIds = input.ordersToProcess || [];\nconst lastLogId = input.lastLogId;\nconst lastProcessedTimestamp = input.lastProcessedTimestamp;\nconst lastProcessedDate = input.lastProcessedDate;\nconst logsProcessed = input.logsProcessed;\n\nconsole.log(\"=== TRANSFORM IF OUTPUT TO JOURNAL FORMAT ===\");\nconsole.log(`Converting ${orderIds.length} order IDs to logs format`);\nconsole.log(`Last Log ID: ${lastLogId}`);\nconsole.log(`Logs originally processed: ${logsProcessed}`);\n\n// Create synthetic logs from the order IDs\n// We need to create a logs array that looks like what the API would return\n// but with ALL the orders from the last hour, not just new ones\nconst logs = [];\n\n// Generate log entries for each order\n// We'll create one log entry per order (log_type 1 = order created/modified)\norderIds.forEach((orderId, index) => {\n  logs.push({\n    log_id: lastLogId - (orderIds.length - index - 1), // Create sequential log IDs\n    log_type: 1, // Type 1 = order event\n    order_id: orderId,\n    object_id: 0,\n    date: lastProcessedTimestamp // Use the timestamp from processing\n  });\n});\n\nconsole.log(`✅ Created ${logs.length} log entries from ${orderIds.length} orders`);\n\n// Create output in the same format as \"Get Journal Changes\" would return\nconst output = {\n  status: \"SUCCESS\",\n  logs: logs,\n  // Add metadata for tracking\n  _metadata: {\n    source: \"If_node_transformation\",\n    originalOrderCount: orderIds.length,\n    lastLogId: lastLogId,\n    lastProcessedDate: lastProcessedDate,\n    processedAt: new Date().toISOString()\n  }\n};\n\n// Log sample for debugging\nif (logs.length > 0) {\n  console.log(\"\\nSample log entries (first 3):\");\n  logs.slice(0, 3).forEach(log => {\n    console.log(`  Order ${log.order_id}, Log ID: ${log.log_id}`);\n  });\n  if (logs.length > 3) {\n    console.log(`  ... and ${logs.length - 3} more`);\n  }\n}\n\n// Return in the format expected by \"Check If Changes Exist\"\nreturn [{ json: output }];"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[80,-176],"id":"7ef1fd1a-8d85-4bea-ac79-b6f3ed3e3cde","name":"Code in JavaScript4"},{"parameters":{"aggregate":"aggregateAllItemData","options":{}},"type":"n8n-nodes-base.aggregate","typeVersion":1,"position":[1872,-248],"id":"494369fd-2f92-487d-914b-f62b31de2e2e","name":"Aggregate"},{"parameters":{"jsCode":"// Filter out marketplace orders - keep ONLY online shop orders\n// Marketplace IDs to EXCLUDE: 15156 (Allegro), 15157 (Erli), 15158 (eMag)\n\nconst MARKETPLACE_IDS_TO_EXCLUDE = [15156, 15157, 15158];\n\nconst items = $input.all();\nconst orders = items[0]?.json?.orders || [];\n\nconsole.log('=== FILTER ONLINE SHOP ORDERS ===');\nconsole.log(`Total orders before filtering: ${orders.length}`);\n\n// Statistics\nconst stats = {\n  total: orders.length,\n  onlineShop: 0,\n  marketplace: 0,\n  bySource: {}\n};\n\n// Filter orders\nconst filteredOrders = orders.filter(order => {\n  const orderSourceId = parseInt(order.order_source_id);\n  \n  // Track statistics\n  const sourceName = order.order_source || 'Unknown';\n  stats.bySource[sourceName] = (stats.bySource[sourceName] || 0) + 1;\n  \n  const isMarketplace = MARKETPLACE_IDS_TO_EXCLUDE.includes(orderSourceId);\n  \n  if (isMarketplace) {\n    stats.marketplace++;\n    console.log(`  ❌ EXCLUDED: Order ${order.order_id} from ${sourceName} (ID: ${orderSourceId})`);\n    return false;\n  } else {\n    stats.onlineShop++;\n    return true;\n  }\n});\n\nconsole.log('\\n=== FILTERING RESULTS ===');\nconsole.log(`✅ Online shop orders: ${stats.onlineShop}`);\nconsole.log(`❌ Marketplace orders (excluded): ${stats.marketplace}`);\nconsole.log(`📊 Filter efficiency: ${((stats.onlineShop/stats.total)*100).toFixed(1)}% kept`);\n\nconsole.log('\\n=== ORDERS BY SOURCE ===');\nObject.entries(stats.bySource).forEach(([source, count]) => {\n  console.log(`  ${source}: ${count} orders`);\n});\n\n// Return filtered orders in the same structure\nreturn [{\n  json: {\n    orders: filteredOrders,\n    _filterStats: stats,\n    _timestamp: new Date().toISOString()\n  }\n}];"},"id":"filter_shop_orders","name":"Filter Online Shop Orders","type":"n8n-nodes-base.code","typeVersion":2,"position":[1424,16]},{"parameters":{"operation":"get","dataTableId":{"__rl":true,"value":"ZLqPZuPvd8alE2nF","mode":"list","cachedResultName":"workflow_state_synerise","cachedResultUrl":"/projects/jlSr7IBJt4siEqiM/datatables/ZLqPZuPvd8alE2nF"},"returnAll":true},"type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[-1040,-176],"id":"07b192e4-4094-4e70-82da-6f63b22ed42d","name":"get state"},{"parameters":{"operation":"update","dataTableId":{"__rl":true,"value":"ZLqPZuPvd8alE2nF","mode":"list","cachedResultName":"workflow_state_synerise","cachedResultUrl":"/projects/jlSr7IBJt4siEqiM/datatables/ZLqPZuPvd8alE2nF"},"filters":{"conditions":[{"keyName":"key","keyValue":"={{ $json['1'][0].json.key }}"}]},"columns":{"mappingMode":"defineBelow","value":{"value":"={{ $json['1'][0].json.value }}"},"matchingColumns":[],"schema":[{"id":"key","displayName":"key","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":true},{"id":"value","displayName":"value","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":false}],"attemptToConvertTypes":false,"convertFieldsToString":false},"options":{}},"type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[976,-992],"id":"141ccff1-591f-4c54-b04a-bec2a0fea7dd","name":"Update row(s)"},{"parameters":{"operation":"update","dataTableId":{"__rl":true,"value":"ZLqPZuPvd8alE2nF","mode":"list","cachedResultName":"workflow_state_synerise","cachedResultUrl":"/projects/jlSr7IBJt4siEqiM/datatables/ZLqPZuPvd8alE2nF"},"filters":{"conditions":[{"keyName":"key","keyValue":"={{ $('Edit Fields').item.json['1'][1].json.key }}"}]},"columns":{"mappingMode":"defineBelow","value":{"value":"={{ $('Edit Fields').item.json['1'][1].json.value }}"},"matchingColumns":[],"schema":[{"id":"key","displayName":"key","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":true},{"id":"value","displayName":"value","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":false}],"attemptToConvertTypes":false,"convertFieldsToString":false},"options":{}},"type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[1200,-992],"id":"6cf4c1c6-bc18-4477-804e-565104921745","name":"Update row(s)1"},{"parameters":{"operation":"update","dataTableId":{"__rl":true,"value":"ZLqPZuPvd8alE2nF","mode":"list","cachedResultName":"workflow_state_synerise","cachedResultUrl":"/projects/jlSr7IBJt4siEqiM/datatables/ZLqPZuPvd8alE2nF"},"filters":{"conditions":[{"keyName":"key","keyValue":"={{ $('Edit Fields').item.json['1'][2].json.key }}"}]},"columns":{"mappingMode":"defineBelow","value":{"value":"={{ $('Edit Fields').item.json['1'][2].json.value }}"},"matchingColumns":[],"schema":[{"id":"key","displayName":"key","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":true},{"id":"value","displayName":"value","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":false}],"attemptToConvertTypes":false,"convertFieldsToString":false},"options":{}},"type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[1424,-992],"id":"2cb43758-e040-4b61-8195-459ec5a4cd2a","name":"Update row(s)2"},{"parameters":{"operation":"update","dataTableId":{"__rl":true,"value":"ZLqPZuPvd8alE2nF","mode":"list","cachedResultName":"workflow_state_synerise","cachedResultUrl":"/projects/jlSr7IBJt4siEqiM/datatables/ZLqPZuPvd8alE2nF"},"filters":{"conditions":[{"keyName":"key","keyValue":"={{ $('Edit Fields').item.json['1'][3].json.key }}"}]},"columns":{"mappingMode":"defineBelow","value":{"value":"={{ $('Edit Fields').item.json['1'][3].json.value }}"},"matchingColumns":[],"schema":[{"id":"key","displayName":"key","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":true},{"id":"value","displayName":"value","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":false}],"attemptToConvertTypes":false,"convertFieldsToString":false},"options":{}},"type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[1648,-992],"id":"57f5a418-5443-4c70-b546-65333a3b54e1","name":"Update row(s)3"}],"connections":{"Manual Trigger":{"main":[[{"node":"get state","type":"main","index":0}]]},"Schedule Trigger - Every 15 min":{"main":[[{"node":"get state","type":"main","index":0}]]},"Transform to Synerise Format":{"main":[[{"node":"Prepare CSV Data","type":"main","index":0}]]},"Prepare CSV Data":{"main":[[{"node":"Code in JavaScript","type":"main","index":0}]]},"Check If Changes Exist":{"main":[[{"node":"Extract Order IDs & Prepare Update","type":"main","index":0},{"node":"Get Product Manufacturers","type":"main","index":0},{"node":"Get Order Status List","type":"main","index":0}],[{"node":"No Changes - Stop","type":"main","index":0}]]},"Extract Product IDs":{"main":[[{"node":"Switch","type":"main","index":0}]]},"Get Product Brands":{"main":[[{"node":"Loop Over Items1","type":"main","index":0}]]},"Merge Product Data":{"main":[[{"node":"Filter Order Status","type":"main","index":0}]]},"Get Product Manufacturers":{"main":[[{"node":"Merge","type":"main","index":0}]]},"Get Order Status List":{"main":[[{"node":"Merge","type":"main","index":3}]]},"Merge":{"main":[[{"node":"Merge Product Data","type":"main","index":0}]]},"Filter Order Status":{"main":[[{"node":"Transform to Synerise Format","type":"main","index":0}]]},"Extract Order IDs & Prepare Update":{"main":[[{"node":"Split Out1","type":"main","index":0},{"node":"Edit Fields","type":"main","index":0}]]},"Split Out1":{"main":[[{"node":"Loop Over Items","type":"main","index":0}]]},"Edit Fields":{"main":[[{"node":"Update row(s)","type":"main","index":0}]]},"Loop Over Items":{"main":[[{"node":"Extract Product IDs","type":"main","index":0}],[{"node":"Get Orders by IDs","type":"main","index":0}]]},"Code in JavaScript2":{"main":[[{"node":"Get Journal Changes2","type":"main","index":0}]]},"Get Journal Changes2":{"main":[[{"node":"Code in JavaScript3","type":"main","index":0}]]},"Code in JavaScript3":{"main":[[{"node":"If","type":"main","index":0}]]},"If":{"main":[[{"node":"Get Journal Changes2","type":"main","index":0}],[{"node":"Code in JavaScript4","type":"main","index":0}]]},"Loop Over Items1":{"main":[[{"node":"Aggregate","type":"main","index":0}],[{"node":"Get Product Brands","type":"main","index":0}]]},"Switch":{"main":[[{"node":"Merge","type":"main","index":1}],[{"node":"Loop Over Items1","type":"main","index":0}]]},"Code in JavaScript4":{"main":[[{"node":"Check If Changes Exist","type":"main","index":0}]]},"Aggregate":{"main":[[{"node":"Merge","type":"main","index":2}]]},"Get Orders by IDs":{"main":[[{"node":"Filter Online Shop Orders","type":"main","index":0}]]},"Filter Online Shop Orders":{"main":[[{"node":"Loop Over Items","type":"main","index":0}]]},"get state":{"main":[[{"node":"Code in JavaScript2","type":"main","index":0}]]},"Update row(s)":{"main":[[{"node":"Update row(s)1","type":"main","index":0}]]},"Update row(s)1":{"main":[[{"node":"Update row(s)2","type":"main","index":0}]]},"Update row(s)2":{"main":[[{"node":"Update row(s)3","type":"main","index":0}]]}},"settings":{"saveManualExecutions":true,"executionOrder":"v1","callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":{"node:Schedule Trigger - Every 15 min":{"recurrenceRules":[]}},"meta":{"templateCredsSetupCompleted":true},"pinData":{},"versionId":"7ac2845b-1070-42c3-9e42-4e4cc0756a7b","versionCounter":4,"triggerCount":1,"shared":[{"updatedAt":"2025-09-25T14:46:56.372Z","createdAt":"2025-09-25T14:46:56.372Z","role":"workflow:owner","workflowId":"HMS43ySZOcEfGSyX","projectId":"jlSr7IBJt4siEqiM","project":{"updatedAt":"2025-01-31T18:30:45.852Z","createdAt":"2025-01-31T18:13:38.763Z","id":"jlSr7IBJt4siEqiM","name":"N8N Estyl <n8n@estyl.pl>","type":"personal","icon":null,"description":null,"projectRelations":[{"updatedAt":"2025-01-31T18:13:38.763Z","createdAt":"2025-01-31T18:13:38.763Z","userId":"5089e5a8-9975-48ff-8451-f5c3fb3e78ac","projectId":"jlSr7IBJt4siEqiM","user":{"updatedAt":"2025-11-13T23:00:06.270Z","createdAt":"2025-01-31T18:13:37.839Z","id":"5089e5a8-9975-48ff-8451-f5c3fb3e78ac","email":"n8n@estyl.pl","firstName":"N8N","lastName":"Estyl","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2025-01-31T18:30:49.691Z","personalization_survey_n8n_version":"1.76.1"},"settings":{"userActivated":true,"firstSuccessfulWorkflowId":"xbn9AqbzxO9Xbruw","userActivatedAt":1739378108329,"easyAIWorkflowOnboarded":true,"npsSurvey":{"responded":true,"lastShownAt":1758812569793}},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2025-11-13","isPending":false}}]}}],"tags":[]}

operations:
  [{"type":"updateNode","nodeName":"Transform to Synerise Format","updates":{"parameters.jsCode":"// Transform BaseLinker orders to Synerise CSV format\n// Mode: Run Once for All Items\n// FIXED: orderId now uses shop_order_id (short format) instead of order_id\n// FIXED: recordedAt now uses correct UTC timestamps (removed incorrect +2h offset)\n\nconst items = $input.all();\nconst results = [];\n\nconsole.log(`Processing ${items.length} orders for Synerise export`);\n\n// Map status using the enriched status_name\nconst statusMapping = {\n  'ZREALIZOWANE': 'completed',\n  'ANULOWANE': 'cancelled',\n  'ZWROT': 'returned',\n  'WYSŁANE': 'shipped'\n};\n\n// Process each order\nitems.forEach((item, index) => {\n  try {\n    const order = item.json;\n    \n    // Generate eventSalt\n    const eventSalt = `${order.order_id}_${order.date_add || Date.now()}`;\n    \n    // TIMEZONE FIX: BaseLinker returns date_confirmed as Unix timestamp in UTC\n    // No offset needed - UTC is the correct format for data systems\n    // Synerise will handle timezone conversion for display purposes\n    let recordedAt;\n    if (order.date_confirmed) {\n      const timestamp = parseInt(order.date_confirmed);\n      recordedAt = new Date(timestamp * 1000).toISOString();\n      \n      // Debug log for first order\n      if (index === 0) {\n        console.log(`Timestamp conversion example:`);\n        console.log(`  Unix timestamp: ${timestamp}`);\n        console.log(`  UTC ISO format: ${recordedAt}`);\n      }\n    } else {\n      recordedAt = new Date().toISOString();\n    }\n    \n    // Find appropriate status based on name\n    let orderStatus = 'unknown';\n    const statusNameUpper = (order.status_name || '').toUpperCase();\n    for (const [key, value] of Object.entries(statusMapping)) {\n      if (statusNameUpper.includes(key)) {\n        orderStatus = value;\n        break;\n      }\n    }\n    \n    // Determine payment type\n    const paymentType = order.payment_method_cod === '1' ? 'Pobranie' : 'Przelew';\n    \n    // Safe parseFloat\n    const safeParseFloat = (value) => {\n      const parsed = parseFloat(value);\n      return isNaN(parsed) ? 0 : parsed;\n    };\n    \n    // Extract client name with fallbacks\n    const clientName = order.delivery_fullname || \n                      order.invoice_fullname || \n                      order.delivery_company ||\n                      `${order.delivery_name || ''} ${order.delivery_surname || ''}`.trim() ||\n                      'Unknown Customer';\n    \n    // BUGFIX #1: Use shop_order_id (short format) instead of order_id\n    // shop_order_id examples: 184031, 184042 (human-readable short numbers)\n    // order_id examples: 790964970, 790932859 (internal BaseLinker IDs)\n    const orderId = order.shop_order_id || order.order_id; // Fallback to order_id if shop_order_id is missing\n    \n    // Debug log for first order to verify fix\n    if (index === 0) {\n      console.log(`Order ID fix example:`);\n      console.log(`  OLD (order_id): ${order.order_id}`);\n      console.log(`  NEW (shop_order_id): ${order.shop_order_id}`);\n      console.log(`  Using: ${orderId}`);\n    }\n    \n    // Add transformed order to results\n    results.push({\n      json: {\n        eventSalt: eventSalt,\n        orderId: orderId,\n        recordedAt: recordedAt, // FIXED: Now correct UTC format without offset\n        clientEmail: order.email || '',\n        clientName: clientName,\n        clientPhone: order.phone || '',\n        status: orderStatus,\n        statusName: order.status_name || '',\n        totalAmount: safeParseFloat(order.payment_done),\n        currency: order.currency || 'PLN',\n        paymentMethod: order.payment_method || '',\n        paymentType: paymentType,\n        deliveryMethod: order.delivery_method || '',\n        deliveryPrice: safeParseFloat(order.delivery_price),\n        products: order.products || [],\n        source: order.order_source || 'BaseLinker',\n        notes: order.admin_comments || ''\n      }\n    });\n    \n  } catch (error) {\n    console.error(`Error processing order at index ${index}:`, error);\n    console.error(`Order data:`, JSON.stringify(item.json).substring(0, 500));\n    \n    // Add failed order with error info\n    results.push({\n      json: {\n        error: true,\n        errorMessage: error.message,\n        orderId: item.json?.order_id || 'UNKNOWN',\n        originalData: item.json\n      }\n    });\n  }\n});\n\nconsole.log(`Successfully transformed ${results.length} orders`);\nconsole.log(`Sample order IDs: ${results.slice(0, 5).map(r => r.json.orderId).join(', ')}`);\n\n// Return all transformed items\nreturn results;"}}]


workflow_id: HMS43ySZOcEfGSyX
workflow_name: BaseLinker to Synerise CSV Export - Complete MVP
trigger: partial_update
created_at: 2025-11-14 16:08:10
workflow_snapshot:
  {"updatedAt":"2025-11-14T16:07:39.267Z","createdAt":"2025-09-25T14:46:56.372Z","id":"HMS43ySZOcEfGSyX","name":"BaseLinker to Synerise CSV Export - Complete MVP","active":false,"isArchived":false,"nodes":[{"parameters":{"rule":{"interval":[{"field":"minutes","minutesInterval":15}]}},"id":"schedule_trigger","name":"Schedule Trigger - Every 15 min","type":"n8n-nodes-base.scheduleTrigger","position":[-1264,-272],"typeVersion":1.2},{"parameters":{},"id":"manual_trigger","name":"Manual Trigger","type":"n8n-nodes-base.manualTrigger","position":[-1264,-80],"typeVersion":1},{"parameters":{"jsCode":"// Transform BaseLinker orders to Synerise CSV format\n// Mode: Run Once for All Items\n// FIXED: orderId now uses shop_order_id (short format) instead of order_id\n// FIXED: recordedAt now uses correct UTC timestamps (removed incorrect +2h offset)\n\nconst items = $input.all();\nconst results = [];\n\nconsole.log(`Processing ${items.length} orders for Synerise export`);\n\n// Map status using the enriched status_name\nconst statusMapping = {\n  'ZREALIZOWANE': 'completed',\n  'ANULOWANE': 'cancelled',\n  'ZWROT': 'returned',\n  'WYSŁANE': 'shipped'\n};\n\n// Process each order\nitems.forEach((item, index) => {\n  try {\n    const order = item.json;\n    \n    // Generate eventSalt\n    const eventSalt = `${order.order_id}_${order.date_add || Date.now()}`;\n    \n    // TIMEZONE FIX: BaseLinker returns date_confirmed as Unix timestamp in UTC\n    // No offset needed - UTC is the correct format for data systems\n    // Synerise will handle timezone conversion for display purposes\n    let recordedAt;\n    if (order.date_confirmed) {\n      const timestamp = parseInt(order.date_confirmed);\n      recordedAt = new Date(timestamp * 1000).toISOString();\n      \n      // Debug log for first order\n      if (index === 0) {\n        console.log(`Timestamp conversion example:`);\n        console.log(`  Unix timestamp: ${timestamp}`);\n        console.log(`  UTC ISO format: ${recordedAt}`);\n      }\n    } else {\n      recordedAt = new Date().toISOString();\n    }\n    \n    // Find appropriate status based on name\n    let orderStatus = 'unknown';\n    const statusNameUpper = (order.status_name || '').toUpperCase();\n    for (const [key, value] of Object.entries(statusMapping)) {\n      if (statusNameUpper.includes(key)) {\n        orderStatus = value;\n        break;\n      }\n    }\n    \n    // Determine payment type\n    const paymentType = order.payment_method_cod === '1' ? 'Pobranie' : 'Przelew';\n    \n    // Safe parseFloat\n    const safeParseFloat = (value) => {\n      const parsed = parseFloat(value);\n      return isNaN(parsed) ? 0 : parsed;\n    };\n    \n    // Extract client name with fallbacks\n    const clientName = order.delivery_fullname || \n                      order.invoice_fullname || \n                      order.delivery_company ||\n                      `${order.delivery_name || ''} ${order.delivery_surname || ''}`.trim() ||\n                      'Unknown Customer';\n    \n    // BUGFIX #1: Use shop_order_id (short format) instead of order_id\n    // shop_order_id examples: 184031, 184042 (human-readable short numbers)\n    // order_id examples: 790964970, 790932859 (internal BaseLinker IDs)\n    const orderId = order.shop_order_id || order.order_id; // Fallback to order_id if shop_order_id is missing\n    \n    // Debug log for first order to verify fix\n    if (index === 0) {\n      console.log(`Order ID fix example:`);\n      console.log(`  OLD (order_id): ${order.order_id}`);\n      console.log(`  NEW (shop_order_id): ${order.shop_order_id}`);\n      console.log(`  Using: ${orderId}`);\n    }\n    \n    // Add transformed order to results\n    results.push({\n      json: {\n        eventSalt: eventSalt,\n        orderId: orderId,\n        recordedAt: recordedAt, // FIXED: Now correct UTC format without offset\n        clientEmail: order.email || '',\n        clientName: clientName,\n        clientPhone: order.phone || '',\n        status: orderStatus,\n        statusName: order.status_name || '',\n        totalAmount: safeParseFloat(order.payment_done),\n        currency: order.currency || 'PLN',\n        paymentMethod: order.payment_method || '',\n        paymentType: paymentType,\n        deliveryMethod: order.delivery_method || '',\n        deliveryPrice: safeParseFloat(order.delivery_price),\n        products: order.products || [],\n        source: order.order_source || 'BaseLinker',\n        notes: order.admin_comments || ''\n      }\n    });\n    \n  } catch (error) {\n    console.error(`Error processing order at index ${index}:`, error);\n    console.error(`Order data:`, JSON.stringify(item.json).substring(0, 500));\n    \n    // Add failed order with error info\n    results.push({\n      json: {\n        error: true,\n        errorMessage: error.message,\n        orderId: item.json?.order_id || 'UNKNOWN',\n        originalData: item.json\n      }\n    });\n  }\n});\n\nconsole.log(`Successfully transformed ${results.length} orders`);\nconsole.log(`Sample order IDs: ${results.slice(0, 5).map(r => r.json.orderId).join(', ')}`);\n\n// Return all transformed items\nreturn results;"},"id":"transform_data","name":"Transform to Synerise Format","type":"n8n-nodes-base.code","position":[2768,-440],"typeVersion":2},{"parameters":{"jsCode":"// Flatten nested product data for CSV\n// UPDATED: 16 columns (removed: eventSalt, clientName, clientPhone, productCategory, productEan, source, notes)\nconst items = $input.all();\nconst csvRows = [];\n\nitems.forEach(item => {\n  const order = item.json;\n  \n  // If order has products, create a row for each product\n  if (order.products && order.products.length > 0) {\n    order.products.forEach(product => {\n      csvRows.push({\n        orderId: order.orderId,\n        recordedAt: order.recordedAt,\n        clientEmail: order.clientEmail,\n        status: order.status,\n        statusName: order.statusName,\n        totalAmount: order.totalAmount,\n        currency: order.currency,\n        productSku: product.sku || product.product_id,\n        productName: product.name,\n        productBrand: product.brand || 'Unknown',\n        productPrice: product.price_brutto,\n        productQuantity: product.quantity,\n        paymentMethod: order.paymentMethod,\n        paymentType: order.paymentType,\n        deliveryMethod: order.deliveryMethod,\n        deliveryPrice: order.deliveryPrice\n      });\n    });\n  } else {\n    // Order without products\n    csvRows.push({\n      orderId: order.orderId,\n      recordedAt: order.recordedAt,\n      clientEmail: order.clientEmail,\n      status: order.status,\n      statusName: order.statusName,\n      totalAmount: order.totalAmount,\n      currency: order.currency,\n      productSku: '',\n      productName: '',\n      productBrand: '',\n      productPrice: '',\n      productQuantity: '',\n      paymentMethod: order.paymentMethod,\n      paymentType: order.paymentType,\n      deliveryMethod: order.deliveryMethod,\n      deliveryPrice: order.deliveryPrice\n    });\n  }\n});\n\n// Generate filename with timestamp\nconst now = new Date();\nconst filename = `synerise_export_${now.toISOString().slice(0,10)}_${now.toTimeString().slice(0,5).replace(':','')}.csv`;\n\nreturn [{\n  json: {\n    filename: filename,\n    rowCount: csvRows.length,\n    exportTime: now.toISOString(),\n    data: csvRows\n  }\n}];"},"id":"prepare_csv","name":"Prepare CSV Data","type":"n8n-nodes-base.code","position":[2992,-440],"typeVersion":2},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getOrders"},{"name":"parameters","value":"={{ JSON.stringify({ order_id: parseInt($json.json.orderId), get_unconfirmed_orders: false }) }}"}]},"options":{"timeout":30000}},"id":"get_orders_batch","name":"Get Orders by IDs","type":"n8n-nodes-base.httpRequest","position":[1200,-32],"typeVersion":4.2,"retryOnFail":true,"maxTries":5,"waitBetweenTries":5000},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":1},"conditions":[{"leftValue":"={{ $json.logs?.length }}","rightValue":0,"operator":{"type":"number","operation":"gt"},"id":"fb5b9b41-36c6-408f-a7ce-41f8c97e3146"}],"combinator":"and"},"options":{}},"id":"check_changes","name":"Check If Changes Exist","type":"n8n-nodes-base.if","position":[304,-176],"typeVersion":2},{"parameters":{},"id":"no_changes","name":"No Changes - Stop","type":"n8n-nodes-base.noOp","position":[528,112],"typeVersion":1},{"parameters":{"jsCode":"// Extract Product IDs - WERSJA Z JEDNYM OUTPUTEM I RÓŻNYMI TYPAMI\nconst items = $input.all();\nconst allProductIds = new Set();\nconst allOrders = [];\n\n// Aggregate node zwraca wszystko w polu \"data\"\nconst aggregatedData = items[0]?.json?.data;\n\nif (aggregatedData && Array.isArray(aggregatedData)) {\n  // Sprawdź strukturę danych z Aggregate\n  aggregatedData.forEach(item => {\n    // Jeśli każdy item ma pole \"orders\" (batche z BaseLinker)\n    if (item.orders && Array.isArray(item.orders)) {\n      allOrders.push(...item.orders);\n    } \n    // Jeśli to bezpośrednio zamówienia (spłaszczone)\n    else if (item.order_id) {\n      allOrders.push(item);\n    }\n    // Jeśli to zagnieżdżone w json\n    else if (item.json?.orders) {\n      allOrders.push(...item.json.orders);\n    }\n    else if (item.json?.order_id) {\n      allOrders.push(item.json);\n    }\n  });\n} \n// Fallback - może dane są bezpośrednio w items\nelse {\n  items.forEach(item => {\n    if (item.json?.orders) {\n      allOrders.push(...item.json.orders);\n    } else if (item.json?.order_id) {\n      allOrders.push(item.json);\n    }\n  });\n}\n\n// Zbierz wszystkie unikalne product IDs\nallOrders.forEach(order => {\n  if (order.products && Array.isArray(order.products)) {\n    order.products.forEach(product => {\n      if (product.product_id) {\n        allProductIds.add(parseInt(product.product_id));\n      }\n    });\n  }\n});\n\nconst productIdsArray = Array.from(allProductIds);\n\n// Podziel produkty na batche po 100\nconst BATCH_SIZE = 100;\nconst productBatches = [];\nfor (let i = 0; i < productIdsArray.length; i += BATCH_SIZE) {\n  productBatches.push(productIdsArray.slice(i, i + BATCH_SIZE));\n}\n\nconsole.log('=== EXTRACT PRODUCT IDS ===');\nconsole.log(`Total orders: ${allOrders.length}`);\nconsole.log(`Unique products: ${productIdsArray.length}`);\nconsole.log(`Product batches: ${productBatches.length} (${BATCH_SIZE} per batch)`);\nproductBatches.forEach((batch, index) => {\n  console.log(`  Batch ${index + 1}: ${batch.length} products`);\n});\n\n// Przygotuj output items\nconst outputItems = [];\n\n// PIERWSZY ITEM: Zamówienia (dla Merge)\noutputItems.push({\n  json: {\n    type: 'ordersData',  // Unikalny typ dla Switch\n    orders: allOrders,\n    stats: {\n      batchOrderCount: allOrders.length,\n      uniqueProductCount: allProductIds.size,\n      totalBatches: productBatches.length,\n      avgProductsPerOrder: (allOrders.reduce((sum, o) => sum + (o.products?.length || 0), 0) / allOrders.length).toFixed(2)\n    }\n  }\n});\n\n// KOLEJNE ITEMY: Każdy batch produktów (dla Loop/Get Product Brands)\nproductBatches.forEach((batch, index) => {\n  outputItems.push({\n    json: {\n      type: 'productBatch',  // Unikalny typ dla Switch\n      batchIndex: index + 1,\n      totalBatches: productBatches.length,\n      productIdsToEnrich: batch,\n      batchSize: batch.length\n    }\n  });\n});\n\nconsole.log(`Returning ${outputItems.length} items: 1 ordersData + ${productBatches.length} productBatch(es)`);\n\nreturn outputItems;"},"id":"extract_product_ids","name":"Extract Product IDs","type":"n8n-nodes-base.code","typeVersion":2,"position":[1200,-272]},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getInventoryProductsData"},{"name":"parameters","value":"={{ JSON.stringify({ \n  inventory_id: 758, \n  products: $json.productIdsToEnrich || [] \n}) }}"}]},"options":{"timeout":30000}},"id":"get_product_brands","name":"Get Product Brands","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1872,-56],"retryOnFail":true},{"parameters":{"jsCode":"// FIXED: Merge Product Data - Correctly extracts data from Merge node\n// This version properly handles the 4 inputs from the Merge node\n\nconst allInputs = $input.all();\nconsole.log(`=== MERGE PRODUCT DATA - DEBUGGING ===`);\nconsole.log(`Total inputs received: ${allInputs.length}`);\n\n// Initialize data storage\nlet manufacturersData = [];\nlet ordersData = [];\nlet productsData = {};\nlet statusesData = [];\n\n// The Merge node combines 4 inputs:\n// Input 0: Product Manufacturers\n// Input 1: Orders (from Switch - ordersData branch)  \n// Input 2: Aggregated Products\n// Input 3: Order Status List\n\n// Process each input and extract the correct data\nallInputs.forEach((item, index) => {\n  const data = item.json;\n  \n  console.log(`\\nInput ${index} analysis:`, {\n    hasManufacturers: !!(data.manufacturers || data.manufacturer),\n    hasOrders: !!(data.orders || data.type === 'ordersData'),\n    hasProducts: !!(data.products || data.data),\n    hasStatuses: !!(data.statuses || data.status),\n    topKeys: Object.keys(data).slice(0, 5),\n    dataType: data.type || 'unknown'\n  });\n  \n  // Extract manufacturers (usually first input from Get Product Manufacturers)\n  if (data.manufacturers && Array.isArray(data.manufacturers)) {\n    manufacturersData = data.manufacturers;\n    console.log(`✓ Found ${manufacturersData.length} manufacturers`);\n  }\n  \n  // Extract orders (from ordersData type)\n  if (data.type === 'ordersData' && data.orders) {\n    ordersData = data.orders;\n    console.log(`✓ Found ${ordersData.length} orders from ordersData`);\n  } else if (data.orders && Array.isArray(data.orders)) {\n    ordersData = data.orders;\n    console.log(`✓ Found ${ordersData.length} orders`);\n  }\n  \n  // Extract products from aggregated data\n  if (data.data && Array.isArray(data.data)) {\n    // This is the aggregated products output\n    console.log(`✓ Found aggregated data with ${data.data.length} items`);\n    data.data.forEach(item => {\n      if (item.products && typeof item.products === 'object') {\n        Object.assign(productsData, item.products);\n      }\n    });\n  } else if (data.products && typeof data.products === 'object') {\n    // Direct products object\n    Object.assign(productsData, data.products);\n  }\n  \n  // Extract statuses\n  if (data.statuses && Array.isArray(data.statuses)) {\n    statusesData = data.statuses;\n    console.log(`✓ Found ${statusesData.length} order statuses`);\n  }\n});\n\n// Create manufacturer mapping (ID → Name)\nconst manufacturerMap = {};\nmanufacturersData.forEach(manufacturer => {\n  const id = String(manufacturer.manufacturer_id);\n  manufacturerMap[id] = manufacturer.name;\n});\n\nconsole.log(`\\n=== DATA SUMMARY ===`);\nconsole.log(`Manufacturers: ${Object.keys(manufacturerMap).length}`);\nconsole.log(`Sample manufacturers:`, Object.entries(manufacturerMap).slice(0, 5));\nconsole.log(`Products with brand data: ${Object.keys(productsData).length}`);\nconsole.log(`Orders to process: ${ordersData.length}`);\nconsole.log(`Statuses: ${statusesData.length}`);\n\n// Create status mapping\nconst statusMap = {};\nstatusesData.forEach(status => {\n  statusMap[String(status.id)] = status.name;\n});\n\n// Statistics\nlet stats = {\n  totalProducts: 0,\n  productsWithBrand: 0,\n  productsWithoutBrand: 0,\n  missingProductData: 0,\n  uniqueBrands: new Set(),\n  missingProductIds: new Set()\n};\n\n// Process and enrich orders\nconst enrichedOrders = ordersData.map((order, orderIndex) => {\n  // Clone the order to avoid mutation\n  const enrichedOrder = JSON.parse(JSON.stringify(order));\n  \n  // Add status name\n  enrichedOrder.status_name = statusMap[String(order.order_status_id)] || 'Unknown';\n  \n  // Process products in the order\n  if (enrichedOrder.products && Array.isArray(enrichedOrder.products)) {\n    enrichedOrder.products = enrichedOrder.products.map((product, productIndex) => {\n      stats.totalProducts++;\n      \n      const productIdStr = String(product.product_id);\n      const productData = productsData[productIdStr];\n      \n      // Initialize brand as Unknown\n      let brandName = 'Unknown';\n      let categoryName = '';\n      let ean = product.ean || '';\n      \n      if (productData) {\n        // Get manufacturer/brand name\n        if (productData.manufacturer_id) {\n          const manufacturerIdStr = String(productData.manufacturer_id);\n          const manufacturer = manufacturerMap[manufacturerIdStr];\n          \n          if (manufacturer) {\n            brandName = manufacturer;\n            stats.uniqueBrands.add(brandName);\n            stats.productsWithBrand++;\n          } else {\n            console.warn(`Manufacturer ID ${manufacturerIdStr} not found in map for product ${productIdStr}`);\n            stats.productsWithoutBrand++;\n          }\n        } else {\n          stats.productsWithoutBrand++;\n        }\n        \n        // Get category from text_fields\n        if (productData.text_fields) {\n          categoryName = productData.text_fields.extra_field_851 || '';\n        }\n        \n        // Get EAN\n        ean = productData.ean || product.ean || '';\n        \n      } else {\n        stats.missingProductData++;\n        stats.missingProductIds.add(productIdStr);\n        \n        if (orderIndex < 3 && productIndex < 2) {\n          console.warn(`No product data found for ID: ${productIdStr}`);\n        }\n      }\n      \n      // Debug first few products\n      if (orderIndex === 0 && productIndex < 3) {\n        console.log(`\\nSample enrichment - Product ${productIdStr}:`, {\n          originalName: product.name,\n          brand: brandName,\n          hasProductData: !!productData,\n          manufacturerId: productData?.manufacturer_id,\n          category: categoryName ? categoryName.substring(0, 50) + '...' : 'None'\n        });\n      }\n      \n      // Return enriched product\n      return {\n        ...product,\n        brand: brandName,\n        category_name: categoryName,\n        ean: ean\n      };\n    });\n  }\n  \n  return enrichedOrder;\n});\n\n// Calculate coverage\nconst coverage = stats.totalProducts > 0 \n  ? ((stats.productsWithBrand / stats.totalProducts) * 100).toFixed(1)\n  : 0;\n\nconsole.log(`\\n=== ENRICHMENT RESULTS ===`);\nconsole.log(`Total products: ${stats.totalProducts}`);\nconsole.log(`Products with brands: ${stats.productsWithBrand} (${coverage}%)`);\nconsole.log(`Products without brands: ${stats.productsWithoutBrand}`);\nconsole.log(`Products missing data: ${stats.missingProductData}`);\nconsole.log(`Unique brands found: ${stats.uniqueBrands.size}`);\nconsole.log(`Brand names:`, Array.from(stats.uniqueBrands).slice(0, 10).join(', '));\n\nif (stats.missingProductData > 0) {\n  console.warn(`\\n⚠️ Missing product IDs (first 10):`, \n    Array.from(stats.missingProductIds).slice(0, 10).join(', '));\n}\n\n// Return results\nconst output = [];\n\n// Add statistics as first item\noutput.push({\n  json: {\n    _type: 'statistics',\n    _timestamp: new Date().toISOString(),\n    coverage: {\n      total_products: stats.totalProducts,\n      with_brand: stats.productsWithBrand,\n      without_brand: stats.productsWithoutBrand,\n      missing_data: stats.missingProductData,\n      coverage_percent: `${coverage}%`,\n      unique_brands: stats.uniqueBrands.size\n    },\n    brands: Array.from(stats.uniqueBrands),\n    missing_ids: Array.from(stats.missingProductIds).slice(0, 20)\n  }\n});\n\n// Add enriched orders\nenrichedOrders.forEach(order => {\n  output.push({ json: order });\n});\n\nreturn output;"},"id":"merge_product_data","name":"Merge Product Data","type":"n8n-nodes-base.code","typeVersion":2,"position":[2320,-440]},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getInventoryManufacturers"},{"name":"parameters","value":"{\"inventory_id\":758}"}]},"options":{"timeout":30000}},"id":"c5dda8bd-6bde-4b69-951b-1be765ced825","name":"Get Product Manufacturers","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1872,-704],"retryOnFail":true},{"parameters":{"mode":"combine","combineBy":"combineByPosition","numberInputs":4,"options":{}},"type":"n8n-nodes-base.merge","typeVersion":3.2,"position":[2096,-472],"id":"02667abe-006e-4583-8357-2c78e454af1e","name":"Merge"},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getOrderStatusList"},{"name":"parameters","value":"{}"}]},"options":{}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1872,256],"id":"1f5c5d4d-7fb4-4810-97f5-aba0bf10d275","name":"Get Order Status List","retryOnFail":true},{"parameters":{"jsCode":"// Konfiguracja dozwolonych statusów - zgodnie z wymaganiami klienta\n// Tylko statusy końcowe: zrealizowane, anulowane, zwroty\n\nconst allowedStatuses = [\n  // === WYSŁANE/ZREALIZOWANE/DOSTARCZONE ===\n  300027, // WYSŁANE // .PL\n  300028, // WYSŁANE // _PL\n  361867, // DE // Wysłane\n  386333, // SM // WYSŁANE\n  354789, // Dostarczone\n  354788, // Paczka w punkcie\n  300828, // OO // Odebrane\n  \n  // === ANULOWANE ===\n  301588, // Anulowane (przez klienta)\n  302472, // Anulowane (brak wpłaty)\n  302473, // Anulowane (zwrot paczki)\n  302475, // Anulowane (brak towaru)\n  303313, // Anulowane STA\n  304745, // Anulacja przed wysyłką\n  316385, // Anulacje (testowe)\n  359429, // WMS Anulowane\n  302981, // [Optima] Anulacja RO\n  \n  // === ZWROTY ===\n  300346, // ZWR // Przedpłacone\n  303161, // ZWR // Deklaracja zwrotu\n  311537, // SOK // Zwroty paczek\n  354792, // Zwrot kurierski\n  372310, // MolZwrotPrzyjęty [sok] zwrot$\n  374054, // Zwroty MADS (Hebe i Mitago_EU)\n  374055, // Zwroty MAGU Kau+Pigu\n  356655, // [MOL] Zwroty częściowe\n  356674, // EXC zwrócone\n  \n  // === NIEDOSTARCZONE (może być traktowane jako anulowane) ===\n  354791  // Niedostarczone\n];\n\n// Mapowanie kategorii statusów dla lepszego raportowania\nconst statusCategories = {\n  'ZREALIZOWANE': [300027, 300028, 361867, 386333, 354789, 354788, 300828],\n  'ANULOWANE': [301588, 302472, 302473, 302475, 303313, 304745, 316385, 359429, 302981],\n  'ZWROTY': [300346, 303161, 311537, 354792, 372310, 374054, 374055, 356655, 356674],\n  'NIEDOSTARCZONE': [354791]\n};\n\n// Pobierz wszystkie zamówienia\nconst items = $input.all();\n\nconsole.log(\"=== FILTER ORDER STATUS ===\");\nconsole.log(`Total orders received: ${items.length}`);\n\n// Zbierz statystyki\nconst statusStats = {};\nconst categoryStats = {\n  'ZREALIZOWANE': 0,\n  'ANULOWANE': 0,\n  'ZWROTY': 0,\n  'NIEDOSTARCZONE': 0,\n  'OTHER': 0\n};\n\nlet passedCount = 0;\nlet filteredCount = 0;\n\n// Filtruj zamówienia\nconst filteredOrders = items.filter(item => {\n  const order = item.json;\n  const statusId = parseInt(order.order_status_id);\n  const statusName = order.status_name || 'Unknown';\n  \n  // Zbierz statystyki szczegółowe\n  const key = `${statusId} - ${statusName}`;\n  statusStats[key] = (statusStats[key] || 0) + 1;\n  \n  // Określ kategorię statusu\n  let category = 'OTHER';\n  for (const [cat, ids] of Object.entries(statusCategories)) {\n    if (ids.includes(statusId)) {\n      category = cat;\n      break;\n    }\n  }\n  categoryStats[category]++;\n  \n  // Sprawdź czy status jest dozwolony\n  const isAllowed = allowedStatuses.includes(statusId);\n  \n  if (isAllowed) {\n    passedCount++;\n    console.log(`  ✅ Order ${order.order_id}: Status ${statusId} (${statusName}) - ${category}`);\n  } else {\n    filteredCount++;\n    if (filteredCount <= 5) { // Pokaż tylko pierwsze 5 odrzuconych\n      console.log(`  ❌ Order ${order.order_id}: Status ${statusId} (${statusName}) - FILTERED OUT`);\n    }\n  }\n  \n  return isAllowed;\n});\n\n// Raport podsumowujący\nconsole.log(\"\\n=== STATUS CATEGORY SUMMARY ===\");\nconsole.log(`📦 Zrealizowane: ${categoryStats.ZREALIZOWANE} orders`);\nconsole.log(`❌ Anulowane: ${categoryStats.ANULOWANE} orders`);\nconsole.log(`↩️ Zwroty: ${categoryStats.ZWROTY} orders`);\nconsole.log(`⚠️ Niedostarczone: ${categoryStats.NIEDOSTARCZONE} orders`);\nconsole.log(`❓ Inne (odrzucone): ${categoryStats.OTHER} orders`);\n\nconsole.log(\"\\n=== DETAILED STATUS DISTRIBUTION ===\");\n// Sortuj statusy według liczby zamówień\nconst sortedStats = Object.entries(statusStats)\n  .sort((a, b) => b[1] - a[1])\n  .slice(0, 10); // Pokaż top 10\n\nsortedStats.forEach(([status, count]) => {\n  const statusId = parseInt(status.split(' ')[0]);\n  const allowed = allowedStatuses.includes(statusId) ? '✅' : '❌';\n  \n  // Znajdź kategorię\n  let category = 'OTHER';\n  for (const [cat, ids] of Object.entries(statusCategories)) {\n    if (ids.includes(statusId)) {\n      category = cat;\n      break;\n    }\n  }\n  \n  console.log(`  ${allowed} ${status}: ${count} orders [${category}]`);\n});\n\nconsole.log(`\\n=== FINAL SUMMARY ===`);\nconsole.log(`✅ Passed to Synerise: ${passedCount} orders`);\nconsole.log(`❌ Filtered out: ${filteredCount} orders`);\nconsole.log(`📊 Filter efficiency: ${((passedCount/items.length)*100).toFixed(1)}%`);\n\n// Ostrzeżenie jeśli wszystko odrzucone\nif (passedCount === 0 && items.length > 0) {\n  console.error(\"⚠️ WARNING: All orders were filtered out! Check status configuration.\");\n}\n\n// WAŻNE: Zwróć przefiltrowane zamówienia jako items\nreturn filteredOrders;"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[2544,-440],"id":"b86b7a29-5237-4232-af23-adac098b11e6","name":"Filter Order Status"},{"parameters":{"jsCode":"// Generate CSV with semicolon delimiter (16 columns)\n// UPDATED: Changed from 23 columns (comma) to 16 columns (semicolon)\nconst input = $input.all()[0].json;\nconst csvRows = input.data;\n\n// 16 columns (removed: eventSalt, clientName, clientPhone, productCategory, productEan, source, notes)\nconst headers = [\n  'orderId',\n  'recordedAt',\n  'clientEmail',\n  'status',\n  'statusName',\n  'totalAmount',\n  'currency',\n  'productSku',\n  'productName',\n  'productBrand',\n  'productPrice',\n  'productQuantity',\n  'paymentMethod',\n  'paymentType',\n  'deliveryMethod',\n  'deliveryPrice'\n];\n\n// Function to escape CSV values for semicolon delimiter\nfunction escapeCsvValue(value) {\n  if (value === null || value === undefined) return '';\n  const stringValue = String(value);\n  \n  // If contains semicolon, double quote, or newline, wrap in quotes\n  if (stringValue.includes(';') || stringValue.includes('\"') || stringValue.includes('\\n')) {\n    return `\"${stringValue.replace(/\"/g, '\"\"')}\"`;\n  }\n  return stringValue;\n}\n\n// Generate CSV with SEMICOLON delimiter\nlet csvContent = headers.join(';') + '\\n';\n\n// Add data rows\ncsvRows.forEach(row => {\n  const values = headers.map(header => escapeCsvValue(row[header]));\n  csvContent += values.join(';') + '\\n';\n});\n\nconsole.log(`Generated CSV: ${csvRows.length} rows, 16 columns, semicolon delimiter`);\n\n// Return as binary data\nreturn {\n  json: {\n    filename: input.filename,\n    rowCount: csvRows.length,\n    columnCount: headers.length,\n    delimiter: 'semicolon'\n  },\n  binary: {\n    data: {\n      data: Buffer.from(csvContent, 'utf8').toString('base64'),\n      mimeType: 'text/csv',\n      fileName: input.filename\n    }\n  }\n};"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[3216,-440],"id":"f78311b1-4880-45f1-83cb-b134a3eea978","name":"Code in JavaScript"},{"parameters":{"jsCode":"// Extract unique order IDs from journal\nconst items = $input.all();\nconst logs = items[0]?.json?.logs || [];\n\n// Get unique order IDs\nconst orderIds = [...new Set(logs.map(log => log.order_id).filter(id => id))];\n\n// Przygotuj outputy\nlet output0 = []; // Order IDs do przetworzenia\nlet output1 = []; // Dane do Google Sheets update\n\nif (logs.length > 0) {\n  // Znajdź log z najwyższą datą (najświeższy)\n  const newestLog = logs.reduce((newest, current) => {\n    return current.date > newest.date ? current : newest;\n  }, logs[0]);\n  \n  const newLastLogId = newestLog.log_id;\n  const newestDate = new Date(newestLog.date * 1000).toISOString();\n  \n  console.log(`Found ${logs.length} logs, ${orderIds.length} unique orders`);\n  console.log(`Newest log_id: ${newLastLogId} from date: ${newestDate}`);\n  console.log(`Newest log details:`, newestLog);\n  \n  // Output 0: Order IDs do przetworzenia\n  output0 = orderIds.map((orderId, index) => ({\n    json: {\n      orderId: orderId,\n      totalCount: orderIds.length,\n      currentIndex: index + 1\n    }\n  }));\n  \n  // Output 1: Dane do update Google Sheets (4 wiersze - dodałem newest_date)\n  output1 = [\n    {\n      json: {\n        key: 'last_log_id',\n        value: newLastLogId.toString(),\n        row_number: 2\n      }\n    },\n    {\n      json: {\n        key: 'last_sync',\n        value: new Date().toISOString(),\n        row_number: 3\n      }\n    },\n    {\n      json: {\n        key: 'total_orders',\n        value: orderIds.length.toString(),\n        row_number: 4\n      }\n    },\n    {\n      json: {\n        key: 'newest_log_date',\n        value: newestDate,\n        row_number: 5\n      }\n    }\n  ];\n} else {\n  console.log(\"No new orders to process\");\n}\n\n// Zwróć dane dla obu outputów\nreturn {\n  0: output0,  // Idzie do Loop Over Items\n  1: output1   // Idzie do Google Sheets Update\n};"},"id":"extract_order_ids","name":"Extract Order IDs & Prepare Update","type":"n8n-nodes-base.code","position":[528,-272],"typeVersion":2},{"parameters":{"fieldToSplitOut":"[\"0\"]","options":{}},"type":"n8n-nodes-base.splitOut","typeVersion":1,"position":[752,-80],"id":"bd9dc77a-8626-42cb-8d89-7029d5957980","name":"Split Out1"},{"parameters":{"assignments":{"assignments":[{"id":"89673c3e-0d1b-4e40-a3b7-87bc1d14ac25","name":"1","value":"={{ $json['1'] }}","type":"array"}]},"options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[752,-992],"id":"ab3d3a59-bf28-44ff-9bf3-2b2385af2e63","name":"Edit Fields"},{"parameters":{"options":{}},"type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[976,-80],"id":"35f888c4-1fd5-4f9f-83b0-f4b5d86f2f2b","name":"Loop Over Items"},{"parameters":{"jsCode":"// Inicjalizacja pętli - FIXED for Data Table compatibility\nconst staticData = $getWorkflowStaticData('global');\n\n// Data Table returns rows with {key, value, id, createdAt, updatedAt}\n// We need to find the specific row where key === 'last_log_id'\nconst items = $input.all();\nconst lastLogIdRow = items.find(item => item.json.key === 'last_log_id');\nconst lastLogId = lastLogIdRow ? lastLogIdRow.json.value : null;\n\nconst maxIterations = 50; // Zabezpieczenie przed nieskończoną pętlą\n\nconsole.log('=== STATE INITIALIZATION ===');\nconsole.log(`Found last_log_id row:`, lastLogIdRow ? 'YES' : 'NO');\nconsole.log(`Last log ID value: ${lastLogId}`);\nconsole.log(`Total state rows: ${items.length}`);\n\nreturn [{\n  json: {\n    lastLogId: lastLogId,\n    iteration: 0,\n    maxIterations: maxIterations,\n    continue: true,\n    cutoffTimestamp: Math.floor(Date.now() / 1000) - (24 * 60 * 60) // 24h temu\n  }\n}];"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[-816,-176],"id":"5f9815c0-2159-4208-adf0-078fde06e7be","name":"Code in JavaScript2"},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getJournalList"},{"name":"parameters","value":"={{ JSON.stringify({ logs_types: [1, 18], last_log_id: $json.lastLogId }) }}"}]},"options":{"timeout":30000}},"id":"8936a624-f84b-49a6-a2ab-149c91788642","name":"Get Journal Changes2","type":"n8n-nodes-base.httpRequest","position":[-592,-176],"typeVersion":4.2},{"parameters":{"jsCode":"// Sprawdź czy dotarliśmy do świeżych logów\nconst logs = $json.logs || [];\nconst iteration = $json.iteration || 0;\nconst maxIterations = $json.maxIterations || 50;\n\n// ZMIANA: Ustaw cutoffTimestamp na 1 godzinę temu\nconst cutoffTimestamp = $json.cutoffTimestamp || (Math.floor(Date.now() / 1000) - (360 * 60)); // 1h temu\n\nif (logs.length === 0) {\n  // Brak więcej logów - zakończ\n  return [{\n    json: {\n      continue: false,\n      reason: \"No more logs\",\n      ordersToProcess: [],\n      lastProcessedDate: null,\n      lastLogId: null\n    }\n  }];\n}\n\n// Bezpieczne znajdowanie min/max z walidacją\nconst validLogs = logs.filter(log => \n  log && \n  log.date && !isNaN(log.date) && \n  log.log_id && !isNaN(log.log_id)\n);\n\nif (validLogs.length === 0) {\n  console.error(\"No valid logs found:\", logs);\n  return [{\n    json: {\n      continue: false,\n      reason: \"Invalid log data\",\n      ordersToProcess: [],\n      error: \"No valid logs with date and log_id\"\n    }\n  }];\n}\n\n// Znajdź log z najnowszą datą (największy timestamp)\nconst newestLog = validLogs.reduce((newest, current) => {\n  return current.date > newest.date ? current : newest;\n}, validLogs[0]);\n\n// Znajdź log z najstarszą datą \nconst oldestLog = validLogs.reduce((oldest, current) => {\n  return current.date < oldest.date ? current : oldest;\n}, validLogs[0]);\n\nconst oldestTimestamp = oldestLog.date;\nconst newestTimestamp = newestLog.date;\nconst newestLogId = newestLog.log_id;\n\n// Konwersja na daty\nconst oldestDate = new Date(oldestTimestamp * 1000);\nconst newestDate = new Date(newestTimestamp * 1000);\nconst cutoffDate = new Date(cutoffTimestamp * 1000);\nconst currentDate = new Date();\n\n// KLUCZOWA LOGIKA: Sprawdź czy najstarszy log jest w ciągu ostatniej godziny\nconst isRecent = oldestTimestamp >= cutoffTimestamp;\n\n// Filtruj tylko logi z ostatniej godziny\nconst recentLogs = validLogs.filter(log => log.date >= cutoffTimestamp);\nconst orderIds = [...new Set(recentLogs.map(log => log.order_id).filter(id => id))];\n\n// Oblicz różnicę czasu dla lepszego debugowania\nconst timeDiff = (currentDate.getTime() - oldestDate.getTime()) / 1000; // w sekundach\nconst timeDiffMinutes = Math.round(timeDiff / 60);\nconst timeDiffFormatted = timeDiffMinutes < 60 ? \n  `${timeDiffMinutes} minut temu` : \n  `${Math.round(timeDiffMinutes / 60)} godzin ${timeDiffMinutes % 60} minut temu`;\n\n// Debug info\nconsole.log({\n  iteration: iteration,\n  logsCount: logs.length,\n  validLogsCount: validLogs.length,\n  currentTime: currentDate.toISOString(),\n  oldestDate: oldestDate.toISOString(),\n  newestDate: newestDate.toISOString(),\n  cutoffDate: cutoffDate.toISOString(),\n  oldestLogAge: timeDiffFormatted,\n  newestLogId: newestLogId,\n  isRecent: isRecent,\n  recentLogsCount: recentLogs.length,\n  uniqueOrdersCount: orderIds.length,\n  shouldContinue: !isRecent && iteration < maxIterations\n});\n\n// LOGIKA DECYZYJNA - szukamy danych z ostatniej GODZINY\nif (isRecent || iteration >= maxIterations) {\n  // ZAKOŃCZ tylko gdy:\n  // 1. Znaleźliśmy dane z ostatniej godziny (isRecent = true)\n  // 2. LUB osiągnęliśmy limit iteracji\n  \n  console.log(`STOPPING: isRecent=${isRecent}, iteration=${iteration}/${maxIterations}`);\n  console.log(`Found ${recentLogs.length} logs from last hour with ${orderIds.length} unique orders`);\n  \n  // Zapisz stan\n  const staticData = $getWorkflowStaticData('global');\n  staticData.lastLogId = newestLogId;\n  staticData.lastProcessedTimestamp = newestTimestamp;\n  staticData.lastProcessedDate = newestDate.toISOString();\n  \n  return [{\n    json: {\n      continue: false, // STOP\n      ordersToProcess: orderIds,\n      logsProcessed: recentLogs.length,\n      lastLogId: newestLogId,\n      lastProcessedTimestamp: newestTimestamp,\n      lastProcessedDate: newestDate.toISOString(),\n      oldestDateInBatch: oldestDate.toISOString(),\n      newestDateInBatch: newestDate.toISOString(),\n      timePeriod: \"Last 1 hour\", // Informacja o okresie czasu\n      newestLogDetails: {\n        log_id: newestLog.log_id,\n        order_id: newestLog.order_id,\n        log_type: newestLog.log_type,\n        date_timestamp: newestLog.date,\n        date_iso: newestDate.toISOString()\n      },\n      reachedLimit: iteration >= maxIterations,\n      totalIterations: iteration,\n      cutoffUsed: cutoffDate.toISOString(),\n      isRecent: isRecent,\n      stopReason: isRecent ? \"Found logs from last hour\" : \"Reached iteration limit\",\n      summary: `Processed ${recentLogs.length} logs from last hour containing ${orderIds.length} unique orders`\n    }\n  }];\n} else {\n  // KONTYNUUJ - nie znaleźliśmy jeszcze danych z ostatniej godziny\n  console.log(`CONTINUING: Looking for logs after ${cutoffDate.toISOString()} (last hour)`);\n  console.log(`Current oldest: ${oldestDate.toISOString()} (${timeDiffFormatted}), need to go deeper...`);\n  \n  return [{\n    json: {\n      continue: true, // KONTYNUUJ PĘTLĘ\n      lastLogId: newestLogId, // Użyj najnowszy log_id jako punkt startowy dla następnej iteracji\n      iteration: iteration + 1,\n      maxIterations: maxIterations,\n      cutoffTimestamp: cutoffTimestamp,\n      partialOrders: orderIds, // Przekaż częściowe wyniki\n      dateRange: {\n        oldest: oldestDate.toISOString(),\n        newest: newestDate.toISOString(),\n        cutoff: cutoffDate.toISOString()\n      },\n      timePeriod: \"Last 1 hour\",\n      message: `Iteration ${iteration + 1}: Found ${logs.length} logs, oldest is ${timeDiffFormatted}, need logs from last hour`\n    }\n  }];\n}"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[-368,-248],"id":"129494d7-d610-4501-aaee-83fb1b699587","name":"Code in JavaScript3"},{"parameters":{"conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"conditions":[{"id":"fbeec7a0-b97b-4e8c-b5e4-057af2b9e4c7","leftValue":"={{ $json.continue }}","rightValue":"","operator":{"type":"boolean","operation":"true","singleValue":true}}],"combinator":"and"},"options":{}},"type":"n8n-nodes-base.if","typeVersion":2.2,"position":[-144,-176],"id":"3372a372-6500-4dbb-af4b-1dc8d2697902","name":"If"},{"parameters":{"options":{}},"type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[1648,-200],"id":"a153f10d-55bb-49a6-9c18-7b087b9bbc27","name":"Loop Over Items1"},{"parameters":{"rules":{"values":[{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"leftValue":"={{ $json.type }}","rightValue":"ordersData","operator":{"type":"string","operation":"equals"},"id":"16455973-160a-451f-9ce5-d3e00d4ec21c"}],"combinator":"and"},"renameOutput":true,"outputKey":"orders"},{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"id":"d0ff60de-b3c8-45e4-8d2e-b00c0acc1685","leftValue":"={{ $json.type }}","rightValue":"productBatch","operator":{"type":"string","operation":"equals","name":"filter.operator.equals"}}],"combinator":"and"},"renameOutput":true,"outputKey":"product"}]},"options":{}},"type":"n8n-nodes-base.switch","typeVersion":3.3,"position":[1424,-272],"id":"2eeef40d-3217-4583-9238-e45cdf92fd64","name":"Switch"},{"parameters":{"jsCode":"// Transform If Node Output to Journal Format\n// This replaces both \"Edit Fields1\" and \"Get Journal Changes\" nodes\n// Converts ordersToProcess array into a logs format expected by downstream nodes\n\nconst input = $input.first().json;\n\n// Extract data from the If node\nconst orderIds = input.ordersToProcess || [];\nconst lastLogId = input.lastLogId;\nconst lastProcessedTimestamp = input.lastProcessedTimestamp;\nconst lastProcessedDate = input.lastProcessedDate;\nconst logsProcessed = input.logsProcessed;\n\nconsole.log(\"=== TRANSFORM IF OUTPUT TO JOURNAL FORMAT ===\");\nconsole.log(`Converting ${orderIds.length} order IDs to logs format`);\nconsole.log(`Last Log ID: ${lastLogId}`);\nconsole.log(`Logs originally processed: ${logsProcessed}`);\n\n// Create synthetic logs from the order IDs\n// We need to create a logs array that looks like what the API would return\n// but with ALL the orders from the last hour, not just new ones\nconst logs = [];\n\n// Generate log entries for each order\n// We'll create one log entry per order (log_type 1 = order created/modified)\norderIds.forEach((orderId, index) => {\n  logs.push({\n    log_id: lastLogId - (orderIds.length - index - 1), // Create sequential log IDs\n    log_type: 1, // Type 1 = order event\n    order_id: orderId,\n    object_id: 0,\n    date: lastProcessedTimestamp // Use the timestamp from processing\n  });\n});\n\nconsole.log(`✅ Created ${logs.length} log entries from ${orderIds.length} orders`);\n\n// Create output in the same format as \"Get Journal Changes\" would return\nconst output = {\n  status: \"SUCCESS\",\n  logs: logs,\n  // Add metadata for tracking\n  _metadata: {\n    source: \"If_node_transformation\",\n    originalOrderCount: orderIds.length,\n    lastLogId: lastLogId,\n    lastProcessedDate: lastProcessedDate,\n    processedAt: new Date().toISOString()\n  }\n};\n\n// Log sample for debugging\nif (logs.length > 0) {\n  console.log(\"\\nSample log entries (first 3):\");\n  logs.slice(0, 3).forEach(log => {\n    console.log(`  Order ${log.order_id}, Log ID: ${log.log_id}`);\n  });\n  if (logs.length > 3) {\n    console.log(`  ... and ${logs.length - 3} more`);\n  }\n}\n\n// Return in the format expected by \"Check If Changes Exist\"\nreturn [{ json: output }];"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[80,-176],"id":"7ef1fd1a-8d85-4bea-ac79-b6f3ed3e3cde","name":"Code in JavaScript4"},{"parameters":{"aggregate":"aggregateAllItemData","options":{}},"type":"n8n-nodes-base.aggregate","typeVersion":1,"position":[1872,-248],"id":"494369fd-2f92-487d-914b-f62b31de2e2e","name":"Aggregate"},{"parameters":{"jsCode":"// Filter out marketplace orders - keep ONLY online shop orders\n// Marketplace IDs to EXCLUDE: 15156 (Allegro), 15157 (Erli), 15158 (eMag)\n\nconst MARKETPLACE_IDS_TO_EXCLUDE = [15156, 15157, 15158];\n\nconst items = $input.all();\nconst orders = items[0]?.json?.orders || [];\n\nconsole.log('=== FILTER ONLINE SHOP ORDERS ===');\nconsole.log(`Total orders before filtering: ${orders.length}`);\n\n// Statistics\nconst stats = {\n  total: orders.length,\n  onlineShop: 0,\n  marketplace: 0,\n  bySource: {}\n};\n\n// Filter orders\nconst filteredOrders = orders.filter(order => {\n  const orderSourceId = parseInt(order.order_source_id);\n  \n  // Track statistics\n  const sourceName = order.order_source || 'Unknown';\n  stats.bySource[sourceName] = (stats.bySource[sourceName] || 0) + 1;\n  \n  const isMarketplace = MARKETPLACE_IDS_TO_EXCLUDE.includes(orderSourceId);\n  \n  if (isMarketplace) {\n    stats.marketplace++;\n    console.log(`  ❌ EXCLUDED: Order ${order.order_id} from ${sourceName} (ID: ${orderSourceId})`);\n    return false;\n  } else {\n    stats.onlineShop++;\n    return true;\n  }\n});\n\nconsole.log('\\n=== FILTERING RESULTS ===');\nconsole.log(`✅ Online shop orders: ${stats.onlineShop}`);\nconsole.log(`❌ Marketplace orders (excluded): ${stats.marketplace}`);\nconsole.log(`📊 Filter efficiency: ${((stats.onlineShop/stats.total)*100).toFixed(1)}% kept`);\n\nconsole.log('\\n=== ORDERS BY SOURCE ===');\nObject.entries(stats.bySource).forEach(([source, count]) => {\n  console.log(`  ${source}: ${count} orders`);\n});\n\n// Return filtered orders in the same structure\nreturn [{\n  json: {\n    orders: filteredOrders,\n    _filterStats: stats,\n    _timestamp: new Date().toISOString()\n  }\n}];"},"id":"filter_shop_orders","name":"Filter Online Shop Orders","type":"n8n-nodes-base.code","typeVersion":2,"position":[1424,16]},{"parameters":{"operation":"get","dataTableId":{"__rl":true,"value":"ZLqPZuPvd8alE2nF","mode":"list","cachedResultName":"workflow_state_synerise","cachedResultUrl":"/projects/jlSr7IBJt4siEqiM/datatables/ZLqPZuPvd8alE2nF"},"returnAll":true},"type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[-1040,-176],"id":"07b192e4-4094-4e70-82da-6f63b22ed42d","name":"get state"},{"parameters":{"operation":"update","dataTableId":{"__rl":true,"value":"ZLqPZuPvd8alE2nF","mode":"list","cachedResultName":"workflow_state_synerise","cachedResultUrl":"/projects/jlSr7IBJt4siEqiM/datatables/ZLqPZuPvd8alE2nF"},"filters":{"conditions":[{"keyName":"key","keyValue":"={{ $json['1'][0].json.key }}"}]},"columns":{"mappingMode":"defineBelow","value":{"value":"={{ $json['1'][0].json.value }}"},"matchingColumns":[],"schema":[{"id":"key","displayName":"key","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":true},{"id":"value","displayName":"value","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":false}],"attemptToConvertTypes":false,"convertFieldsToString":false},"options":{}},"type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[976,-992],"id":"141ccff1-591f-4c54-b04a-bec2a0fea7dd","name":"Update row(s)"},{"parameters":{"operation":"update","dataTableId":{"__rl":true,"value":"ZLqPZuPvd8alE2nF","mode":"list","cachedResultName":"workflow_state_synerise","cachedResultUrl":"/projects/jlSr7IBJt4siEqiM/datatables/ZLqPZuPvd8alE2nF"},"filters":{"conditions":[{"keyName":"key","keyValue":"={{ $('Edit Fields').item.json['1'][1].json.key }}"}]},"columns":{"mappingMode":"defineBelow","value":{"value":"={{ $('Edit Fields').item.json['1'][1].json.value }}"},"matchingColumns":[],"schema":[{"id":"key","displayName":"key","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":true},{"id":"value","displayName":"value","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":false}],"attemptToConvertTypes":false,"convertFieldsToString":false},"options":{}},"type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[1200,-992],"id":"6cf4c1c6-bc18-4477-804e-565104921745","name":"Update row(s)1"},{"parameters":{"operation":"update","dataTableId":{"__rl":true,"value":"ZLqPZuPvd8alE2nF","mode":"list","cachedResultName":"workflow_state_synerise","cachedResultUrl":"/projects/jlSr7IBJt4siEqiM/datatables/ZLqPZuPvd8alE2nF"},"filters":{"conditions":[{"keyName":"key","keyValue":"={{ $('Edit Fields').item.json['1'][2].json.key }}"}]},"columns":{"mappingMode":"defineBelow","value":{"value":"={{ $('Edit Fields').item.json['1'][2].json.value }}"},"matchingColumns":[],"schema":[{"id":"key","displayName":"key","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":true},{"id":"value","displayName":"value","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":false}],"attemptToConvertTypes":false,"convertFieldsToString":false},"options":{}},"type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[1424,-992],"id":"2cb43758-e040-4b61-8195-459ec5a4cd2a","name":"Update row(s)2"},{"parameters":{"operation":"update","dataTableId":{"__rl":true,"value":"ZLqPZuPvd8alE2nF","mode":"list","cachedResultName":"workflow_state_synerise","cachedResultUrl":"/projects/jlSr7IBJt4siEqiM/datatables/ZLqPZuPvd8alE2nF"},"filters":{"conditions":[{"keyName":"key","keyValue":"={{ $('Edit Fields').item.json['1'][3].json.key }}"}]},"columns":{"mappingMode":"defineBelow","value":{"value":"={{ $('Edit Fields').item.json['1'][3].json.value }}"},"matchingColumns":[],"schema":[{"id":"key","displayName":"key","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":true},{"id":"value","displayName":"value","required":false,"defaultMatch":false,"display":true,"type":"string","readOnly":false,"removed":false}],"attemptToConvertTypes":false,"convertFieldsToString":false},"options":{}},"type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[1648,-992],"id":"57f5a418-5443-4c70-b546-65333a3b54e1","name":"Update row(s)3"}],"connections":{"Manual Trigger":{"main":[[{"node":"get state","type":"main","index":0}]]},"Schedule Trigger - Every 15 min":{"main":[[{"node":"get state","type":"main","index":0}]]},"Transform to Synerise Format":{"main":[[{"node":"Prepare CSV Data","type":"main","index":0}]]},"Prepare CSV Data":{"main":[[{"node":"Code in JavaScript","type":"main","index":0}]]},"Check If Changes Exist":{"main":[[{"node":"Extract Order IDs & Prepare Update","type":"main","index":0},{"node":"Get Product Manufacturers","type":"main","index":0},{"node":"Get Order Status List","type":"main","index":0}],[{"node":"No Changes - Stop","type":"main","index":0}]]},"Extract Product IDs":{"main":[[{"node":"Switch","type":"main","index":0}]]},"Get Product Brands":{"main":[[{"node":"Loop Over Items1","type":"main","index":0}]]},"Merge Product Data":{"main":[[{"node":"Filter Order Status","type":"main","index":0}]]},"Get Product Manufacturers":{"main":[[{"node":"Merge","type":"main","index":0}]]},"Get Order Status List":{"main":[[{"node":"Merge","type":"main","index":3}]]},"Merge":{"main":[[{"node":"Merge Product Data","type":"main","index":0}]]},"Filter Order Status":{"main":[[{"node":"Transform to Synerise Format","type":"main","index":0}]]},"Extract Order IDs & Prepare Update":{"main":[[{"node":"Split Out1","type":"main","index":0},{"node":"Edit Fields","type":"main","index":0}]]},"Split Out1":{"main":[[{"node":"Loop Over Items","type":"main","index":0}]]},"Edit Fields":{"main":[[{"node":"Update row(s)","type":"main","index":0}]]},"Loop Over Items":{"main":[[{"node":"Extract Product IDs","type":"main","index":0}],[{"node":"Get Orders by IDs","type":"main","index":0}]]},"Code in JavaScript2":{"main":[[{"node":"Get Journal Changes2","type":"main","index":0}]]},"Get Journal Changes2":{"main":[[{"node":"Code in JavaScript3","type":"main","index":0}]]},"Code in JavaScript3":{"main":[[{"node":"If","type":"main","index":0}]]},"If":{"main":[[{"node":"Get Journal Changes2","type":"main","index":0}],[{"node":"Code in JavaScript4","type":"main","index":0}]]},"Loop Over Items1":{"main":[[{"node":"Aggregate","type":"main","index":0}],[{"node":"Get Product Brands","type":"main","index":0}]]},"Switch":{"main":[[{"node":"Merge","type":"main","index":1}],[{"node":"Loop Over Items1","type":"main","index":0}]]},"Code in JavaScript4":{"main":[[{"node":"Check If Changes Exist","type":"main","index":0}]]},"Aggregate":{"main":[[{"node":"Merge","type":"main","index":2}]]},"Get Orders by IDs":{"main":[[{"node":"Filter Online Shop Orders","type":"main","index":0}]]},"Filter Online Shop Orders":{"main":[[{"node":"Loop Over Items","type":"main","index":0}]]},"get state":{"main":[[{"node":"Code in JavaScript2","type":"main","index":0}]]},"Update row(s)":{"main":[[{"node":"Update row(s)1","type":"main","index":0}]]},"Update row(s)1":{"main":[[{"node":"Update row(s)2","type":"main","index":0}]]},"Update row(s)2":{"main":[[{"node":"Update row(s)3","type":"main","index":0}]]}},"settings":{"saveManualExecutions":true,"executionOrder":"v1","callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":{"node:Schedule Trigger - Every 15 min":{"recurrenceRules":[]}},"meta":{"templateCredsSetupCompleted":true},"pinData":{},"versionId":"6c97fecf-4996-42b0-8cd1-c06b29366df6","versionCounter":5,"triggerCount":1,"shared":[{"updatedAt":"2025-09-25T14:46:56.372Z","createdAt":"2025-09-25T14:46:56.372Z","role":"workflow:owner","workflowId":"HMS43ySZOcEfGSyX","projectId":"jlSr7IBJt4siEqiM","project":{"updatedAt":"2025-01-31T18:30:45.852Z","createdAt":"2025-01-31T18:13:38.763Z","id":"jlSr7IBJt4siEqiM","name":"N8N Estyl <n8n@estyl.pl>","type":"personal","icon":null,"description":null,"projectRelations":[{"updatedAt":"2025-01-31T18:13:38.763Z","createdAt":"2025-01-31T18:13:38.763Z","userId":"5089e5a8-9975-48ff-8451-f5c3fb3e78ac","projectId":"jlSr7IBJt4siEqiM","user":{"updatedAt":"2025-11-13T23:00:06.270Z","createdAt":"2025-01-31T18:13:37.839Z","id":"5089e5a8-9975-48ff-8451-f5c3fb3e78ac","email":"n8n@estyl.pl","firstName":"N8N","lastName":"Estyl","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2025-01-31T18:30:49.691Z","personalization_survey_n8n_version":"1.76.1"},"settings":{"userActivated":true,"firstSuccessfulWorkflowId":"xbn9AqbzxO9Xbruw","userActivatedAt":1739378108329,"easyAIWorkflowOnboarded":true,"npsSurvey":{"responded":true,"lastShownAt":1758812569793}},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2025-11-13","isPending":false}}]}}],"tags":[]}

operations:
  [{"type":"updateNode","nodeName":"Filter Online Shop Orders","updates":{"parameters.jsCode":"// Filter to include ONLY online shop orders (inclusion-based approach)\n// FIXED: Changed from exclusion list to checking order_source field directly\n// This ensures ALL marketplaces are excluded (Allegro, Kaufland, Hebe, etc.)\n\nconst items = $input.all();\nconst orders = items[0]?.json?.orders || [];\n\nconsole.log('=== FILTER ONLINE SHOP ORDERS (INCLUSION-BASED) ===');\nconsole.log(`Total orders before filtering: ${orders.length}`);\n\n// Statistics\nconst stats = {\n  total: orders.length,\n  onlineShop: 0,\n  marketplace: 0,\n  unknown: 0,\n  bySource: {}\n};\n\n// Filter orders - ONLY include shop orders\nconst filteredOrders = orders.filter(order => {\n  const orderSource = (order.order_source || '').toLowerCase();\n  const orderSourceId = order.order_source_id;\n  \n  // Track statistics by source\n  const sourceName = order.order_source || 'Unknown';\n  stats.bySource[sourceName] = (stats.bySource[sourceName] || 0) + 1;\n  \n  // INCLUSION FILTER: Only accept orders where source is \"shop\"\n  const isShop = orderSource === 'shop' || orderSource.includes('shop');\n  \n  if (isShop) {\n    stats.onlineShop++;\n    if (stats.onlineShop <= 5) {\n      console.log(`  ✅ INCLUDED: Order ${order.order_id} from '${sourceName}' (source_id: ${orderSourceId})`);\n    }\n    return true;\n  } else if (!orderSource || orderSource === 'unknown') {\n    stats.unknown++;\n    if (stats.unknown <= 3) {\n      console.log(`  ⚠️ UNKNOWN: Order ${order.order_id} has unknown/empty source (source_id: ${orderSourceId})`);\n    }\n    return false;\n  } else {\n    stats.marketplace++;\n    if (stats.marketplace <= 5) {\n      console.log(`  ❌ EXCLUDED: Order ${order.order_id} from '${sourceName}' (source_id: ${orderSourceId})`);\n    }\n    return false;\n  }\n});\n\nconsole.log('\\n=== FILTERING RESULTS (INCLUSION-BASED) ===');\nconsole.log(`✅ Online shop orders: ${stats.onlineShop}`);\nconsole.log(`❌ Marketplace orders (excluded): ${stats.marketplace}`);\nconsole.log(`⚠️ Unknown source (excluded): ${stats.unknown}`);\nconsole.log(`📊 Filter efficiency: ${((stats.onlineShop/stats.total)*100).toFixed(1)}% kept`);\n\nconsole.log('\\n=== ORDERS BY SOURCE ===');\nObject.entries(stats.bySource)\n  .sort((a, b) => b[1] - a[1])\n  .forEach(([source, count]) => {\n    const isShop = source.toLowerCase().includes('shop');\n    const emoji = isShop ? '✅' : '❌';\n    console.log(`  ${emoji} ${source}: ${count} orders`);\n  });\n\nconsole.log('\\n=== FILTER APPROACH ===');\nconsole.log('Using INCLUSION-based filter: only order_source=\"shop\" passes');\nconsole.log('This automatically excludes ALL marketplaces:');\nconsole.log('  - Allegro, Kaufland, Hebe, Empik, eMag, etc.');\nconsole.log('  - Any future marketplace integrations');\n\n// Return filtered orders in the same structure\nreturn [{\n  json: {\n    orders: filteredOrders,\n    _filterStats: stats,\n    _filterApproach: 'inclusion-based',\n    _filterCriteria: 'order_source === \"shop\"',\n    _timestamp: new Date().toISOString()\n  }\n}];"}}]


workflow_id: 7DBmgk8syS86CCmY
workflow_name: [TEST] Simple Webhook to Code - No Settings
trigger: partial_update
operations: [{"type":"moveNode","nodeName":"Code","position":[480,320]}]
created_at: 2025-11-20 15:36:15
workflow_snapshot:
  {"updatedAt":"2025-11-20T15:36:08.758Z","createdAt":"2025-11-20T15:36:08.758Z","id":"7DBmgk8syS86CCmY","name":"[TEST] Simple Webhook to Code - No Settings","active":false,"isArchived":false,"nodes":[{"id":"webhook_node","name":"Webhook","type":"n8n-nodes-base.webhook","typeVersion":2,"position":[240,300],"parameters":{"httpMethod":"GET","path":"test-no-settings"}},{"id":"code_node","name":"Code","type":"n8n-nodes-base.code","typeVersion":2,"position":[460,300],"parameters":{"jsCode":"return [{json: {message: 'Hello from test'}}];"}}],"connections":{"Webhook":{"main":[[{"node":"Code","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","saveDataErrorExecution":"all","saveDataSuccessExecution":"all","saveManualExecutions":true,"saveExecutionProgress":true},"staticData":null,"meta":null,"pinData":null,"versionId":"e517fd3e-cd61-4f69-8de3-9804cad950b0","versionCounter":1,"triggerCount":0,"shared":[{"updatedAt":"2025-11-20T15:36:08.761Z","createdAt":"2025-11-20T15:36:08.761Z","role":"workflow:owner","workflowId":"7DBmgk8syS86CCmY","projectId":"K0HtvGddtPI24Iyt","project":{"updatedAt":"2025-10-03T09:49:33.347Z","createdAt":"2025-10-03T09:48:05.062Z","id":"K0HtvGddtPI24Iyt","name":"Romuald Członkowski <romuald@n8n-mcp.com>","type":"personal","icon":null,"description":null,"projectRelations":[{"updatedAt":"2025-10-03T09:48:05.063Z","createdAt":"2025-10-03T09:48:05.063Z","userId":"ebb1bcfe-d103-4506-a994-9b18ef25e289","projectId":"K0HtvGddtPI24Iyt","user":{"updatedAt":"2025-11-20T15:36:08.000Z","createdAt":"2025-10-03T09:48:04.462Z","id":"ebb1bcfe-d103-4506-a994-9b18ef25e289","email":"romuald@n8n-mcp.com","firstName":"Romuald","lastName":"Członkowski","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2025-10-03T09:49:36.595Z","personalization_survey_n8n_version":"1.113.3"},"settings":{"userActivated":true,"firstSuccessfulWorkflowId":"DWyvSVPDFSSo9jwN","userActivatedAt":1759494619257,"easyAIWorkflowOnboarded":true,"npsSurvey":{"waitingForResponse":true,"ignoredCount":1,"lastShownAt":1762117185907}},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2025-11-19","isPending":false}}]}}],"tags":[]}


workflow_id: 7DBmgk8syS86CCmY
workflow_name: [TEST] Simple Webhook to Code - No Settings
trigger: partial_update
operations: [{"type":"updateSettings","settings":{}}]
created_at: 2025-11-20 15:36:25
workflow_snapshot:
  {"updatedAt":"2025-11-20T15:36:15.901Z","createdAt":"2025-11-20T15:36:08.758Z","id":"7DBmgk8syS86CCmY","name":"[TEST] Simple Webhook to Code - No Settings","active":false,"isArchived":false,"nodes":[{"id":"webhook_node","name":"Webhook","type":"n8n-nodes-base.webhook","typeVersion":2,"position":[240,300],"parameters":{"httpMethod":"GET","path":"test-no-settings"}},{"id":"code_node","name":"Code","type":"n8n-nodes-base.code","typeVersion":2,"position":[480,320],"parameters":{"jsCode":"return [{json: {message: 'Hello from test'}}];"}}],"connections":{"Webhook":{"main":[[{"node":"Code","type":"main","index":0}]]}},"settings":{"saveExecutionProgress":true,"saveManualExecutions":true,"saveDataErrorExecution":"all","saveDataSuccessExecution":"all","executionOrder":"v1"},"staticData":null,"meta":null,"pinData":null,"versionId":"63220894-8e4e-483d-816b-32a77f69b4d7","versionCounter":2,"triggerCount":0,"shared":[{"updatedAt":"2025-11-20T15:36:08.761Z","createdAt":"2025-11-20T15:36:08.761Z","role":"workflow:owner","workflowId":"7DBmgk8syS86CCmY","projectId":"K0HtvGddtPI24Iyt","project":{"updatedAt":"2025-10-03T09:49:33.347Z","createdAt":"2025-10-03T09:48:05.062Z","id":"K0HtvGddtPI24Iyt","name":"Romuald Członkowski <romuald@n8n-mcp.com>","type":"personal","icon":null,"description":null,"projectRelations":[{"updatedAt":"2025-10-03T09:48:05.063Z","createdAt":"2025-10-03T09:48:05.063Z","userId":"ebb1bcfe-d103-4506-a994-9b18ef25e289","projectId":"K0HtvGddtPI24Iyt","user":{"updatedAt":"2025-11-20T15:36:08.000Z","createdAt":"2025-10-03T09:48:04.462Z","id":"ebb1bcfe-d103-4506-a994-9b18ef25e289","email":"romuald@n8n-mcp.com","firstName":"Romuald","lastName":"Członkowski","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2025-10-03T09:49:36.595Z","personalization_survey_n8n_version":"1.113.3"},"settings":{"userActivated":true,"firstSuccessfulWorkflowId":"DWyvSVPDFSSo9jwN","userActivatedAt":1759494619257,"easyAIWorkflowOnboarded":true,"npsSurvey":{"waitingForResponse":true,"ignoredCount":1,"lastShownAt":1762117185907}},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2025-11-19","isPending":false}}]}}],"tags":[]}


workflow_id: OKxEofhNxdEGnpzS
workflow_name: [TEST] Workflow With Custom Settings
trigger: partial_update
operations: [{"type":"moveNode","nodeName":"Process Data","position":[500,300]}]
created_at: 2025-11-20 15:36:45
workflow_snapshot:
  {"updatedAt":"2025-11-20T15:36:38.789Z","createdAt":"2025-11-20T15:36:38.789Z","id":"OKxEofhNxdEGnpzS","name":"[TEST] Workflow With Custom Settings","active":false,"isArchived":false,"nodes":[{"id":"webhook_2","name":"Webhook Input","type":"n8n-nodes-base.webhook","typeVersion":2,"position":[240,300],"parameters":{"httpMethod":"POST","path":"test-settings"}},{"id":"code_2","name":"Process Data","type":"n8n-nodes-base.code","typeVersion":2,"position":[460,300],"parameters":{"jsCode":"return [{json: {processed: true}}];"}}],"connections":{"Webhook Input":{"main":[[{"node":"Process Data","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","timezone":"America/New_York","saveDataErrorExecution":"all","saveDataSuccessExecution":"none"},"staticData":null,"meta":null,"pinData":null,"versionId":"909a2759-f3ff-43ff-9b5f-59191ad1f6ce","versionCounter":1,"triggerCount":0,"shared":[{"updatedAt":"2025-11-20T15:36:38.792Z","createdAt":"2025-11-20T15:36:38.792Z","role":"workflow:owner","workflowId":"OKxEofhNxdEGnpzS","projectId":"K0HtvGddtPI24Iyt","project":{"updatedAt":"2025-10-03T09:49:33.347Z","createdAt":"2025-10-03T09:48:05.062Z","id":"K0HtvGddtPI24Iyt","name":"Romuald Członkowski <romuald@n8n-mcp.com>","type":"personal","icon":null,"description":null,"projectRelations":[{"updatedAt":"2025-10-03T09:48:05.063Z","createdAt":"2025-10-03T09:48:05.063Z","userId":"ebb1bcfe-d103-4506-a994-9b18ef25e289","projectId":"K0HtvGddtPI24Iyt","user":{"updatedAt":"2025-11-20T15:36:08.000Z","createdAt":"2025-10-03T09:48:04.462Z","id":"ebb1bcfe-d103-4506-a994-9b18ef25e289","email":"romuald@n8n-mcp.com","firstName":"Romuald","lastName":"Członkowski","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2025-10-03T09:49:36.595Z","personalization_survey_n8n_version":"1.113.3"},"settings":{"userActivated":true,"firstSuccessfulWorkflowId":"DWyvSVPDFSSo9jwN","userActivatedAt":1759494619257,"easyAIWorkflowOnboarded":true,"npsSurvey":{"waitingForResponse":true,"ignoredCount":1,"lastShownAt":1762117185907}},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2025-11-19","isPending":false}}]}}],"tags":[]}


workflow_id: QOL9V6fN8ClEs6EN
workflow_name: [TEST] Minimal Workflow - No Settings Property
trigger: partial_update
created_at: 2025-11-20 15:36:56
workflow_snapshot:
  {"updatedAt":"2025-11-20T15:36:50.480Z","createdAt":"2025-11-20T15:36:50.480Z","id":"QOL9V6fN8ClEs6EN","name":"[TEST] Minimal Workflow - No Settings Property","active":false,"isArchived":false,"nodes":[{"id":"webhook_3","name":"Start","type":"n8n-nodes-base.webhook","typeVersion":2,"position":[240,300],"parameters":{"httpMethod":"GET","path":"minimal"}},{"id":"code_3","name":"End","type":"n8n-nodes-base.code","typeVersion":2,"position":[460,300],"parameters":{"jsCode":"return [];"}}],"connections":{"Start":{"main":[[{"node":"End","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","saveDataErrorExecution":"all","saveDataSuccessExecution":"all","saveManualExecutions":true,"saveExecutionProgress":true},"staticData":null,"meta":null,"pinData":null,"versionId":"f42b1221-dc0b-41f0-a033-03f8bed5eff7","versionCounter":1,"triggerCount":0,"shared":[{"updatedAt":"2025-11-20T15:36:50.482Z","createdAt":"2025-11-20T15:36:50.482Z","role":"workflow:owner","workflowId":"QOL9V6fN8ClEs6EN","projectId":"K0HtvGddtPI24Iyt","project":{"updatedAt":"2025-10-03T09:49:33.347Z","createdAt":"2025-10-03T09:48:05.062Z","id":"K0HtvGddtPI24Iyt","name":"Romuald Członkowski <romuald@n8n-mcp.com>","type":"personal","icon":null,"description":null,"projectRelations":[{"updatedAt":"2025-10-03T09:48:05.063Z","createdAt":"2025-10-03T09:48:05.063Z","userId":"ebb1bcfe-d103-4506-a994-9b18ef25e289","projectId":"K0HtvGddtPI24Iyt","user":{"updatedAt":"2025-11-20T15:36:08.000Z","createdAt":"2025-10-03T09:48:04.462Z","id":"ebb1bcfe-d103-4506-a994-9b18ef25e289","email":"romuald@n8n-mcp.com","firstName":"Romuald","lastName":"Członkowski","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2025-10-03T09:49:36.595Z","personalization_survey_n8n_version":"1.113.3"},"settings":{"userActivated":true,"firstSuccessfulWorkflowId":"DWyvSVPDFSSo9jwN","userActivatedAt":1759494619257,"easyAIWorkflowOnboarded":true,"npsSurvey":{"waitingForResponse":true,"ignoredCount":1,"lastShownAt":1762117185907}},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2025-11-19","isPending":false}}]}}],"tags":[]}

operations:
  [{"type":"updateNode","nodeName":"Start","updates":{"parameters.path":"updated-path"}},{"type":"moveNode","nodeName":"End","position":[500,320]},{"type":"addConnection","source":"End","target":"End"}]


workflow_id: ZAZxVj2143XdVvOe
workflow_name: BaseLinker to Synerise CSV Export - historical backfill
trigger: partial_update
created_at: 2025-11-20 15:44:15
workflow_snapshot:
  {"updatedAt":"2025-11-20T15:11:20.870Z","createdAt":"2025-11-20T14:44:24.861Z","id":"ZAZxVj2143XdVvOe","name":"BaseLinker to Synerise CSV Export - historical backfill","description":"test","active":false,"isArchived":false,"nodes":[{"parameters":{"jsCode":"// Transform BaseLinker orders to Synerise CSV format\n  // Mode: Run Once for All Items\n  // FIXED: orderId now uses shop_order_id (short format) instead of order_id\n  // FIXED: recordedAt now uses correct UTC timestamps (removed incorrect +2h offset)\n  // FIXED: totalAmount now calculates from products + delivery (not payment_done which is 0 for COD)\n\n  const items = $input.all();\n  const results = [];\n\n  console.log(`Processing ${items.length} orders for Synerise export`);\n\n  // Map status using the enriched status_name\n  const statusMapping = {\n    'ZREALIZOWANE': 'completed',\n    'ANULOWANE': 'cancelled',\n    'ZWROT': 'returned',\n    'WYSŁANE': 'shipped'\n  };\n\n  // Process each order\n  items.forEach((item, index) => {\n    try {\n      const order = item.json;\n\n      // Generate eventSalt\n      const eventSalt = `${order.order_id}_${order.date_add || Date.now()}`;\n\n      // TIMEZONE FIX: BaseLinker returns date_confirmed as Unix timestamp in UTC\n      // No offset needed - UTC is the correct format for data systems\n      // Synerise will handle timezone conversion for display purposes\n      let recordedAt;\n      if (order.date_confirmed) {\n        const timestamp = parseInt(order.date_confirmed);\n        recordedAt = new Date(timestamp * 1000).toISOString();\n\n        // Debug log for first order\n        if (index === 0) {\n          console.log(`Timestamp conversion example:`);\n          console.log(`  Unix timestamp: ${timestamp}`);\n          console.log(`  UTC ISO format: ${recordedAt}`);\n        }\n      } else {\n        recordedAt = new Date().toISOString();\n      }\n\n      // Find appropriate status based on name\n      let orderStatus = 'unknown';\n      const statusNameUpper = (order.status_name || '').toUpperCase();\n      for (const [key, value] of Object.entries(statusMapping)) {\n        if (statusNameUpper.includes(key)) {\n          orderStatus = value;\n          break;\n        }\n      }\n\n      // Determine payment type\n      const paymentType = order.payment_method_cod === '1' ? 'Pobranie' : 'Przelew';\n\n      // Safe parseFloat\n      const safeParseFloat = (value) => {\n        const parsed = parseFloat(value);\n        return isNaN(parsed) ? 0 : parsed;\n      };\n\n      // BUGFIX #3: Calculate totalAmount from products + delivery\n      // BaseLinker API doesn't provide order total - must calculate it\n      // payment_done shows 0 for COD orders (unpaid), but order still has value\n      let totalAmount = 0;\n\n      // Calculate products subtotal\n      if (order.products && Array.isArray(order.products)) {\n        const productsSubtotal = order.products.reduce((sum, product) => {\n          const price = safeParseFloat(product.price_brutto);\n          const quantity = parseInt(product.quantity) || 0;\n          return sum + (price * quantity);\n        }, 0);\n\n        // Add delivery cost\n        totalAmount = productsSubtotal + safeParseFloat(order.delivery_price);\n\n        // Debug log for first order\n        if (index === 0) {\n          console.log(`Total amount calculation example:`);\n          console.log(`  Products subtotal: ${productsSubtotal.toFixed(2)}`);\n          console.log(`  Delivery price: ${safeParseFloat(order.delivery_price).toFixed(2)}`);\n          console.log(`  Total amount: ${totalAmount.toFixed(2)}`);\n          console.log(`  Payment done (old approach): ${safeParseFloat(order.payment_done)}`);\n        }\n      } else {\n        // Fallback: if no products array, use delivery_price only\n        totalAmount = safeParseFloat(order.delivery_price);\n      }\n\n      // Extract client name with fallbacks\n      const clientName = order.delivery_fullname ||\n                        order.invoice_fullname ||\n                        order.delivery_company ||\n                        `${order.delivery_name || ''} ${order.delivery_surname || ''}`.trim() ||\n                        'Unknown Customer';\n\n      // BUGFIX #1: Use shop_order_id (short format) instead of order_id\n      // shop_order_id examples: 184031, 184042 (human-readable short numbers)\n      // order_id examples: 790964970, 790932859 (internal BaseLinker IDs)\n      const orderId = order.shop_order_id || order.order_id; // Fallback to order_id if shop_order_id is missing\n\n      // Debug log for first order to verify fix\n      if (index === 0) {\n        console.log(`Order ID fix example:`);\n        console.log(`  OLD (order_id): ${order.order_id}`);\n        console.log(`  NEW (shop_order_id): ${order.shop_order_id}`);\n        console.log(`  Using: ${orderId}`);\n      }\n\n      // Add transformed order to results\n      results.push({\n        json: {\n          eventSalt: eventSalt,\n          orderId: orderId,\n          recordedAt: recordedAt, // FIXED: Now correct UTC format without offset\n          clientEmail: order.email || '',\n          clientName: clientName,\n          clientPhone: order.phone || '',\n          status: orderStatus,\n          statusName: order.status_name || '',\n          totalAmount: Math.round(totalAmount * 100) / 100, // FIXED: Calculated from products + delivery\n          currency: order.currency || 'PLN',\n          paymentMethod: order.payment_method || '',\n          paymentType: paymentType,\n          deliveryMethod: order.delivery_method || '',\n          deliveryPrice: safeParseFloat(order.delivery_price),\n          products: order.products || [],\n          source: order.order_source || 'BaseLinker',\n          notes: order.admin_comments || ''\n        }\n      });\n\n    } catch (error) {\n      console.error(`Error processing order at index ${index}:`, error);\n      console.error(`Order data:`, JSON.stringify(item.json).substring(0, 500));\n\n      // Add failed order with error info\n      results.push({\n        json: {\n          error: true,\n          errorMessage: error.message,\n          orderId: item.json?.order_id || 'UNKNOWN',\n          originalData: item.json\n        }\n      });\n    }\n  });\n\n  console.log(`Successfully transformed ${results.length} orders`);\n  console.log(`Sample order IDs: ${results.slice(0, 5).map(r => r.json.orderId).join(', ')}`);\n\n  // Return all transformed items\n  return results;\n"},"id":"4dc6cd3a-4e59-45f5-90ce-035791d545b4","name":"Transform to Synerise Format","type":"n8n-nodes-base.code","position":[3184,-448],"typeVersion":2},{"parameters":{"jsCode":"// Flatten nested product data for CSV\n// UPDATED: 16 columns (removed: eventSalt, clientName, clientPhone, productCategory, productEan, source, notes)\nconst items = $input.all();\nconst csvRows = [];\n\nitems.forEach(item => {\n  const order = item.json;\n  \n  // If order has products, create a row for each product\n  if (order.products && order.products.length > 0) {\n    order.products.forEach(product => {\n      csvRows.push({\n        orderId: order.orderId,\n        recordedAt: order.recordedAt,\n        clientEmail: order.clientEmail,\n        status: order.status,\n        statusName: order.statusName,\n        totalAmount: order.totalAmount,\n        currency: order.currency,\n        productSku: product.sku || product.product_id,\n        productName: product.name,\n        productBrand: product.brand || 'Unknown',\n        productPrice: product.price_brutto,\n        productQuantity: product.quantity,\n        paymentMethod: order.paymentMethod,\n        paymentType: order.paymentType,\n        deliveryMethod: order.deliveryMethod,\n        deliveryPrice: order.deliveryPrice\n      });\n    });\n  } else {\n    // Order without products\n    csvRows.push({\n      orderId: order.orderId,\n      recordedAt: order.recordedAt,\n      clientEmail: order.clientEmail,\n      status: order.status,\n      statusName: order.statusName,\n      totalAmount: order.totalAmount,\n      currency: order.currency,\n      productSku: '',\n      productName: '',\n      productBrand: '',\n      productPrice: '',\n      productQuantity: '',\n      paymentMethod: order.paymentMethod,\n      paymentType: order.paymentType,\n      deliveryMethod: order.deliveryMethod,\n      deliveryPrice: order.deliveryPrice\n    });\n  }\n});\n\n// Generate filename with timestamp\nconst now = new Date();\nconst filename = `synerise_export_${now.toISOString().slice(0,10)}_${now.toTimeString().slice(0,5).replace(':','')}.csv`;\n\nreturn [{\n  json: {\n    filename: filename,\n    rowCount: csvRows.length,\n    exportTime: now.toISOString(),\n    data: csvRows\n  }\n}];"},"id":"6c52f816-b911-4c5d-9264-b66c81c5f06a","name":"Prepare CSV Data","type":"n8n-nodes-base.code","position":[3408,-448],"typeVersion":2},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getOrders"},{"name":"parameters","value":"={{ JSON.stringify({ order_id: parseInt($json.json.orderId), get_unconfirmed_orders: false }) }}"}]},"options":{"timeout":30000}},"id":"15ca7ed2-b332-49ee-8168-ae2c30ae43c3","name":"Get Orders by IDs","type":"n8n-nodes-base.httpRequest","position":[1616,-32],"typeVersion":4.2,"retryOnFail":true,"maxTries":5,"waitBetweenTries":5000},{"parameters":{"jsCode":"// Extract Product IDs - WERSJA Z JEDNYM OUTPUTEM I RÓŻNYMI TYPAMI\nconst items = $input.all();\nconst allProductIds = new Set();\nconst allOrders = [];\n\n// Aggregate node zwraca wszystko w polu \"data\"\nconst aggregatedData = items[0]?.json?.data;\n\nif (aggregatedData && Array.isArray(aggregatedData)) {\n  // Sprawdź strukturę danych z Aggregate\n  aggregatedData.forEach(item => {\n    // Jeśli każdy item ma pole \"orders\" (batche z BaseLinker)\n    if (item.orders && Array.isArray(item.orders)) {\n      allOrders.push(...item.orders);\n    } \n    // Jeśli to bezpośrednio zamówienia (spłaszczone)\n    else if (item.order_id) {\n      allOrders.push(item);\n    }\n    // Jeśli to zagnieżdżone w json\n    else if (item.json?.orders) {\n      allOrders.push(...item.json.orders);\n    }\n    else if (item.json?.order_id) {\n      allOrders.push(item.json);\n    }\n  });\n} \n// Fallback - może dane są bezpośrednio w items\nelse {\n  items.forEach(item => {\n    if (item.json?.orders) {\n      allOrders.push(...item.json.orders);\n    } else if (item.json?.order_id) {\n      allOrders.push(item.json);\n    }\n  });\n}\n\n// Zbierz wszystkie unikalne product IDs\nallOrders.forEach(order => {\n  if (order.products && Array.isArray(order.products)) {\n    order.products.forEach(product => {\n      if (product.product_id) {\n        allProductIds.add(parseInt(product.product_id));\n      }\n    });\n  }\n});\n\nconst productIdsArray = Array.from(allProductIds);\n\n// Podziel produkty na batche po 100\nconst BATCH_SIZE = 100;\nconst productBatches = [];\nfor (let i = 0; i < productIdsArray.length; i += BATCH_SIZE) {\n  productBatches.push(productIdsArray.slice(i, i + BATCH_SIZE));\n}\n\nconsole.log('=== EXTRACT PRODUCT IDS ===');\nconsole.log(`Total orders: ${allOrders.length}`);\nconsole.log(`Unique products: ${productIdsArray.length}`);\nconsole.log(`Product batches: ${productBatches.length} (${BATCH_SIZE} per batch)`);\nproductBatches.forEach((batch, index) => {\n  console.log(`  Batch ${index + 1}: ${batch.length} products`);\n});\n\n// Przygotuj output items\nconst outputItems = [];\n\n// PIERWSZY ITEM: Zamówienia (dla Merge)\noutputItems.push({\n  json: {\n    type: 'ordersData',  // Unikalny typ dla Switch\n    orders: allOrders,\n    stats: {\n      batchOrderCount: allOrders.length,\n      uniqueProductCount: allProductIds.size,\n      totalBatches: productBatches.length,\n      avgProductsPerOrder: (allOrders.reduce((sum, o) => sum + (o.products?.length || 0), 0) / allOrders.length).toFixed(2)\n    }\n  }\n});\n\n// KOLEJNE ITEMY: Każdy batch produktów (dla Loop/Get Product Brands)\nproductBatches.forEach((batch, index) => {\n  outputItems.push({\n    json: {\n      type: 'productBatch',  // Unikalny typ dla Switch\n      batchIndex: index + 1,\n      totalBatches: productBatches.length,\n      productIdsToEnrich: batch,\n      batchSize: batch.length\n    }\n  });\n});\n\nconsole.log(`Returning ${outputItems.length} items: 1 ordersData + ${productBatches.length} productBatch(es)`);\n\nreturn outputItems;"},"id":"ec3603bf-23ec-4859-98d3-354391f11e8e","name":"Extract Product IDs","type":"n8n-nodes-base.code","typeVersion":2,"position":[1616,-272]},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getInventoryProductsData"},{"name":"parameters","value":"={{ JSON.stringify({ \n  inventory_id: 758, \n  products: $json.productIdsToEnrich || [] \n}) }}"}]},"options":{"timeout":30000}},"id":"3417e7b6-d432-4e68-8500-6ea48ce52427","name":"Get Product Brands","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2288,-64],"retryOnFail":true},{"parameters":{"jsCode":"// FIXED: Merge Product Data - Correctly extracts data from Merge node\n// This version properly handles the 4 inputs from the Merge node\n\nconst allInputs = $input.all();\nconsole.log(`=== MERGE PRODUCT DATA - DEBUGGING ===`);\nconsole.log(`Total inputs received: ${allInputs.length}`);\n\n// Initialize data storage\nlet manufacturersData = [];\nlet ordersData = [];\nlet productsData = {};\nlet statusesData = [];\n\n// The Merge node combines 4 inputs:\n// Input 0: Product Manufacturers\n// Input 1: Orders (from Switch - ordersData branch)  \n// Input 2: Aggregated Products\n// Input 3: Order Status List\n\n// Process each input and extract the correct data\nallInputs.forEach((item, index) => {\n  const data = item.json;\n  \n  console.log(`\\nInput ${index} analysis:`, {\n    hasManufacturers: !!(data.manufacturers || data.manufacturer),\n    hasOrders: !!(data.orders || data.type === 'ordersData'),\n    hasProducts: !!(data.products || data.data),\n    hasStatuses: !!(data.statuses || data.status),\n    topKeys: Object.keys(data).slice(0, 5),\n    dataType: data.type || 'unknown'\n  });\n  \n  // Extract manufacturers (usually first input from Get Product Manufacturers)\n  if (data.manufacturers && Array.isArray(data.manufacturers)) {\n    manufacturersData = data.manufacturers;\n    console.log(`✓ Found ${manufacturersData.length} manufacturers`);\n  }\n  \n  // Extract orders (from ordersData type)\n  if (data.type === 'ordersData' && data.orders) {\n    ordersData = data.orders;\n    console.log(`✓ Found ${ordersData.length} orders from ordersData`);\n  } else if (data.orders && Array.isArray(data.orders)) {\n    ordersData = data.orders;\n    console.log(`✓ Found ${ordersData.length} orders`);\n  }\n  \n  // Extract products from aggregated data\n  if (data.data && Array.isArray(data.data)) {\n    // This is the aggregated products output\n    console.log(`✓ Found aggregated data with ${data.data.length} items`);\n    data.data.forEach(item => {\n      if (item.products && typeof item.products === 'object') {\n        Object.assign(productsData, item.products);\n      }\n    });\n  } else if (data.products && typeof data.products === 'object') {\n    // Direct products object\n    Object.assign(productsData, data.products);\n  }\n  \n  // Extract statuses\n  if (data.statuses && Array.isArray(data.statuses)) {\n    statusesData = data.statuses;\n    console.log(`✓ Found ${statusesData.length} order statuses`);\n  }\n});\n\n// Create manufacturer mapping (ID → Name)\nconst manufacturerMap = {};\nmanufacturersData.forEach(manufacturer => {\n  const id = String(manufacturer.manufacturer_id);\n  manufacturerMap[id] = manufacturer.name;\n});\n\nconsole.log(`\\n=== DATA SUMMARY ===`);\nconsole.log(`Manufacturers: ${Object.keys(manufacturerMap).length}`);\nconsole.log(`Sample manufacturers:`, Object.entries(manufacturerMap).slice(0, 5));\nconsole.log(`Products with brand data: ${Object.keys(productsData).length}`);\nconsole.log(`Orders to process: ${ordersData.length}`);\nconsole.log(`Statuses: ${statusesData.length}`);\n\n// Create status mapping\nconst statusMap = {};\nstatusesData.forEach(status => {\n  statusMap[String(status.id)] = status.name;\n});\n\n// Statistics\nlet stats = {\n  totalProducts: 0,\n  productsWithBrand: 0,\n  productsWithoutBrand: 0,\n  missingProductData: 0,\n  uniqueBrands: new Set(),\n  missingProductIds: new Set()\n};\n\n// Process and enrich orders\nconst enrichedOrders = ordersData.map((order, orderIndex) => {\n  // Clone the order to avoid mutation\n  const enrichedOrder = JSON.parse(JSON.stringify(order));\n  \n  // Add status name\n  enrichedOrder.status_name = statusMap[String(order.order_status_id)] || 'Unknown';\n  \n  // Process products in the order\n  if (enrichedOrder.products && Array.isArray(enrichedOrder.products)) {\n    enrichedOrder.products = enrichedOrder.products.map((product, productIndex) => {\n      stats.totalProducts++;\n      \n      const productIdStr = String(product.product_id);\n      const productData = productsData[productIdStr];\n      \n      // Initialize brand as Unknown\n      let brandName = 'Unknown';\n      let categoryName = '';\n      let ean = product.ean || '';\n      \n      if (productData) {\n        // Get manufacturer/brand name\n        if (productData.manufacturer_id) {\n          const manufacturerIdStr = String(productData.manufacturer_id);\n          const manufacturer = manufacturerMap[manufacturerIdStr];\n          \n          if (manufacturer) {\n            brandName = manufacturer;\n            stats.uniqueBrands.add(brandName);\n            stats.productsWithBrand++;\n          } else {\n            console.warn(`Manufacturer ID ${manufacturerIdStr} not found in map for product ${productIdStr}`);\n            stats.productsWithoutBrand++;\n          }\n        } else {\n          stats.productsWithoutBrand++;\n        }\n        \n        // Get category from text_fields\n        if (productData.text_fields) {\n          categoryName = productData.text_fields.extra_field_851 || '';\n        }\n        \n        // Get EAN\n        ean = productData.ean || product.ean || '';\n        \n      } else {\n        stats.missingProductData++;\n        stats.missingProductIds.add(productIdStr);\n        \n        if (orderIndex < 3 && productIndex < 2) {\n          console.warn(`No product data found for ID: ${productIdStr}`);\n        }\n      }\n      \n      // Debug first few products\n      if (orderIndex === 0 && productIndex < 3) {\n        console.log(`\\nSample enrichment - Product ${productIdStr}:`, {\n          originalName: product.name,\n          brand: brandName,\n          hasProductData: !!productData,\n          manufacturerId: productData?.manufacturer_id,\n          category: categoryName ? categoryName.substring(0, 50) + '...' : 'None'\n        });\n      }\n      \n      // Return enriched product\n      return {\n        ...product,\n        brand: brandName,\n        category_name: categoryName,\n        ean: ean\n      };\n    });\n  }\n  \n  return enrichedOrder;\n});\n\n// Calculate coverage\nconst coverage = stats.totalProducts > 0 \n  ? ((stats.productsWithBrand / stats.totalProducts) * 100).toFixed(1)\n  : 0;\n\nconsole.log(`\\n=== ENRICHMENT RESULTS ===`);\nconsole.log(`Total products: ${stats.totalProducts}`);\nconsole.log(`Products with brands: ${stats.productsWithBrand} (${coverage}%)`);\nconsole.log(`Products without brands: ${stats.productsWithoutBrand}`);\nconsole.log(`Products missing data: ${stats.missingProductData}`);\nconsole.log(`Unique brands found: ${stats.uniqueBrands.size}`);\nconsole.log(`Brand names:`, Array.from(stats.uniqueBrands).slice(0, 10).join(', '));\n\nif (stats.missingProductData > 0) {\n  console.warn(`\\n⚠️ Missing product IDs (first 10):`, \n    Array.from(stats.missingProductIds).slice(0, 10).join(', '));\n}\n\n// Return results\nconst output = [];\n\n// Add statistics as first item\noutput.push({\n  json: {\n    _type: 'statistics',\n    _timestamp: new Date().toISOString(),\n    coverage: {\n      total_products: stats.totalProducts,\n      with_brand: stats.productsWithBrand,\n      without_brand: stats.productsWithoutBrand,\n      missing_data: stats.missingProductData,\n      coverage_percent: `${coverage}%`,\n      unique_brands: stats.uniqueBrands.size\n    },\n    brands: Array.from(stats.uniqueBrands),\n    missing_ids: Array.from(stats.missingProductIds).slice(0, 20)\n  }\n});\n\n// Add enriched orders\nenrichedOrders.forEach(order => {\n  output.push({ json: order });\n});\n\nreturn output;"},"id":"aa52a712-23fe-42e2-b71a-414707430739","name":"Merge Product Data","type":"n8n-nodes-base.code","typeVersion":2,"position":[2736,-448]},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getInventoryManufacturers"},{"name":"parameters","value":"{\"inventory_id\":758}"}]},"options":{"timeout":30000}},"id":"8098ddd8-f311-48f1-b7e0-bc13de4a4712","name":"Get Product Manufacturers","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2288,-704],"retryOnFail":true},{"parameters":{"mode":"combine","combineBy":"combineByPosition","numberInputs":4,"options":{}},"type":"n8n-nodes-base.merge","typeVersion":3.2,"position":[2512,-480],"id":"5e3a6a8e-44a2-444a-a3bc-6d162dd4cf94","name":"Merge"},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getOrderStatusList"},{"name":"parameters","value":"{}"}]},"options":{}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2288,256],"id":"e923ab81-95d9-43c5-9271-e76a4fecd0aa","name":"Get Order Status List","retryOnFail":true},{"parameters":{"jsCode":"// Konfiguracja dozwolonych statusów - zgodnie z wymaganiami klienta\n// Tylko statusy końcowe: zrealizowane, anulowane, zwroty\n\nconst allowedStatuses = [\n  // === WYSŁANE/ZREALIZOWANE/DOSTARCZONE ===\n  300027, // WYSŁANE // .PL\n  300028, // WYSŁANE // _PL\n  361867, // DE // Wysłane\n  386333, // SM // WYSŁANE\n  354789, // Dostarczone\n  354788, // Paczka w punkcie\n  300828, // OO // Odebrane\n  \n  // === ANULOWANE ===\n  301588, // Anulowane (przez klienta)\n  302472, // Anulowane (brak wpłaty)\n  302473, // Anulowane (zwrot paczki)\n  302475, // Anulowane (brak towaru)\n  303313, // Anulowane STA\n  304745, // Anulacja przed wysyłką\n  316385, // Anulacje (testowe)\n  359429, // WMS Anulowane\n  302981, // [Optima] Anulacja RO\n  \n  // === ZWROTY ===\n  300346, // ZWR // Przedpłacone\n  303161, // ZWR // Deklaracja zwrotu\n  311537, // SOK // Zwroty paczek\n  354792, // Zwrot kurierski\n  372310, // MolZwrotPrzyjęty [sok] zwrot$\n  374054, // Zwroty MADS (Hebe i Mitago_EU)\n  374055, // Zwroty MAGU Kau+Pigu\n  356655, // [MOL] Zwroty częściowe\n  356674, // EXC zwrócone\n  \n  // === NIEDOSTARCZONE (może być traktowane jako anulowane) ===\n  354791  // Niedostarczone\n];\n\n// Mapowanie kategorii statusów dla lepszego raportowania\nconst statusCategories = {\n  'ZREALIZOWANE': [300027, 300028, 361867, 386333, 354789, 354788, 300828],\n  'ANULOWANE': [301588, 302472, 302473, 302475, 303313, 304745, 316385, 359429, 302981],\n  'ZWROTY': [300346, 303161, 311537, 354792, 372310, 374054, 374055, 356655, 356674],\n  'NIEDOSTARCZONE': [354791]\n};\n\n// Pobierz wszystkie zamówienia\nconst items = $input.all();\n\nconsole.log(\"=== FILTER ORDER STATUS ===\");\nconsole.log(`Total orders received: ${items.length}`);\n\n// Zbierz statystyki\nconst statusStats = {};\nconst categoryStats = {\n  'ZREALIZOWANE': 0,\n  'ANULOWANE': 0,\n  'ZWROTY': 0,\n  'NIEDOSTARCZONE': 0,\n  'OTHER': 0\n};\n\nlet passedCount = 0;\nlet filteredCount = 0;\n\n// Filtruj zamówienia\nconst filteredOrders = items.filter(item => {\n  const order = item.json;\n  const statusId = parseInt(order.order_status_id);\n  const statusName = order.status_name || 'Unknown';\n  \n  // Zbierz statystyki szczegółowe\n  const key = `${statusId} - ${statusName}`;\n  statusStats[key] = (statusStats[key] || 0) + 1;\n  \n  // Określ kategorię statusu\n  let category = 'OTHER';\n  for (const [cat, ids] of Object.entries(statusCategories)) {\n    if (ids.includes(statusId)) {\n      category = cat;\n      break;\n    }\n  }\n  categoryStats[category]++;\n  \n  // Sprawdź czy status jest dozwolony\n  const isAllowed = allowedStatuses.includes(statusId);\n  \n  if (isAllowed) {\n    passedCount++;\n    console.log(`  ✅ Order ${order.order_id}: Status ${statusId} (${statusName}) - ${category}`);\n  } else {\n    filteredCount++;\n    if (filteredCount <= 5) { // Pokaż tylko pierwsze 5 odrzuconych\n      console.log(`  ❌ Order ${order.order_id}: Status ${statusId} (${statusName}) - FILTERED OUT`);\n    }\n  }\n  \n  return isAllowed;\n});\n\n// Raport podsumowujący\nconsole.log(\"\\n=== STATUS CATEGORY SUMMARY ===\");\nconsole.log(`📦 Zrealizowane: ${categoryStats.ZREALIZOWANE} orders`);\nconsole.log(`❌ Anulowane: ${categoryStats.ANULOWANE} orders`);\nconsole.log(`↩️ Zwroty: ${categoryStats.ZWROTY} orders`);\nconsole.log(`⚠️ Niedostarczone: ${categoryStats.NIEDOSTARCZONE} orders`);\nconsole.log(`❓ Inne (odrzucone): ${categoryStats.OTHER} orders`);\n\nconsole.log(\"\\n=== DETAILED STATUS DISTRIBUTION ===\");\n// Sortuj statusy według liczby zamówień\nconst sortedStats = Object.entries(statusStats)\n  .sort((a, b) => b[1] - a[1])\n  .slice(0, 10); // Pokaż top 10\n\nsortedStats.forEach(([status, count]) => {\n  const statusId = parseInt(status.split(' ')[0]);\n  const allowed = allowedStatuses.includes(statusId) ? '✅' : '❌';\n  \n  // Znajdź kategorię\n  let category = 'OTHER';\n  for (const [cat, ids] of Object.entries(statusCategories)) {\n    if (ids.includes(statusId)) {\n      category = cat;\n      break;\n    }\n  }\n  \n  console.log(`  ${allowed} ${status}: ${count} orders [${category}]`);\n});\n\nconsole.log(`\\n=== FINAL SUMMARY ===`);\nconsole.log(`✅ Passed to Synerise: ${passedCount} orders`);\nconsole.log(`❌ Filtered out: ${filteredCount} orders`);\nconsole.log(`📊 Filter efficiency: ${((passedCount/items.length)*100).toFixed(1)}%`);\n\n// Ostrzeżenie jeśli wszystko odrzucone\nif (passedCount === 0 && items.length > 0) {\n  console.error(\"⚠️ WARNING: All orders were filtered out! Check status configuration.\");\n}\n\n// WAŻNE: Zwróć przefiltrowane zamówienia jako items\nreturn filteredOrders;"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[2960,-448],"id":"48ed4ca2-613f-4699-9553-6a59168168e5","name":"Filter Order Status"},{"parameters":{"jsCode":"// Generate CSV with semicolon delimiter (16 columns)\n// UPDATED: Changed from 23 columns (comma) to 16 columns (semicolon)\nconst input = $input.all()[0].json;\nconst csvRows = input.data;\n\n// 16 columns (removed: eventSalt, clientName, clientPhone, productCategory, productEan, source, notes)\nconst headers = [\n  'orderId',\n  'recordedAt',\n  'clientEmail',\n  'status',\n  'statusName',\n  'totalAmount',\n  'currency',\n  'productSku',\n  'productName',\n  'productBrand',\n  'productPrice',\n  'productQuantity',\n  'paymentMethod',\n  'paymentType',\n  'deliveryMethod',\n  'deliveryPrice'\n];\n\n// Function to escape CSV values for semicolon delimiter\nfunction escapeCsvValue(value) {\n  if (value === null || value === undefined) return '';\n  const stringValue = String(value);\n  \n  // If contains semicolon, double quote, or newline, wrap in quotes\n  if (stringValue.includes(';') || stringValue.includes('\"') || stringValue.includes('\\n')) {\n    return `\"${stringValue.replace(/\"/g, '\"\"')}\"`;\n  }\n  return stringValue;\n}\n\n// Generate CSV with SEMICOLON delimiter\nlet csvContent = headers.join(';') + '\\n';\n\n// Add data rows\ncsvRows.forEach(row => {\n  const values = headers.map(header => escapeCsvValue(row[header]));\n  csvContent += values.join(';') + '\\n';\n});\n\nconsole.log(`Generated CSV: ${csvRows.length} rows, 16 columns, semicolon delimiter`);\n\n// Return as binary data\nreturn {\n  json: {\n    filename: input.filename,\n    rowCount: csvRows.length,\n    columnCount: headers.length,\n    delimiter: 'semicolon'\n  },\n  binary: {\n    data: {\n      data: Buffer.from(csvContent, 'utf8').toString('base64'),\n      mimeType: 'text/csv',\n      fileName: input.filename\n    }\n  }\n};"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[3632,-448],"id":"5a820d61-40f2-40eb-8a98-ba40fa84f8d0","name":"Code in JavaScript"},{"parameters":{"fieldToSplitOut":"[\"0\"]","options":{}},"type":"n8n-nodes-base.splitOut","typeVersion":1,"position":[1168,-80],"id":"e9603521-9b07-44ee-8587-503b8cdb1d27","name":"Split Out1"},{"parameters":{"options":{}},"type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[1392,-80],"id":"8880595d-e24f-4e1e-8306-1419e710ba3b","name":"Loop Over Items"},{"parameters":{"options":{}},"type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[2064,-208],"id":"b3546273-74ce-4094-870e-b7aabee04600","name":"Loop Over Items1"},{"parameters":{"rules":{"values":[{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"leftValue":"={{ $json.type }}","rightValue":"ordersData","operator":{"type":"string","operation":"equals"},"id":"16455973-160a-451f-9ce5-d3e00d4ec21c"}],"combinator":"and"},"renameOutput":true,"outputKey":"orders"},{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"id":"d0ff60de-b3c8-45e4-8d2e-b00c0acc1685","leftValue":"={{ $json.type }}","rightValue":"productBatch","operator":{"type":"string","operation":"equals","name":"filter.operator.equals"}}],"combinator":"and"},"renameOutput":true,"outputKey":"product"}]},"options":{}},"type":"n8n-nodes-base.switch","typeVersion":3.3,"position":[1840,-272],"id":"bded8fd0-05ff-4eb6-8d6c-cc30cebe60b9","name":"Switch"},{"parameters":{"aggregate":"aggregateAllItemData","options":{}},"type":"n8n-nodes-base.aggregate","typeVersion":1,"position":[2288,-256],"id":"637e4855-58c9-434e-a41b-86a01d129154","name":"Aggregate"},{"parameters":{"jsCode":"// Filter to include ONLY online shop orders (inclusion-based approach)\n// FIXED: Changed from exclusion list to checking order_source field directly\n// This ensures ALL marketplaces are excluded (Allegro, Kaufland, Hebe, etc.)\n\nconst items = $input.all();\nconst orders = items[0]?.json?.orders || [];\n\nconsole.log('=== FILTER ONLINE SHOP ORDERS (INCLUSION-BASED) ===');\nconsole.log(`Total orders before filtering: ${orders.length}`);\n\n// Statistics\nconst stats = {\n  total: orders.length,\n  onlineShop: 0,\n  marketplace: 0,\n  unknown: 0,\n  bySource: {}\n};\n\n// Filter orders - ONLY include shop orders\nconst filteredOrders = orders.filter(order => {\n  const orderSource = (order.order_source || '').toLowerCase();\n  const orderSourceId = order.order_source_id;\n  \n  // Track statistics by source\n  const sourceName = order.order_source || 'Unknown';\n  stats.bySource[sourceName] = (stats.bySource[sourceName] || 0) + 1;\n  \n  // INCLUSION FILTER: Only accept orders where source is \"shop\"\n  const isShop = orderSource === 'shop' || orderSource.includes('shop');\n  \n  if (isShop) {\n    stats.onlineShop++;\n    if (stats.onlineShop <= 5) {\n      console.log(`  ✅ INCLUDED: Order ${order.order_id} from '${sourceName}' (source_id: ${orderSourceId})`);\n    }\n    return true;\n  } else if (!orderSource || orderSource === 'unknown') {\n    stats.unknown++;\n    if (stats.unknown <= 3) {\n      console.log(`  ⚠️ UNKNOWN: Order ${order.order_id} has unknown/empty source (source_id: ${orderSourceId})`);\n    }\n    return false;\n  } else {\n    stats.marketplace++;\n    if (stats.marketplace <= 5) {\n      console.log(`  ❌ EXCLUDED: Order ${order.order_id} from '${sourceName}' (source_id: ${orderSourceId})`);\n    }\n    return false;\n  }\n});\n\nconsole.log('\\n=== FILTERING RESULTS (INCLUSION-BASED) ===');\nconsole.log(`✅ Online shop orders: ${stats.onlineShop}`);\nconsole.log(`❌ Marketplace orders (excluded): ${stats.marketplace}`);\nconsole.log(`⚠️ Unknown source (excluded): ${stats.unknown}`);\nconsole.log(`📊 Filter efficiency: ${((stats.onlineShop/stats.total)*100).toFixed(1)}% kept`);\n\nconsole.log('\\n=== ORDERS BY SOURCE ===');\nObject.entries(stats.bySource)\n  .sort((a, b) => b[1] - a[1])\n  .forEach(([source, count]) => {\n    const isShop = source.toLowerCase().includes('shop');\n    const emoji = isShop ? '✅' : '❌';\n    console.log(`  ${emoji} ${source}: ${count} orders`);\n  });\n\nconsole.log('\\n=== FILTER APPROACH ===');\nconsole.log('Using INCLUSION-based filter: only order_source=\"shop\" passes');\nconsole.log('This automatically excludes ALL marketplaces:');\nconsole.log('  - Allegro, Kaufland, Hebe, Empik, eMag, etc.');\nconsole.log('  - Any future marketplace integrations');\n\n// Return filtered orders in the same structure\nreturn [{\n  json: {\n    orders: filteredOrders,\n    _filterStats: stats,\n    _filterApproach: 'inclusion-based',\n    _filterCriteria: 'order_source === \"shop\"',\n    _timestamp: new Date().toISOString()\n  }\n}];"},"id":"3312a255-a865-4d0f-b4d4-112045b1767f","name":"Filter Online Shop Orders","type":"n8n-nodes-base.code","typeVersion":2,"position":[1840,16]},{"parameters":{"jsCode":"// Extract unique order IDs from journal\nconst items = $input.all();\nconst logs = items[0]?.json?.logs || [];\n\n// Get unique order IDs\nconst orderIds = [...new Set(logs.map(log => log.order_id).filter(id => id))];\n\n// Przygotuj outputy\nlet output0 = []; // Order IDs do przetworzenia\nlet output1 = []; // Dane do Google Sheets update\n\nif (logs.length > 0) {\n  // Znajdź log z najwyższą datą (najświeższy)\n  const newestLog = logs.reduce((newest, current) => {\n    return current.date > newest.date ? current : newest;\n  }, logs[0]);\n  \n  const newLastLogId = newestLog.log_id;\n  const newestDate = new Date(newestLog.date * 1000).toISOString();\n  \n  console.log(`Found ${logs.length} logs, ${orderIds.length} unique orders`);\n  console.log(`Newest log_id: ${newLastLogId} from date: ${newestDate}`);\n  console.log(`Newest log details:`, newestLog);\n  \n  // Output 0: Order IDs do przetworzenia\n  output0 = orderIds.map((orderId, index) => ({\n    json: {\n      orderId: orderId,\n      totalCount: orderIds.length,\n      currentIndex: index + 1\n    }\n  }));\n  \n  // Output 1: Dane do update Google Sheets (4 wiersze - dodałem newest_date)\n  output1 = [\n    {\n      json: {\n        key: 'last_log_id',\n        value: newLastLogId.toString(),\n        row_number: 2\n      }\n    },\n    {\n      json: {\n        key: 'last_sync',\n        value: new Date().toISOString(),\n        row_number: 3\n      }\n    },\n    {\n      json: {\n        key: 'total_orders',\n        value: orderIds.length.toString(),\n        row_number: 4\n      }\n    },\n    {\n      json: {\n        key: 'newest_log_date',\n        value: newestDate,\n        row_number: 5\n      }\n    }\n  ];\n} else {\n  console.log(\"No new orders to process\");\n}\n\n// Zwróć dane dla obu outputów\nreturn {\n  0: output0,  // Idzie do Loop Over Items\n  1: output1   // Idzie do Google Sheets Update\n};"},"id":"b28a21d8-2e1d-4af5-95fe-313744f427ea","name":"Extract Order IDs & Prepare Update","type":"n8n-nodes-base.code","position":[944,-272],"typeVersion":2},{"parameters":{},"id":"6a75883f-a764-487d-943c-0d9aabb925c2","name":"No Changes - Stop","type":"n8n-nodes-base.noOp","position":[944,112],"typeVersion":1},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":1},"conditions":[{"leftValue":"={{ $json.logs?.length }}","rightValue":0,"operator":{"type":"number","operation":"gt"},"id":"fb5b9b41-36c6-408f-a7ce-41f8c97e3146"}],"combinator":"and"},"options":{}},"id":"f10b58dc-75a8-4be2-8b34-1a4070c97f71","name":"Check If Changes Exist","type":"n8n-nodes-base.if","position":[720,-176],"typeVersion":2},{"parameters":{},"type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[-96,-176],"id":"27acb096-8bae-4f19-9237-5139dd7dc10f","name":"When clicking ‘Execute workflow’"},{"parameters":{"jsCode":"// Set date range for historical retrieval\n  const daysBack = 60; // Last 30 days\n  const dateFrom = Math.floor(Date.now() / 1000) - (daysBack * 24 * 60 * 60);\n  const dateTo = Math.floor(Date.now() / 1000); // Now\n\n  console.log(`Fetching orders from: ${new Date(dateFrom * 1000).toISOString()}`);\n  console.log(`To: ${new Date(dateTo * 1000).toISOString()}`);\n  console.log(`Days back: ${daysBack}`);\n\n  return [{\n    json: {\n      dateFrom: dateFrom,\n      dateTo: dateTo,\n      daysBack: daysBack\n    }\n  }]"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[112,-176],"id":"2e55bcec-9628-4f92-802d-dc31f9d01247","name":"Code in JavaScript1"},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getOrders"},{"name":"parameters","value":"={{ JSON.stringify({ date_confirmed_from: $json.dateFrom, get_unconfirmed_orders: false }) }}"}]},"options":{}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.3,"position":[320,-176],"id":"137fe081-3884-42b0-9c47-4f4ded00479a","name":"Get Historical Orders"},{"parameters":{"jsCode":"// Transform getOrders response to logs format expected by downstream nodes\n  const items = $input.all();\n  const response = items[0]?.json;\n\n  // Get orders from response\n  const orders = response?.orders || [];\n\n  console.log('=== TRANSFORM GETORDERS TO LOGS FORMAT ===');\n  console.log(`Received ${orders.length} orders from getOrders API`);\n\n  if (orders.length === 0) {\n    console.log('⚠️ No orders returned from API');\n    return [{\n      json: {\n        status: \"SUCCESS\",\n        logs: [],\n        _metadata: {\n          source: \"getOrders_historical\",\n          orderCount: 0,\n          processedAt: new Date().toISOString()\n        }\n      }\n    }];\n  }\n\n  // Create synthetic logs array from orders\n  // Each order becomes a log entry\n  const logs = orders.map(order => ({\n    log_id: order.order_id, // Use order_id as log_id\n    log_type: 1, // Type 1 = order event\n    order_id: order.order_id,\n    object_id: 0,\n    date: order.date_confirmed || order.date_add // Use order date\n  }));\n\n  console.log(`✅ Created ${logs.length} log entries from orders`);\n  console.log(`Date range: ${new Date(orders[0].date_confirmed * 1000).toISOString()} to ${new Date(orders[orders.length-1].date_confirmed * 1000).toISOString()}`);\n\n  // Return in format expected by \"Check If Changes Exist\"\n  return [{\n    json: {\n      status: \"SUCCESS\",\n      logs: logs,\n      _metadata: {\n        source: \"getOrders_historical\",\n        orderCount: orders.length,\n        oldestOrder: new Date(orders[0].date_confirmed * 1000).toISOString(),\n        newestOrder: new Date(orders[orders.length-1].date_confirmed * 1000).toISOString(),\n        processedAt: new Date().toISOString()\n      }\n    }\n  }];"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[528,-176],"id":"a0097467-1b8f-409c-b1ab-57fa5a1852ff","name":"Check If Changes Exist1"}],"connections":{"Transform to Synerise Format":{"main":[[{"node":"Prepare CSV Data","type":"main","index":0}]]},"Prepare CSV Data":{"main":[[{"node":"Code in JavaScript","type":"main","index":0}]]},"Extract Product IDs":{"main":[[{"node":"Switch","type":"main","index":0}]]},"Get Product Brands":{"main":[[{"node":"Loop Over Items1","type":"main","index":0}]]},"Merge Product Data":{"main":[[{"node":"Filter Order Status","type":"main","index":0}]]},"Get Product Manufacturers":{"main":[[{"node":"Merge","type":"main","index":0}]]},"Get Order Status List":{"main":[[{"node":"Merge","type":"main","index":3}]]},"Merge":{"main":[[{"node":"Merge Product Data","type":"main","index":0}]]},"Filter Order Status":{"main":[[{"node":"Transform to Synerise Format","type":"main","index":0}]]},"Split Out1":{"main":[[{"node":"Loop Over Items","type":"main","index":0}]]},"Loop Over Items":{"main":[[{"node":"Extract Product IDs","type":"main","index":0}],[{"node":"Get Orders by IDs","type":"main","index":0}]]},"Loop Over Items1":{"main":[[{"node":"Aggregate","type":"main","index":0}],[{"node":"Get Product Brands","type":"main","index":0}]]},"Switch":{"main":[[{"node":"Merge","type":"main","index":1}],[{"node":"Loop Over Items1","type":"main","index":0}]]},"Aggregate":{"main":[[{"node":"Merge","type":"main","index":2}]]},"Get Orders by IDs":{"main":[[{"node":"Filter Online Shop Orders","type":"main","index":0}]]},"Filter Online Shop Orders":{"main":[[{"node":"Loop Over Items","type":"main","index":0}]]},"Extract Order IDs & Prepare Update":{"main":[[{"node":"Split Out1","type":"main","index":0}]]},"Check If Changes Exist":{"main":[[{"node":"Get Order Status List","type":"main","index":0},{"node":"Get Product Manufacturers","type":"main","index":0},{"node":"Extract Order IDs & Prepare Update","type":"main","index":0}],[{"node":"No Changes - Stop","type":"main","index":0}]]},"When clicking ‘Execute workflow’":{"main":[[{"node":"Code in JavaScript1","type":"main","index":0}]]},"Code in JavaScript1":{"main":[[{"node":"Get Historical Orders","type":"main","index":0}]]},"Get Historical Orders":{"main":[[{"node":"Check If Changes Exist1","type":"main","index":0}]]},"Check If Changes Exist1":{"main":[[{"node":"Check If Changes Exist","type":"main","index":0}]]}},"settings":{"saveManualExecutions":true,"executionOrder":"v1","callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":{"node:Schedule Trigger - Every 15 min":{"recurrenceRules":[]}},"meta":{"templateCredsSetupCompleted":true},"pinData":{},"versionId":"6fa2e14e-afc7-4777-83f2-7cb2e286a5fd","versionCounter":15,"triggerCount":1,"shared":[{"updatedAt":"2025-11-20T14:44:24.861Z","createdAt":"2025-11-20T14:44:24.861Z","role":"workflow:owner","workflowId":"ZAZxVj2143XdVvOe","projectId":"jlSr7IBJt4siEqiM","project":{"updatedAt":"2025-01-31T18:30:45.852Z","createdAt":"2025-01-31T18:13:38.763Z","id":"jlSr7IBJt4siEqiM","name":"N8N Estyl <n8n@estyl.pl>","type":"personal","icon":null,"description":null,"projectRelations":[{"updatedAt":"2025-01-31T18:13:38.763Z","createdAt":"2025-01-31T18:13:38.763Z","userId":"5089e5a8-9975-48ff-8451-f5c3fb3e78ac","projectId":"jlSr7IBJt4siEqiM","user":{"updatedAt":"2025-11-19T23:00:50.363Z","createdAt":"2025-01-31T18:13:37.839Z","id":"5089e5a8-9975-48ff-8451-f5c3fb3e78ac","email":"n8n@estyl.pl","firstName":"N8N","lastName":"Estyl","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2025-01-31T18:30:49.691Z","personalization_survey_n8n_version":"1.76.1"},"settings":{"userActivated":true,"firstSuccessfulWorkflowId":"xbn9AqbzxO9Xbruw","userActivatedAt":1739378108329,"easyAIWorkflowOnboarded":true,"npsSurvey":{"responded":true,"lastShownAt":1758812569793}},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2025-11-19","isPending":false}}]}}],"tags":[]}

operations:
  [{"type":"addNode","node":{"id":"pagination-read-state","name":"Read Pagination State","type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[112,-400],"parameters":{"resource":"row","operation":"get","dataTableId":{"mode":"id","value":"pagination_state_july2020"},"returnAll":false,"limit":1,"filters":{"conditions":[{"keyName":"id","condition":"eq","keyValue":"1"}]}}}}]


workflow_id: ZAZxVj2143XdVvOe
workflow_name: BaseLinker to Synerise CSV Export - historical backfill
trigger: partial_update
created_at: 2025-11-20 15:44:44
workflow_snapshot:
  {"updatedAt":"2025-11-20T15:11:20.870Z","createdAt":"2025-11-20T14:44:24.861Z","id":"ZAZxVj2143XdVvOe","name":"BaseLinker to Synerise CSV Export - historical backfill","description":"test","active":false,"isArchived":false,"nodes":[{"parameters":{"jsCode":"// Transform BaseLinker orders to Synerise CSV format\n  // Mode: Run Once for All Items\n  // FIXED: orderId now uses shop_order_id (short format) instead of order_id\n  // FIXED: recordedAt now uses correct UTC timestamps (removed incorrect +2h offset)\n  // FIXED: totalAmount now calculates from products + delivery (not payment_done which is 0 for COD)\n\n  const items = $input.all();\n  const results = [];\n\n  console.log(`Processing ${items.length} orders for Synerise export`);\n\n  // Map status using the enriched status_name\n  const statusMapping = {\n    'ZREALIZOWANE': 'completed',\n    'ANULOWANE': 'cancelled',\n    'ZWROT': 'returned',\n    'WYSŁANE': 'shipped'\n  };\n\n  // Process each order\n  items.forEach((item, index) => {\n    try {\n      const order = item.json;\n\n      // Generate eventSalt\n      const eventSalt = `${order.order_id}_${order.date_add || Date.now()}`;\n\n      // TIMEZONE FIX: BaseLinker returns date_confirmed as Unix timestamp in UTC\n      // No offset needed - UTC is the correct format for data systems\n      // Synerise will handle timezone conversion for display purposes\n      let recordedAt;\n      if (order.date_confirmed) {\n        const timestamp = parseInt(order.date_confirmed);\n        recordedAt = new Date(timestamp * 1000).toISOString();\n\n        // Debug log for first order\n        if (index === 0) {\n          console.log(`Timestamp conversion example:`);\n          console.log(`  Unix timestamp: ${timestamp}`);\n          console.log(`  UTC ISO format: ${recordedAt}`);\n        }\n      } else {\n        recordedAt = new Date().toISOString();\n      }\n\n      // Find appropriate status based on name\n      let orderStatus = 'unknown';\n      const statusNameUpper = (order.status_name || '').toUpperCase();\n      for (const [key, value] of Object.entries(statusMapping)) {\n        if (statusNameUpper.includes(key)) {\n          orderStatus = value;\n          break;\n        }\n      }\n\n      // Determine payment type\n      const paymentType = order.payment_method_cod === '1' ? 'Pobranie' : 'Przelew';\n\n      // Safe parseFloat\n      const safeParseFloat = (value) => {\n        const parsed = parseFloat(value);\n        return isNaN(parsed) ? 0 : parsed;\n      };\n\n      // BUGFIX #3: Calculate totalAmount from products + delivery\n      // BaseLinker API doesn't provide order total - must calculate it\n      // payment_done shows 0 for COD orders (unpaid), but order still has value\n      let totalAmount = 0;\n\n      // Calculate products subtotal\n      if (order.products && Array.isArray(order.products)) {\n        const productsSubtotal = order.products.reduce((sum, product) => {\n          const price = safeParseFloat(product.price_brutto);\n          const quantity = parseInt(product.quantity) || 0;\n          return sum + (price * quantity);\n        }, 0);\n\n        // Add delivery cost\n        totalAmount = productsSubtotal + safeParseFloat(order.delivery_price);\n\n        // Debug log for first order\n        if (index === 0) {\n          console.log(`Total amount calculation example:`);\n          console.log(`  Products subtotal: ${productsSubtotal.toFixed(2)}`);\n          console.log(`  Delivery price: ${safeParseFloat(order.delivery_price).toFixed(2)}`);\n          console.log(`  Total amount: ${totalAmount.toFixed(2)}`);\n          console.log(`  Payment done (old approach): ${safeParseFloat(order.payment_done)}`);\n        }\n      } else {\n        // Fallback: if no products array, use delivery_price only\n        totalAmount = safeParseFloat(order.delivery_price);\n      }\n\n      // Extract client name with fallbacks\n      const clientName = order.delivery_fullname ||\n                        order.invoice_fullname ||\n                        order.delivery_company ||\n                        `${order.delivery_name || ''} ${order.delivery_surname || ''}`.trim() ||\n                        'Unknown Customer';\n\n      // BUGFIX #1: Use shop_order_id (short format) instead of order_id\n      // shop_order_id examples: 184031, 184042 (human-readable short numbers)\n      // order_id examples: 790964970, 790932859 (internal BaseLinker IDs)\n      const orderId = order.shop_order_id || order.order_id; // Fallback to order_id if shop_order_id is missing\n\n      // Debug log for first order to verify fix\n      if (index === 0) {\n        console.log(`Order ID fix example:`);\n        console.log(`  OLD (order_id): ${order.order_id}`);\n        console.log(`  NEW (shop_order_id): ${order.shop_order_id}`);\n        console.log(`  Using: ${orderId}`);\n      }\n\n      // Add transformed order to results\n      results.push({\n        json: {\n          eventSalt: eventSalt,\n          orderId: orderId,\n          recordedAt: recordedAt, // FIXED: Now correct UTC format without offset\n          clientEmail: order.email || '',\n          clientName: clientName,\n          clientPhone: order.phone || '',\n          status: orderStatus,\n          statusName: order.status_name || '',\n          totalAmount: Math.round(totalAmount * 100) / 100, // FIXED: Calculated from products + delivery\n          currency: order.currency || 'PLN',\n          paymentMethod: order.payment_method || '',\n          paymentType: paymentType,\n          deliveryMethod: order.delivery_method || '',\n          deliveryPrice: safeParseFloat(order.delivery_price),\n          products: order.products || [],\n          source: order.order_source || 'BaseLinker',\n          notes: order.admin_comments || ''\n        }\n      });\n\n    } catch (error) {\n      console.error(`Error processing order at index ${index}:`, error);\n      console.error(`Order data:`, JSON.stringify(item.json).substring(0, 500));\n\n      // Add failed order with error info\n      results.push({\n        json: {\n          error: true,\n          errorMessage: error.message,\n          orderId: item.json?.order_id || 'UNKNOWN',\n          originalData: item.json\n        }\n      });\n    }\n  });\n\n  console.log(`Successfully transformed ${results.length} orders`);\n  console.log(`Sample order IDs: ${results.slice(0, 5).map(r => r.json.orderId).join(', ')}`);\n\n  // Return all transformed items\n  return results;\n"},"id":"4dc6cd3a-4e59-45f5-90ce-035791d545b4","name":"Transform to Synerise Format","type":"n8n-nodes-base.code","position":[3184,-448],"typeVersion":2},{"parameters":{"jsCode":"// Flatten nested product data for CSV\n// UPDATED: 16 columns (removed: eventSalt, clientName, clientPhone, productCategory, productEan, source, notes)\nconst items = $input.all();\nconst csvRows = [];\n\nitems.forEach(item => {\n  const order = item.json;\n  \n  // If order has products, create a row for each product\n  if (order.products && order.products.length > 0) {\n    order.products.forEach(product => {\n      csvRows.push({\n        orderId: order.orderId,\n        recordedAt: order.recordedAt,\n        clientEmail: order.clientEmail,\n        status: order.status,\n        statusName: order.statusName,\n        totalAmount: order.totalAmount,\n        currency: order.currency,\n        productSku: product.sku || product.product_id,\n        productName: product.name,\n        productBrand: product.brand || 'Unknown',\n        productPrice: product.price_brutto,\n        productQuantity: product.quantity,\n        paymentMethod: order.paymentMethod,\n        paymentType: order.paymentType,\n        deliveryMethod: order.deliveryMethod,\n        deliveryPrice: order.deliveryPrice\n      });\n    });\n  } else {\n    // Order without products\n    csvRows.push({\n      orderId: order.orderId,\n      recordedAt: order.recordedAt,\n      clientEmail: order.clientEmail,\n      status: order.status,\n      statusName: order.statusName,\n      totalAmount: order.totalAmount,\n      currency: order.currency,\n      productSku: '',\n      productName: '',\n      productBrand: '',\n      productPrice: '',\n      productQuantity: '',\n      paymentMethod: order.paymentMethod,\n      paymentType: order.paymentType,\n      deliveryMethod: order.deliveryMethod,\n      deliveryPrice: order.deliveryPrice\n    });\n  }\n});\n\n// Generate filename with timestamp\nconst now = new Date();\nconst filename = `synerise_export_${now.toISOString().slice(0,10)}_${now.toTimeString().slice(0,5).replace(':','')}.csv`;\n\nreturn [{\n  json: {\n    filename: filename,\n    rowCount: csvRows.length,\n    exportTime: now.toISOString(),\n    data: csvRows\n  }\n}];"},"id":"6c52f816-b911-4c5d-9264-b66c81c5f06a","name":"Prepare CSV Data","type":"n8n-nodes-base.code","position":[3408,-448],"typeVersion":2},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getOrders"},{"name":"parameters","value":"={{ JSON.stringify({ order_id: parseInt($json.json.orderId), get_unconfirmed_orders: false }) }}"}]},"options":{"timeout":30000}},"id":"15ca7ed2-b332-49ee-8168-ae2c30ae43c3","name":"Get Orders by IDs","type":"n8n-nodes-base.httpRequest","position":[1616,-32],"typeVersion":4.2,"retryOnFail":true,"maxTries":5,"waitBetweenTries":5000},{"parameters":{"jsCode":"// Extract Product IDs - WERSJA Z JEDNYM OUTPUTEM I RÓŻNYMI TYPAMI\nconst items = $input.all();\nconst allProductIds = new Set();\nconst allOrders = [];\n\n// Aggregate node zwraca wszystko w polu \"data\"\nconst aggregatedData = items[0]?.json?.data;\n\nif (aggregatedData && Array.isArray(aggregatedData)) {\n  // Sprawdź strukturę danych z Aggregate\n  aggregatedData.forEach(item => {\n    // Jeśli każdy item ma pole \"orders\" (batche z BaseLinker)\n    if (item.orders && Array.isArray(item.orders)) {\n      allOrders.push(...item.orders);\n    } \n    // Jeśli to bezpośrednio zamówienia (spłaszczone)\n    else if (item.order_id) {\n      allOrders.push(item);\n    }\n    // Jeśli to zagnieżdżone w json\n    else if (item.json?.orders) {\n      allOrders.push(...item.json.orders);\n    }\n    else if (item.json?.order_id) {\n      allOrders.push(item.json);\n    }\n  });\n} \n// Fallback - może dane są bezpośrednio w items\nelse {\n  items.forEach(item => {\n    if (item.json?.orders) {\n      allOrders.push(...item.json.orders);\n    } else if (item.json?.order_id) {\n      allOrders.push(item.json);\n    }\n  });\n}\n\n// Zbierz wszystkie unikalne product IDs\nallOrders.forEach(order => {\n  if (order.products && Array.isArray(order.products)) {\n    order.products.forEach(product => {\n      if (product.product_id) {\n        allProductIds.add(parseInt(product.product_id));\n      }\n    });\n  }\n});\n\nconst productIdsArray = Array.from(allProductIds);\n\n// Podziel produkty na batche po 100\nconst BATCH_SIZE = 100;\nconst productBatches = [];\nfor (let i = 0; i < productIdsArray.length; i += BATCH_SIZE) {\n  productBatches.push(productIdsArray.slice(i, i + BATCH_SIZE));\n}\n\nconsole.log('=== EXTRACT PRODUCT IDS ===');\nconsole.log(`Total orders: ${allOrders.length}`);\nconsole.log(`Unique products: ${productIdsArray.length}`);\nconsole.log(`Product batches: ${productBatches.length} (${BATCH_SIZE} per batch)`);\nproductBatches.forEach((batch, index) => {\n  console.log(`  Batch ${index + 1}: ${batch.length} products`);\n});\n\n// Przygotuj output items\nconst outputItems = [];\n\n// PIERWSZY ITEM: Zamówienia (dla Merge)\noutputItems.push({\n  json: {\n    type: 'ordersData',  // Unikalny typ dla Switch\n    orders: allOrders,\n    stats: {\n      batchOrderCount: allOrders.length,\n      uniqueProductCount: allProductIds.size,\n      totalBatches: productBatches.length,\n      avgProductsPerOrder: (allOrders.reduce((sum, o) => sum + (o.products?.length || 0), 0) / allOrders.length).toFixed(2)\n    }\n  }\n});\n\n// KOLEJNE ITEMY: Każdy batch produktów (dla Loop/Get Product Brands)\nproductBatches.forEach((batch, index) => {\n  outputItems.push({\n    json: {\n      type: 'productBatch',  // Unikalny typ dla Switch\n      batchIndex: index + 1,\n      totalBatches: productBatches.length,\n      productIdsToEnrich: batch,\n      batchSize: batch.length\n    }\n  });\n});\n\nconsole.log(`Returning ${outputItems.length} items: 1 ordersData + ${productBatches.length} productBatch(es)`);\n\nreturn outputItems;"},"id":"ec3603bf-23ec-4859-98d3-354391f11e8e","name":"Extract Product IDs","type":"n8n-nodes-base.code","typeVersion":2,"position":[1616,-272]},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getInventoryProductsData"},{"name":"parameters","value":"={{ JSON.stringify({ \n  inventory_id: 758, \n  products: $json.productIdsToEnrich || [] \n}) }}"}]},"options":{"timeout":30000}},"id":"3417e7b6-d432-4e68-8500-6ea48ce52427","name":"Get Product Brands","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2288,-64],"retryOnFail":true},{"parameters":{"jsCode":"// FIXED: Merge Product Data - Correctly extracts data from Merge node\n// This version properly handles the 4 inputs from the Merge node\n\nconst allInputs = $input.all();\nconsole.log(`=== MERGE PRODUCT DATA - DEBUGGING ===`);\nconsole.log(`Total inputs received: ${allInputs.length}`);\n\n// Initialize data storage\nlet manufacturersData = [];\nlet ordersData = [];\nlet productsData = {};\nlet statusesData = [];\n\n// The Merge node combines 4 inputs:\n// Input 0: Product Manufacturers\n// Input 1: Orders (from Switch - ordersData branch)  \n// Input 2: Aggregated Products\n// Input 3: Order Status List\n\n// Process each input and extract the correct data\nallInputs.forEach((item, index) => {\n  const data = item.json;\n  \n  console.log(`\\nInput ${index} analysis:`, {\n    hasManufacturers: !!(data.manufacturers || data.manufacturer),\n    hasOrders: !!(data.orders || data.type === 'ordersData'),\n    hasProducts: !!(data.products || data.data),\n    hasStatuses: !!(data.statuses || data.status),\n    topKeys: Object.keys(data).slice(0, 5),\n    dataType: data.type || 'unknown'\n  });\n  \n  // Extract manufacturers (usually first input from Get Product Manufacturers)\n  if (data.manufacturers && Array.isArray(data.manufacturers)) {\n    manufacturersData = data.manufacturers;\n    console.log(`✓ Found ${manufacturersData.length} manufacturers`);\n  }\n  \n  // Extract orders (from ordersData type)\n  if (data.type === 'ordersData' && data.orders) {\n    ordersData = data.orders;\n    console.log(`✓ Found ${ordersData.length} orders from ordersData`);\n  } else if (data.orders && Array.isArray(data.orders)) {\n    ordersData = data.orders;\n    console.log(`✓ Found ${ordersData.length} orders`);\n  }\n  \n  // Extract products from aggregated data\n  if (data.data && Array.isArray(data.data)) {\n    // This is the aggregated products output\n    console.log(`✓ Found aggregated data with ${data.data.length} items`);\n    data.data.forEach(item => {\n      if (item.products && typeof item.products === 'object') {\n        Object.assign(productsData, item.products);\n      }\n    });\n  } else if (data.products && typeof data.products === 'object') {\n    // Direct products object\n    Object.assign(productsData, data.products);\n  }\n  \n  // Extract statuses\n  if (data.statuses && Array.isArray(data.statuses)) {\n    statusesData = data.statuses;\n    console.log(`✓ Found ${statusesData.length} order statuses`);\n  }\n});\n\n// Create manufacturer mapping (ID → Name)\nconst manufacturerMap = {};\nmanufacturersData.forEach(manufacturer => {\n  const id = String(manufacturer.manufacturer_id);\n  manufacturerMap[id] = manufacturer.name;\n});\n\nconsole.log(`\\n=== DATA SUMMARY ===`);\nconsole.log(`Manufacturers: ${Object.keys(manufacturerMap).length}`);\nconsole.log(`Sample manufacturers:`, Object.entries(manufacturerMap).slice(0, 5));\nconsole.log(`Products with brand data: ${Object.keys(productsData).length}`);\nconsole.log(`Orders to process: ${ordersData.length}`);\nconsole.log(`Statuses: ${statusesData.length}`);\n\n// Create status mapping\nconst statusMap = {};\nstatusesData.forEach(status => {\n  statusMap[String(status.id)] = status.name;\n});\n\n// Statistics\nlet stats = {\n  totalProducts: 0,\n  productsWithBrand: 0,\n  productsWithoutBrand: 0,\n  missingProductData: 0,\n  uniqueBrands: new Set(),\n  missingProductIds: new Set()\n};\n\n// Process and enrich orders\nconst enrichedOrders = ordersData.map((order, orderIndex) => {\n  // Clone the order to avoid mutation\n  const enrichedOrder = JSON.parse(JSON.stringify(order));\n  \n  // Add status name\n  enrichedOrder.status_name = statusMap[String(order.order_status_id)] || 'Unknown';\n  \n  // Process products in the order\n  if (enrichedOrder.products && Array.isArray(enrichedOrder.products)) {\n    enrichedOrder.products = enrichedOrder.products.map((product, productIndex) => {\n      stats.totalProducts++;\n      \n      const productIdStr = String(product.product_id);\n      const productData = productsData[productIdStr];\n      \n      // Initialize brand as Unknown\n      let brandName = 'Unknown';\n      let categoryName = '';\n      let ean = product.ean || '';\n      \n      if (productData) {\n        // Get manufacturer/brand name\n        if (productData.manufacturer_id) {\n          const manufacturerIdStr = String(productData.manufacturer_id);\n          const manufacturer = manufacturerMap[manufacturerIdStr];\n          \n          if (manufacturer) {\n            brandName = manufacturer;\n            stats.uniqueBrands.add(brandName);\n            stats.productsWithBrand++;\n          } else {\n            console.warn(`Manufacturer ID ${manufacturerIdStr} not found in map for product ${productIdStr}`);\n            stats.productsWithoutBrand++;\n          }\n        } else {\n          stats.productsWithoutBrand++;\n        }\n        \n        // Get category from text_fields\n        if (productData.text_fields) {\n          categoryName = productData.text_fields.extra_field_851 || '';\n        }\n        \n        // Get EAN\n        ean = productData.ean || product.ean || '';\n        \n      } else {\n        stats.missingProductData++;\n        stats.missingProductIds.add(productIdStr);\n        \n        if (orderIndex < 3 && productIndex < 2) {\n          console.warn(`No product data found for ID: ${productIdStr}`);\n        }\n      }\n      \n      // Debug first few products\n      if (orderIndex === 0 && productIndex < 3) {\n        console.log(`\\nSample enrichment - Product ${productIdStr}:`, {\n          originalName: product.name,\n          brand: brandName,\n          hasProductData: !!productData,\n          manufacturerId: productData?.manufacturer_id,\n          category: categoryName ? categoryName.substring(0, 50) + '...' : 'None'\n        });\n      }\n      \n      // Return enriched product\n      return {\n        ...product,\n        brand: brandName,\n        category_name: categoryName,\n        ean: ean\n      };\n    });\n  }\n  \n  return enrichedOrder;\n});\n\n// Calculate coverage\nconst coverage = stats.totalProducts > 0 \n  ? ((stats.productsWithBrand / stats.totalProducts) * 100).toFixed(1)\n  : 0;\n\nconsole.log(`\\n=== ENRICHMENT RESULTS ===`);\nconsole.log(`Total products: ${stats.totalProducts}`);\nconsole.log(`Products with brands: ${stats.productsWithBrand} (${coverage}%)`);\nconsole.log(`Products without brands: ${stats.productsWithoutBrand}`);\nconsole.log(`Products missing data: ${stats.missingProductData}`);\nconsole.log(`Unique brands found: ${stats.uniqueBrands.size}`);\nconsole.log(`Brand names:`, Array.from(stats.uniqueBrands).slice(0, 10).join(', '));\n\nif (stats.missingProductData > 0) {\n  console.warn(`\\n⚠️ Missing product IDs (first 10):`, \n    Array.from(stats.missingProductIds).slice(0, 10).join(', '));\n}\n\n// Return results\nconst output = [];\n\n// Add statistics as first item\noutput.push({\n  json: {\n    _type: 'statistics',\n    _timestamp: new Date().toISOString(),\n    coverage: {\n      total_products: stats.totalProducts,\n      with_brand: stats.productsWithBrand,\n      without_brand: stats.productsWithoutBrand,\n      missing_data: stats.missingProductData,\n      coverage_percent: `${coverage}%`,\n      unique_brands: stats.uniqueBrands.size\n    },\n    brands: Array.from(stats.uniqueBrands),\n    missing_ids: Array.from(stats.missingProductIds).slice(0, 20)\n  }\n});\n\n// Add enriched orders\nenrichedOrders.forEach(order => {\n  output.push({ json: order });\n});\n\nreturn output;"},"id":"aa52a712-23fe-42e2-b71a-414707430739","name":"Merge Product Data","type":"n8n-nodes-base.code","typeVersion":2,"position":[2736,-448]},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getInventoryManufacturers"},{"name":"parameters","value":"{\"inventory_id\":758}"}]},"options":{"timeout":30000}},"id":"8098ddd8-f311-48f1-b7e0-bc13de4a4712","name":"Get Product Manufacturers","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2288,-704],"retryOnFail":true},{"parameters":{"mode":"combine","combineBy":"combineByPosition","numberInputs":4,"options":{}},"type":"n8n-nodes-base.merge","typeVersion":3.2,"position":[2512,-480],"id":"5e3a6a8e-44a2-444a-a3bc-6d162dd4cf94","name":"Merge"},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getOrderStatusList"},{"name":"parameters","value":"{}"}]},"options":{}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2288,256],"id":"e923ab81-95d9-43c5-9271-e76a4fecd0aa","name":"Get Order Status List","retryOnFail":true},{"parameters":{"jsCode":"// Konfiguracja dozwolonych statusów - zgodnie z wymaganiami klienta\n// Tylko statusy końcowe: zrealizowane, anulowane, zwroty\n\nconst allowedStatuses = [\n  // === WYSŁANE/ZREALIZOWANE/DOSTARCZONE ===\n  300027, // WYSŁANE // .PL\n  300028, // WYSŁANE // _PL\n  361867, // DE // Wysłane\n  386333, // SM // WYSŁANE\n  354789, // Dostarczone\n  354788, // Paczka w punkcie\n  300828, // OO // Odebrane\n  \n  // === ANULOWANE ===\n  301588, // Anulowane (przez klienta)\n  302472, // Anulowane (brak wpłaty)\n  302473, // Anulowane (zwrot paczki)\n  302475, // Anulowane (brak towaru)\n  303313, // Anulowane STA\n  304745, // Anulacja przed wysyłką\n  316385, // Anulacje (testowe)\n  359429, // WMS Anulowane\n  302981, // [Optima] Anulacja RO\n  \n  // === ZWROTY ===\n  300346, // ZWR // Przedpłacone\n  303161, // ZWR // Deklaracja zwrotu\n  311537, // SOK // Zwroty paczek\n  354792, // Zwrot kurierski\n  372310, // MolZwrotPrzyjęty [sok] zwrot$\n  374054, // Zwroty MADS (Hebe i Mitago_EU)\n  374055, // Zwroty MAGU Kau+Pigu\n  356655, // [MOL] Zwroty częściowe\n  356674, // EXC zwrócone\n  \n  // === NIEDOSTARCZONE (może być traktowane jako anulowane) ===\n  354791  // Niedostarczone\n];\n\n// Mapowanie kategorii statusów dla lepszego raportowania\nconst statusCategories = {\n  'ZREALIZOWANE': [300027, 300028, 361867, 386333, 354789, 354788, 300828],\n  'ANULOWANE': [301588, 302472, 302473, 302475, 303313, 304745, 316385, 359429, 302981],\n  'ZWROTY': [300346, 303161, 311537, 354792, 372310, 374054, 374055, 356655, 356674],\n  'NIEDOSTARCZONE': [354791]\n};\n\n// Pobierz wszystkie zamówienia\nconst items = $input.all();\n\nconsole.log(\"=== FILTER ORDER STATUS ===\");\nconsole.log(`Total orders received: ${items.length}`);\n\n// Zbierz statystyki\nconst statusStats = {};\nconst categoryStats = {\n  'ZREALIZOWANE': 0,\n  'ANULOWANE': 0,\n  'ZWROTY': 0,\n  'NIEDOSTARCZONE': 0,\n  'OTHER': 0\n};\n\nlet passedCount = 0;\nlet filteredCount = 0;\n\n// Filtruj zamówienia\nconst filteredOrders = items.filter(item => {\n  const order = item.json;\n  const statusId = parseInt(order.order_status_id);\n  const statusName = order.status_name || 'Unknown';\n  \n  // Zbierz statystyki szczegółowe\n  const key = `${statusId} - ${statusName}`;\n  statusStats[key] = (statusStats[key] || 0) + 1;\n  \n  // Określ kategorię statusu\n  let category = 'OTHER';\n  for (const [cat, ids] of Object.entries(statusCategories)) {\n    if (ids.includes(statusId)) {\n      category = cat;\n      break;\n    }\n  }\n  categoryStats[category]++;\n  \n  // Sprawdź czy status jest dozwolony\n  const isAllowed = allowedStatuses.includes(statusId);\n  \n  if (isAllowed) {\n    passedCount++;\n    console.log(`  ✅ Order ${order.order_id}: Status ${statusId} (${statusName}) - ${category}`);\n  } else {\n    filteredCount++;\n    if (filteredCount <= 5) { // Pokaż tylko pierwsze 5 odrzuconych\n      console.log(`  ❌ Order ${order.order_id}: Status ${statusId} (${statusName}) - FILTERED OUT`);\n    }\n  }\n  \n  return isAllowed;\n});\n\n// Raport podsumowujący\nconsole.log(\"\\n=== STATUS CATEGORY SUMMARY ===\");\nconsole.log(`📦 Zrealizowane: ${categoryStats.ZREALIZOWANE} orders`);\nconsole.log(`❌ Anulowane: ${categoryStats.ANULOWANE} orders`);\nconsole.log(`↩️ Zwroty: ${categoryStats.ZWROTY} orders`);\nconsole.log(`⚠️ Niedostarczone: ${categoryStats.NIEDOSTARCZONE} orders`);\nconsole.log(`❓ Inne (odrzucone): ${categoryStats.OTHER} orders`);\n\nconsole.log(\"\\n=== DETAILED STATUS DISTRIBUTION ===\");\n// Sortuj statusy według liczby zamówień\nconst sortedStats = Object.entries(statusStats)\n  .sort((a, b) => b[1] - a[1])\n  .slice(0, 10); // Pokaż top 10\n\nsortedStats.forEach(([status, count]) => {\n  const statusId = parseInt(status.split(' ')[0]);\n  const allowed = allowedStatuses.includes(statusId) ? '✅' : '❌';\n  \n  // Znajdź kategorię\n  let category = 'OTHER';\n  for (const [cat, ids] of Object.entries(statusCategories)) {\n    if (ids.includes(statusId)) {\n      category = cat;\n      break;\n    }\n  }\n  \n  console.log(`  ${allowed} ${status}: ${count} orders [${category}]`);\n});\n\nconsole.log(`\\n=== FINAL SUMMARY ===`);\nconsole.log(`✅ Passed to Synerise: ${passedCount} orders`);\nconsole.log(`❌ Filtered out: ${filteredCount} orders`);\nconsole.log(`📊 Filter efficiency: ${((passedCount/items.length)*100).toFixed(1)}%`);\n\n// Ostrzeżenie jeśli wszystko odrzucone\nif (passedCount === 0 && items.length > 0) {\n  console.error(\"⚠️ WARNING: All orders were filtered out! Check status configuration.\");\n}\n\n// WAŻNE: Zwróć przefiltrowane zamówienia jako items\nreturn filteredOrders;"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[2960,-448],"id":"48ed4ca2-613f-4699-9553-6a59168168e5","name":"Filter Order Status"},{"parameters":{"jsCode":"// Generate CSV with semicolon delimiter (16 columns)\n// UPDATED: Changed from 23 columns (comma) to 16 columns (semicolon)\nconst input = $input.all()[0].json;\nconst csvRows = input.data;\n\n// 16 columns (removed: eventSalt, clientName, clientPhone, productCategory, productEan, source, notes)\nconst headers = [\n  'orderId',\n  'recordedAt',\n  'clientEmail',\n  'status',\n  'statusName',\n  'totalAmount',\n  'currency',\n  'productSku',\n  'productName',\n  'productBrand',\n  'productPrice',\n  'productQuantity',\n  'paymentMethod',\n  'paymentType',\n  'deliveryMethod',\n  'deliveryPrice'\n];\n\n// Function to escape CSV values for semicolon delimiter\nfunction escapeCsvValue(value) {\n  if (value === null || value === undefined) return '';\n  const stringValue = String(value);\n  \n  // If contains semicolon, double quote, or newline, wrap in quotes\n  if (stringValue.includes(';') || stringValue.includes('\"') || stringValue.includes('\\n')) {\n    return `\"${stringValue.replace(/\"/g, '\"\"')}\"`;\n  }\n  return stringValue;\n}\n\n// Generate CSV with SEMICOLON delimiter\nlet csvContent = headers.join(';') + '\\n';\n\n// Add data rows\ncsvRows.forEach(row => {\n  const values = headers.map(header => escapeCsvValue(row[header]));\n  csvContent += values.join(';') + '\\n';\n});\n\nconsole.log(`Generated CSV: ${csvRows.length} rows, 16 columns, semicolon delimiter`);\n\n// Return as binary data\nreturn {\n  json: {\n    filename: input.filename,\n    rowCount: csvRows.length,\n    columnCount: headers.length,\n    delimiter: 'semicolon'\n  },\n  binary: {\n    data: {\n      data: Buffer.from(csvContent, 'utf8').toString('base64'),\n      mimeType: 'text/csv',\n      fileName: input.filename\n    }\n  }\n};"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[3632,-448],"id":"5a820d61-40f2-40eb-8a98-ba40fa84f8d0","name":"Code in JavaScript"},{"parameters":{"fieldToSplitOut":"[\"0\"]","options":{}},"type":"n8n-nodes-base.splitOut","typeVersion":1,"position":[1168,-80],"id":"e9603521-9b07-44ee-8587-503b8cdb1d27","name":"Split Out1"},{"parameters":{"options":{}},"type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[1392,-80],"id":"8880595d-e24f-4e1e-8306-1419e710ba3b","name":"Loop Over Items"},{"parameters":{"options":{}},"type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[2064,-208],"id":"b3546273-74ce-4094-870e-b7aabee04600","name":"Loop Over Items1"},{"parameters":{"rules":{"values":[{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"leftValue":"={{ $json.type }}","rightValue":"ordersData","operator":{"type":"string","operation":"equals"},"id":"16455973-160a-451f-9ce5-d3e00d4ec21c"}],"combinator":"and"},"renameOutput":true,"outputKey":"orders"},{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"id":"d0ff60de-b3c8-45e4-8d2e-b00c0acc1685","leftValue":"={{ $json.type }}","rightValue":"productBatch","operator":{"type":"string","operation":"equals","name":"filter.operator.equals"}}],"combinator":"and"},"renameOutput":true,"outputKey":"product"}]},"options":{}},"type":"n8n-nodes-base.switch","typeVersion":3.3,"position":[1840,-272],"id":"bded8fd0-05ff-4eb6-8d6c-cc30cebe60b9","name":"Switch"},{"parameters":{"aggregate":"aggregateAllItemData","options":{}},"type":"n8n-nodes-base.aggregate","typeVersion":1,"position":[2288,-256],"id":"637e4855-58c9-434e-a41b-86a01d129154","name":"Aggregate"},{"parameters":{"jsCode":"// Filter to include ONLY online shop orders (inclusion-based approach)\n// FIXED: Changed from exclusion list to checking order_source field directly\n// This ensures ALL marketplaces are excluded (Allegro, Kaufland, Hebe, etc.)\n\nconst items = $input.all();\nconst orders = items[0]?.json?.orders || [];\n\nconsole.log('=== FILTER ONLINE SHOP ORDERS (INCLUSION-BASED) ===');\nconsole.log(`Total orders before filtering: ${orders.length}`);\n\n// Statistics\nconst stats = {\n  total: orders.length,\n  onlineShop: 0,\n  marketplace: 0,\n  unknown: 0,\n  bySource: {}\n};\n\n// Filter orders - ONLY include shop orders\nconst filteredOrders = orders.filter(order => {\n  const orderSource = (order.order_source || '').toLowerCase();\n  const orderSourceId = order.order_source_id;\n  \n  // Track statistics by source\n  const sourceName = order.order_source || 'Unknown';\n  stats.bySource[sourceName] = (stats.bySource[sourceName] || 0) + 1;\n  \n  // INCLUSION FILTER: Only accept orders where source is \"shop\"\n  const isShop = orderSource === 'shop' || orderSource.includes('shop');\n  \n  if (isShop) {\n    stats.onlineShop++;\n    if (stats.onlineShop <= 5) {\n      console.log(`  ✅ INCLUDED: Order ${order.order_id} from '${sourceName}' (source_id: ${orderSourceId})`);\n    }\n    return true;\n  } else if (!orderSource || orderSource === 'unknown') {\n    stats.unknown++;\n    if (stats.unknown <= 3) {\n      console.log(`  ⚠️ UNKNOWN: Order ${order.order_id} has unknown/empty source (source_id: ${orderSourceId})`);\n    }\n    return false;\n  } else {\n    stats.marketplace++;\n    if (stats.marketplace <= 5) {\n      console.log(`  ❌ EXCLUDED: Order ${order.order_id} from '${sourceName}' (source_id: ${orderSourceId})`);\n    }\n    return false;\n  }\n});\n\nconsole.log('\\n=== FILTERING RESULTS (INCLUSION-BASED) ===');\nconsole.log(`✅ Online shop orders: ${stats.onlineShop}`);\nconsole.log(`❌ Marketplace orders (excluded): ${stats.marketplace}`);\nconsole.log(`⚠️ Unknown source (excluded): ${stats.unknown}`);\nconsole.log(`📊 Filter efficiency: ${((stats.onlineShop/stats.total)*100).toFixed(1)}% kept`);\n\nconsole.log('\\n=== ORDERS BY SOURCE ===');\nObject.entries(stats.bySource)\n  .sort((a, b) => b[1] - a[1])\n  .forEach(([source, count]) => {\n    const isShop = source.toLowerCase().includes('shop');\n    const emoji = isShop ? '✅' : '❌';\n    console.log(`  ${emoji} ${source}: ${count} orders`);\n  });\n\nconsole.log('\\n=== FILTER APPROACH ===');\nconsole.log('Using INCLUSION-based filter: only order_source=\"shop\" passes');\nconsole.log('This automatically excludes ALL marketplaces:');\nconsole.log('  - Allegro, Kaufland, Hebe, Empik, eMag, etc.');\nconsole.log('  - Any future marketplace integrations');\n\n// Return filtered orders in the same structure\nreturn [{\n  json: {\n    orders: filteredOrders,\n    _filterStats: stats,\n    _filterApproach: 'inclusion-based',\n    _filterCriteria: 'order_source === \"shop\"',\n    _timestamp: new Date().toISOString()\n  }\n}];"},"id":"3312a255-a865-4d0f-b4d4-112045b1767f","name":"Filter Online Shop Orders","type":"n8n-nodes-base.code","typeVersion":2,"position":[1840,16]},{"parameters":{"jsCode":"// Extract unique order IDs from journal\nconst items = $input.all();\nconst logs = items[0]?.json?.logs || [];\n\n// Get unique order IDs\nconst orderIds = [...new Set(logs.map(log => log.order_id).filter(id => id))];\n\n// Przygotuj outputy\nlet output0 = []; // Order IDs do przetworzenia\nlet output1 = []; // Dane do Google Sheets update\n\nif (logs.length > 0) {\n  // Znajdź log z najwyższą datą (najświeższy)\n  const newestLog = logs.reduce((newest, current) => {\n    return current.date > newest.date ? current : newest;\n  }, logs[0]);\n  \n  const newLastLogId = newestLog.log_id;\n  const newestDate = new Date(newestLog.date * 1000).toISOString();\n  \n  console.log(`Found ${logs.length} logs, ${orderIds.length} unique orders`);\n  console.log(`Newest log_id: ${newLastLogId} from date: ${newestDate}`);\n  console.log(`Newest log details:`, newestLog);\n  \n  // Output 0: Order IDs do przetworzenia\n  output0 = orderIds.map((orderId, index) => ({\n    json: {\n      orderId: orderId,\n      totalCount: orderIds.length,\n      currentIndex: index + 1\n    }\n  }));\n  \n  // Output 1: Dane do update Google Sheets (4 wiersze - dodałem newest_date)\n  output1 = [\n    {\n      json: {\n        key: 'last_log_id',\n        value: newLastLogId.toString(),\n        row_number: 2\n      }\n    },\n    {\n      json: {\n        key: 'last_sync',\n        value: new Date().toISOString(),\n        row_number: 3\n      }\n    },\n    {\n      json: {\n        key: 'total_orders',\n        value: orderIds.length.toString(),\n        row_number: 4\n      }\n    },\n    {\n      json: {\n        key: 'newest_log_date',\n        value: newestDate,\n        row_number: 5\n      }\n    }\n  ];\n} else {\n  console.log(\"No new orders to process\");\n}\n\n// Zwróć dane dla obu outputów\nreturn {\n  0: output0,  // Idzie do Loop Over Items\n  1: output1   // Idzie do Google Sheets Update\n};"},"id":"b28a21d8-2e1d-4af5-95fe-313744f427ea","name":"Extract Order IDs & Prepare Update","type":"n8n-nodes-base.code","position":[944,-272],"typeVersion":2},{"parameters":{},"id":"6a75883f-a764-487d-943c-0d9aabb925c2","name":"No Changes - Stop","type":"n8n-nodes-base.noOp","position":[944,112],"typeVersion":1},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":1},"conditions":[{"leftValue":"={{ $json.logs?.length }}","rightValue":0,"operator":{"type":"number","operation":"gt"},"id":"fb5b9b41-36c6-408f-a7ce-41f8c97e3146"}],"combinator":"and"},"options":{}},"id":"f10b58dc-75a8-4be2-8b34-1a4070c97f71","name":"Check If Changes Exist","type":"n8n-nodes-base.if","position":[720,-176],"typeVersion":2},{"parameters":{},"type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[-96,-176],"id":"27acb096-8bae-4f19-9237-5139dd7dc10f","name":"When clicking ‘Execute workflow’"},{"parameters":{"jsCode":"// Set date range for historical retrieval\n  const daysBack = 60; // Last 30 days\n  const dateFrom = Math.floor(Date.now() / 1000) - (daysBack * 24 * 60 * 60);\n  const dateTo = Math.floor(Date.now() / 1000); // Now\n\n  console.log(`Fetching orders from: ${new Date(dateFrom * 1000).toISOString()}`);\n  console.log(`To: ${new Date(dateTo * 1000).toISOString()}`);\n  console.log(`Days back: ${daysBack}`);\n\n  return [{\n    json: {\n      dateFrom: dateFrom,\n      dateTo: dateTo,\n      daysBack: daysBack\n    }\n  }]"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[112,-176],"id":"2e55bcec-9628-4f92-802d-dc31f9d01247","name":"Code in JavaScript1"},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getOrders"},{"name":"parameters","value":"={{ JSON.stringify({ date_confirmed_from: $json.dateFrom, get_unconfirmed_orders: false }) }}"}]},"options":{}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.3,"position":[320,-176],"id":"137fe081-3884-42b0-9c47-4f4ded00479a","name":"Get Historical Orders"},{"parameters":{"jsCode":"// Transform getOrders response to logs format expected by downstream nodes\n  const items = $input.all();\n  const response = items[0]?.json;\n\n  // Get orders from response\n  const orders = response?.orders || [];\n\n  console.log('=== TRANSFORM GETORDERS TO LOGS FORMAT ===');\n  console.log(`Received ${orders.length} orders from getOrders API`);\n\n  if (orders.length === 0) {\n    console.log('⚠️ No orders returned from API');\n    return [{\n      json: {\n        status: \"SUCCESS\",\n        logs: [],\n        _metadata: {\n          source: \"getOrders_historical\",\n          orderCount: 0,\n          processedAt: new Date().toISOString()\n        }\n      }\n    }];\n  }\n\n  // Create synthetic logs array from orders\n  // Each order becomes a log entry\n  const logs = orders.map(order => ({\n    log_id: order.order_id, // Use order_id as log_id\n    log_type: 1, // Type 1 = order event\n    order_id: order.order_id,\n    object_id: 0,\n    date: order.date_confirmed || order.date_add // Use order date\n  }));\n\n  console.log(`✅ Created ${logs.length} log entries from orders`);\n  console.log(`Date range: ${new Date(orders[0].date_confirmed * 1000).toISOString()} to ${new Date(orders[orders.length-1].date_confirmed * 1000).toISOString()}`);\n\n  // Return in format expected by \"Check If Changes Exist\"\n  return [{\n    json: {\n      status: \"SUCCESS\",\n      logs: logs,\n      _metadata: {\n        source: \"getOrders_historical\",\n        orderCount: orders.length,\n        oldestOrder: new Date(orders[0].date_confirmed * 1000).toISOString(),\n        newestOrder: new Date(orders[orders.length-1].date_confirmed * 1000).toISOString(),\n        processedAt: new Date().toISOString()\n      }\n    }\n  }];"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[528,-176],"id":"a0097467-1b8f-409c-b1ab-57fa5a1852ff","name":"Check If Changes Exist1"}],"connections":{"Transform to Synerise Format":{"main":[[{"node":"Prepare CSV Data","type":"main","index":0}]]},"Prepare CSV Data":{"main":[[{"node":"Code in JavaScript","type":"main","index":0}]]},"Extract Product IDs":{"main":[[{"node":"Switch","type":"main","index":0}]]},"Get Product Brands":{"main":[[{"node":"Loop Over Items1","type":"main","index":0}]]},"Merge Product Data":{"main":[[{"node":"Filter Order Status","type":"main","index":0}]]},"Get Product Manufacturers":{"main":[[{"node":"Merge","type":"main","index":0}]]},"Get Order Status List":{"main":[[{"node":"Merge","type":"main","index":3}]]},"Merge":{"main":[[{"node":"Merge Product Data","type":"main","index":0}]]},"Filter Order Status":{"main":[[{"node":"Transform to Synerise Format","type":"main","index":0}]]},"Split Out1":{"main":[[{"node":"Loop Over Items","type":"main","index":0}]]},"Loop Over Items":{"main":[[{"node":"Extract Product IDs","type":"main","index":0}],[{"node":"Get Orders by IDs","type":"main","index":0}]]},"Loop Over Items1":{"main":[[{"node":"Aggregate","type":"main","index":0}],[{"node":"Get Product Brands","type":"main","index":0}]]},"Switch":{"main":[[{"node":"Merge","type":"main","index":1}],[{"node":"Loop Over Items1","type":"main","index":0}]]},"Aggregate":{"main":[[{"node":"Merge","type":"main","index":2}]]},"Get Orders by IDs":{"main":[[{"node":"Filter Online Shop Orders","type":"main","index":0}]]},"Filter Online Shop Orders":{"main":[[{"node":"Loop Over Items","type":"main","index":0}]]},"Extract Order IDs & Prepare Update":{"main":[[{"node":"Split Out1","type":"main","index":0}]]},"Check If Changes Exist":{"main":[[{"node":"Get Order Status List","type":"main","index":0},{"node":"Get Product Manufacturers","type":"main","index":0},{"node":"Extract Order IDs & Prepare Update","type":"main","index":0}],[{"node":"No Changes - Stop","type":"main","index":0}]]},"When clicking ‘Execute workflow’":{"main":[[{"node":"Code in JavaScript1","type":"main","index":0}]]},"Code in JavaScript1":{"main":[[{"node":"Get Historical Orders","type":"main","index":0}]]},"Get Historical Orders":{"main":[[{"node":"Check If Changes Exist1","type":"main","index":0}]]},"Check If Changes Exist1":{"main":[[{"node":"Check If Changes Exist","type":"main","index":0}]]}},"settings":{"saveManualExecutions":true,"executionOrder":"v1","callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":{"node:Schedule Trigger - Every 15 min":{"recurrenceRules":[]}},"meta":{"templateCredsSetupCompleted":true},"pinData":{},"versionId":"6fa2e14e-afc7-4777-83f2-7cb2e286a5fd","versionCounter":15,"triggerCount":1,"shared":[{"updatedAt":"2025-11-20T14:44:24.861Z","createdAt":"2025-11-20T14:44:24.861Z","role":"workflow:owner","workflowId":"ZAZxVj2143XdVvOe","projectId":"jlSr7IBJt4siEqiM","project":{"updatedAt":"2025-01-31T18:30:45.852Z","createdAt":"2025-01-31T18:13:38.763Z","id":"jlSr7IBJt4siEqiM","name":"N8N Estyl <n8n@estyl.pl>","type":"personal","icon":null,"description":null,"projectRelations":[{"updatedAt":"2025-01-31T18:13:38.763Z","createdAt":"2025-01-31T18:13:38.763Z","userId":"5089e5a8-9975-48ff-8451-f5c3fb3e78ac","projectId":"jlSr7IBJt4siEqiM","user":{"updatedAt":"2025-11-19T23:00:50.363Z","createdAt":"2025-01-31T18:13:37.839Z","id":"5089e5a8-9975-48ff-8451-f5c3fb3e78ac","email":"n8n@estyl.pl","firstName":"N8N","lastName":"Estyl","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2025-01-31T18:30:49.691Z","personalization_survey_n8n_version":"1.76.1"},"settings":{"userActivated":true,"firstSuccessfulWorkflowId":"xbn9AqbzxO9Xbruw","userActivatedAt":1739378108329,"easyAIWorkflowOnboarded":true,"npsSurvey":{"responded":true,"lastShownAt":1758812569793}},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2025-11-19","isPending":false}}]}}],"tags":[]}

operations:
  [{"type":"addNode","node":{"id":"init-pagination-code","name":"Initialize Pagination State","type":"n8n-nodes-base.code","typeVersion":2,"position":[112,-400],"parameters":{"mode":"runOnceForAllItems","jsCode":"// Initialize pagination state for July 2020 export\nconst startDate = new Date('2020-07-01T00:00:00Z');\nconst cursor_date = Math.floor(startDate.getTime() / 1000);\n\nreturn [{\n  json: {\n    id: 1,\n    batch_number: 0,\n    total_orders: 0,\n    status: 'ready',\n    last_updated: new Date().toISOString(),\n    cursor_date: cursor_date\n  }\n}];"}}},{"type":"addNode","node":{"id":"upsert-init-state","name":"Upsert Initial State","type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[320,-400],"parameters":{"resource":"row","operation":"upsert","dataTableId":{"mode":"id","value":"pagination_state_july2020"},"filters":{"conditions":[{"keyName":"id","condition":"eq","keyValue":"1"}]},"columns":{"mappingMode":"defineBelow","value":{"id":"={{ $json.id }}","batch_number":"={{ $json.batch_number }}","total_orders":"={{ $json.total_orders }}","status":"={{ $json.status }}","last_updated":"={{ $json.last_updated }}","cursor_date":"={{ $json.cursor_date }}"}}}}},{"type":"addNode","node":{"id":"read-pagination-state","name":"Read Pagination State","type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[528,-400],"parameters":{"resource":"row","operation":"get","dataTableId":{"mode":"id","value":"pagination_state_july2020"},"returnAll":false,"limit":1,"filters":{"conditions":[{"keyName":"id","condition":"eq","keyValue":"1"}]}}}},{"type":"removeConnection","source":"When clicking 'Execute workflow'","target":"Code in JavaScript1"},{"type":"addConnection","source":"When clicking 'Execute workflow'","target":"Initialize Pagination State"},{"type":"addConnection","source":"Initialize Pagination State","target":"Upsert Initial State"},{"type":"addConnection","source":"Upsert Initial State","target":"Read Pagination State"},{"type":"addConnection","source":"Read Pagination State","target":"Code in JavaScript1"}]


workflow_id: ZAZxVj2143XdVvOe
workflow_name: BaseLinker to Synerise CSV Export - historical backfill
trigger: partial_update
created_at: 2025-11-20 15:45:02
workflow_snapshot:
  {"updatedAt":"2025-11-20T15:11:20.870Z","createdAt":"2025-11-20T14:44:24.861Z","id":"ZAZxVj2143XdVvOe","name":"BaseLinker to Synerise CSV Export - historical backfill","description":"test","active":false,"isArchived":false,"nodes":[{"parameters":{"jsCode":"// Transform BaseLinker orders to Synerise CSV format\n  // Mode: Run Once for All Items\n  // FIXED: orderId now uses shop_order_id (short format) instead of order_id\n  // FIXED: recordedAt now uses correct UTC timestamps (removed incorrect +2h offset)\n  // FIXED: totalAmount now calculates from products + delivery (not payment_done which is 0 for COD)\n\n  const items = $input.all();\n  const results = [];\n\n  console.log(`Processing ${items.length} orders for Synerise export`);\n\n  // Map status using the enriched status_name\n  const statusMapping = {\n    'ZREALIZOWANE': 'completed',\n    'ANULOWANE': 'cancelled',\n    'ZWROT': 'returned',\n    'WYSŁANE': 'shipped'\n  };\n\n  // Process each order\n  items.forEach((item, index) => {\n    try {\n      const order = item.json;\n\n      // Generate eventSalt\n      const eventSalt = `${order.order_id}_${order.date_add || Date.now()}`;\n\n      // TIMEZONE FIX: BaseLinker returns date_confirmed as Unix timestamp in UTC\n      // No offset needed - UTC is the correct format for data systems\n      // Synerise will handle timezone conversion for display purposes\n      let recordedAt;\n      if (order.date_confirmed) {\n        const timestamp = parseInt(order.date_confirmed);\n        recordedAt = new Date(timestamp * 1000).toISOString();\n\n        // Debug log for first order\n        if (index === 0) {\n          console.log(`Timestamp conversion example:`);\n          console.log(`  Unix timestamp: ${timestamp}`);\n          console.log(`  UTC ISO format: ${recordedAt}`);\n        }\n      } else {\n        recordedAt = new Date().toISOString();\n      }\n\n      // Find appropriate status based on name\n      let orderStatus = 'unknown';\n      const statusNameUpper = (order.status_name || '').toUpperCase();\n      for (const [key, value] of Object.entries(statusMapping)) {\n        if (statusNameUpper.includes(key)) {\n          orderStatus = value;\n          break;\n        }\n      }\n\n      // Determine payment type\n      const paymentType = order.payment_method_cod === '1' ? 'Pobranie' : 'Przelew';\n\n      // Safe parseFloat\n      const safeParseFloat = (value) => {\n        const parsed = parseFloat(value);\n        return isNaN(parsed) ? 0 : parsed;\n      };\n\n      // BUGFIX #3: Calculate totalAmount from products + delivery\n      // BaseLinker API doesn't provide order total - must calculate it\n      // payment_done shows 0 for COD orders (unpaid), but order still has value\n      let totalAmount = 0;\n\n      // Calculate products subtotal\n      if (order.products && Array.isArray(order.products)) {\n        const productsSubtotal = order.products.reduce((sum, product) => {\n          const price = safeParseFloat(product.price_brutto);\n          const quantity = parseInt(product.quantity) || 0;\n          return sum + (price * quantity);\n        }, 0);\n\n        // Add delivery cost\n        totalAmount = productsSubtotal + safeParseFloat(order.delivery_price);\n\n        // Debug log for first order\n        if (index === 0) {\n          console.log(`Total amount calculation example:`);\n          console.log(`  Products subtotal: ${productsSubtotal.toFixed(2)}`);\n          console.log(`  Delivery price: ${safeParseFloat(order.delivery_price).toFixed(2)}`);\n          console.log(`  Total amount: ${totalAmount.toFixed(2)}`);\n          console.log(`  Payment done (old approach): ${safeParseFloat(order.payment_done)}`);\n        }\n      } else {\n        // Fallback: if no products array, use delivery_price only\n        totalAmount = safeParseFloat(order.delivery_price);\n      }\n\n      // Extract client name with fallbacks\n      const clientName = order.delivery_fullname ||\n                        order.invoice_fullname ||\n                        order.delivery_company ||\n                        `${order.delivery_name || ''} ${order.delivery_surname || ''}`.trim() ||\n                        'Unknown Customer';\n\n      // BUGFIX #1: Use shop_order_id (short format) instead of order_id\n      // shop_order_id examples: 184031, 184042 (human-readable short numbers)\n      // order_id examples: 790964970, 790932859 (internal BaseLinker IDs)\n      const orderId = order.shop_order_id || order.order_id; // Fallback to order_id if shop_order_id is missing\n\n      // Debug log for first order to verify fix\n      if (index === 0) {\n        console.log(`Order ID fix example:`);\n        console.log(`  OLD (order_id): ${order.order_id}`);\n        console.log(`  NEW (shop_order_id): ${order.shop_order_id}`);\n        console.log(`  Using: ${orderId}`);\n      }\n\n      // Add transformed order to results\n      results.push({\n        json: {\n          eventSalt: eventSalt,\n          orderId: orderId,\n          recordedAt: recordedAt, // FIXED: Now correct UTC format without offset\n          clientEmail: order.email || '',\n          clientName: clientName,\n          clientPhone: order.phone || '',\n          status: orderStatus,\n          statusName: order.status_name || '',\n          totalAmount: Math.round(totalAmount * 100) / 100, // FIXED: Calculated from products + delivery\n          currency: order.currency || 'PLN',\n          paymentMethod: order.payment_method || '',\n          paymentType: paymentType,\n          deliveryMethod: order.delivery_method || '',\n          deliveryPrice: safeParseFloat(order.delivery_price),\n          products: order.products || [],\n          source: order.order_source || 'BaseLinker',\n          notes: order.admin_comments || ''\n        }\n      });\n\n    } catch (error) {\n      console.error(`Error processing order at index ${index}:`, error);\n      console.error(`Order data:`, JSON.stringify(item.json).substring(0, 500));\n\n      // Add failed order with error info\n      results.push({\n        json: {\n          error: true,\n          errorMessage: error.message,\n          orderId: item.json?.order_id || 'UNKNOWN',\n          originalData: item.json\n        }\n      });\n    }\n  });\n\n  console.log(`Successfully transformed ${results.length} orders`);\n  console.log(`Sample order IDs: ${results.slice(0, 5).map(r => r.json.orderId).join(', ')}`);\n\n  // Return all transformed items\n  return results;\n"},"id":"4dc6cd3a-4e59-45f5-90ce-035791d545b4","name":"Transform to Synerise Format","type":"n8n-nodes-base.code","position":[3184,-448],"typeVersion":2},{"parameters":{"jsCode":"// Flatten nested product data for CSV\n// UPDATED: 16 columns (removed: eventSalt, clientName, clientPhone, productCategory, productEan, source, notes)\nconst items = $input.all();\nconst csvRows = [];\n\nitems.forEach(item => {\n  const order = item.json;\n  \n  // If order has products, create a row for each product\n  if (order.products && order.products.length > 0) {\n    order.products.forEach(product => {\n      csvRows.push({\n        orderId: order.orderId,\n        recordedAt: order.recordedAt,\n        clientEmail: order.clientEmail,\n        status: order.status,\n        statusName: order.statusName,\n        totalAmount: order.totalAmount,\n        currency: order.currency,\n        productSku: product.sku || product.product_id,\n        productName: product.name,\n        productBrand: product.brand || 'Unknown',\n        productPrice: product.price_brutto,\n        productQuantity: product.quantity,\n        paymentMethod: order.paymentMethod,\n        paymentType: order.paymentType,\n        deliveryMethod: order.deliveryMethod,\n        deliveryPrice: order.deliveryPrice\n      });\n    });\n  } else {\n    // Order without products\n    csvRows.push({\n      orderId: order.orderId,\n      recordedAt: order.recordedAt,\n      clientEmail: order.clientEmail,\n      status: order.status,\n      statusName: order.statusName,\n      totalAmount: order.totalAmount,\n      currency: order.currency,\n      productSku: '',\n      productName: '',\n      productBrand: '',\n      productPrice: '',\n      productQuantity: '',\n      paymentMethod: order.paymentMethod,\n      paymentType: order.paymentType,\n      deliveryMethod: order.deliveryMethod,\n      deliveryPrice: order.deliveryPrice\n    });\n  }\n});\n\n// Generate filename with timestamp\nconst now = new Date();\nconst filename = `synerise_export_${now.toISOString().slice(0,10)}_${now.toTimeString().slice(0,5).replace(':','')}.csv`;\n\nreturn [{\n  json: {\n    filename: filename,\n    rowCount: csvRows.length,\n    exportTime: now.toISOString(),\n    data: csvRows\n  }\n}];"},"id":"6c52f816-b911-4c5d-9264-b66c81c5f06a","name":"Prepare CSV Data","type":"n8n-nodes-base.code","position":[3408,-448],"typeVersion":2},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getOrders"},{"name":"parameters","value":"={{ JSON.stringify({ order_id: parseInt($json.json.orderId), get_unconfirmed_orders: false }) }}"}]},"options":{"timeout":30000}},"id":"15ca7ed2-b332-49ee-8168-ae2c30ae43c3","name":"Get Orders by IDs","type":"n8n-nodes-base.httpRequest","position":[1616,-32],"typeVersion":4.2,"retryOnFail":true,"maxTries":5,"waitBetweenTries":5000},{"parameters":{"jsCode":"// Extract Product IDs - WERSJA Z JEDNYM OUTPUTEM I RÓŻNYMI TYPAMI\nconst items = $input.all();\nconst allProductIds = new Set();\nconst allOrders = [];\n\n// Aggregate node zwraca wszystko w polu \"data\"\nconst aggregatedData = items[0]?.json?.data;\n\nif (aggregatedData && Array.isArray(aggregatedData)) {\n  // Sprawdź strukturę danych z Aggregate\n  aggregatedData.forEach(item => {\n    // Jeśli każdy item ma pole \"orders\" (batche z BaseLinker)\n    if (item.orders && Array.isArray(item.orders)) {\n      allOrders.push(...item.orders);\n    } \n    // Jeśli to bezpośrednio zamówienia (spłaszczone)\n    else if (item.order_id) {\n      allOrders.push(item);\n    }\n    // Jeśli to zagnieżdżone w json\n    else if (item.json?.orders) {\n      allOrders.push(...item.json.orders);\n    }\n    else if (item.json?.order_id) {\n      allOrders.push(item.json);\n    }\n  });\n} \n// Fallback - może dane są bezpośrednio w items\nelse {\n  items.forEach(item => {\n    if (item.json?.orders) {\n      allOrders.push(...item.json.orders);\n    } else if (item.json?.order_id) {\n      allOrders.push(item.json);\n    }\n  });\n}\n\n// Zbierz wszystkie unikalne product IDs\nallOrders.forEach(order => {\n  if (order.products && Array.isArray(order.products)) {\n    order.products.forEach(product => {\n      if (product.product_id) {\n        allProductIds.add(parseInt(product.product_id));\n      }\n    });\n  }\n});\n\nconst productIdsArray = Array.from(allProductIds);\n\n// Podziel produkty na batche po 100\nconst BATCH_SIZE = 100;\nconst productBatches = [];\nfor (let i = 0; i < productIdsArray.length; i += BATCH_SIZE) {\n  productBatches.push(productIdsArray.slice(i, i + BATCH_SIZE));\n}\n\nconsole.log('=== EXTRACT PRODUCT IDS ===');\nconsole.log(`Total orders: ${allOrders.length}`);\nconsole.log(`Unique products: ${productIdsArray.length}`);\nconsole.log(`Product batches: ${productBatches.length} (${BATCH_SIZE} per batch)`);\nproductBatches.forEach((batch, index) => {\n  console.log(`  Batch ${index + 1}: ${batch.length} products`);\n});\n\n// Przygotuj output items\nconst outputItems = [];\n\n// PIERWSZY ITEM: Zamówienia (dla Merge)\noutputItems.push({\n  json: {\n    type: 'ordersData',  // Unikalny typ dla Switch\n    orders: allOrders,\n    stats: {\n      batchOrderCount: allOrders.length,\n      uniqueProductCount: allProductIds.size,\n      totalBatches: productBatches.length,\n      avgProductsPerOrder: (allOrders.reduce((sum, o) => sum + (o.products?.length || 0), 0) / allOrders.length).toFixed(2)\n    }\n  }\n});\n\n// KOLEJNE ITEMY: Każdy batch produktów (dla Loop/Get Product Brands)\nproductBatches.forEach((batch, index) => {\n  outputItems.push({\n    json: {\n      type: 'productBatch',  // Unikalny typ dla Switch\n      batchIndex: index + 1,\n      totalBatches: productBatches.length,\n      productIdsToEnrich: batch,\n      batchSize: batch.length\n    }\n  });\n});\n\nconsole.log(`Returning ${outputItems.length} items: 1 ordersData + ${productBatches.length} productBatch(es)`);\n\nreturn outputItems;"},"id":"ec3603bf-23ec-4859-98d3-354391f11e8e","name":"Extract Product IDs","type":"n8n-nodes-base.code","typeVersion":2,"position":[1616,-272]},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getInventoryProductsData"},{"name":"parameters","value":"={{ JSON.stringify({ \n  inventory_id: 758, \n  products: $json.productIdsToEnrich || [] \n}) }}"}]},"options":{"timeout":30000}},"id":"3417e7b6-d432-4e68-8500-6ea48ce52427","name":"Get Product Brands","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2288,-64],"retryOnFail":true},{"parameters":{"jsCode":"// FIXED: Merge Product Data - Correctly extracts data from Merge node\n// This version properly handles the 4 inputs from the Merge node\n\nconst allInputs = $input.all();\nconsole.log(`=== MERGE PRODUCT DATA - DEBUGGING ===`);\nconsole.log(`Total inputs received: ${allInputs.length}`);\n\n// Initialize data storage\nlet manufacturersData = [];\nlet ordersData = [];\nlet productsData = {};\nlet statusesData = [];\n\n// The Merge node combines 4 inputs:\n// Input 0: Product Manufacturers\n// Input 1: Orders (from Switch - ordersData branch)  \n// Input 2: Aggregated Products\n// Input 3: Order Status List\n\n// Process each input and extract the correct data\nallInputs.forEach((item, index) => {\n  const data = item.json;\n  \n  console.log(`\\nInput ${index} analysis:`, {\n    hasManufacturers: !!(data.manufacturers || data.manufacturer),\n    hasOrders: !!(data.orders || data.type === 'ordersData'),\n    hasProducts: !!(data.products || data.data),\n    hasStatuses: !!(data.statuses || data.status),\n    topKeys: Object.keys(data).slice(0, 5),\n    dataType: data.type || 'unknown'\n  });\n  \n  // Extract manufacturers (usually first input from Get Product Manufacturers)\n  if (data.manufacturers && Array.isArray(data.manufacturers)) {\n    manufacturersData = data.manufacturers;\n    console.log(`✓ Found ${manufacturersData.length} manufacturers`);\n  }\n  \n  // Extract orders (from ordersData type)\n  if (data.type === 'ordersData' && data.orders) {\n    ordersData = data.orders;\n    console.log(`✓ Found ${ordersData.length} orders from ordersData`);\n  } else if (data.orders && Array.isArray(data.orders)) {\n    ordersData = data.orders;\n    console.log(`✓ Found ${ordersData.length} orders`);\n  }\n  \n  // Extract products from aggregated data\n  if (data.data && Array.isArray(data.data)) {\n    // This is the aggregated products output\n    console.log(`✓ Found aggregated data with ${data.data.length} items`);\n    data.data.forEach(item => {\n      if (item.products && typeof item.products === 'object') {\n        Object.assign(productsData, item.products);\n      }\n    });\n  } else if (data.products && typeof data.products === 'object') {\n    // Direct products object\n    Object.assign(productsData, data.products);\n  }\n  \n  // Extract statuses\n  if (data.statuses && Array.isArray(data.statuses)) {\n    statusesData = data.statuses;\n    console.log(`✓ Found ${statusesData.length} order statuses`);\n  }\n});\n\n// Create manufacturer mapping (ID → Name)\nconst manufacturerMap = {};\nmanufacturersData.forEach(manufacturer => {\n  const id = String(manufacturer.manufacturer_id);\n  manufacturerMap[id] = manufacturer.name;\n});\n\nconsole.log(`\\n=== DATA SUMMARY ===`);\nconsole.log(`Manufacturers: ${Object.keys(manufacturerMap).length}`);\nconsole.log(`Sample manufacturers:`, Object.entries(manufacturerMap).slice(0, 5));\nconsole.log(`Products with brand data: ${Object.keys(productsData).length}`);\nconsole.log(`Orders to process: ${ordersData.length}`);\nconsole.log(`Statuses: ${statusesData.length}`);\n\n// Create status mapping\nconst statusMap = {};\nstatusesData.forEach(status => {\n  statusMap[String(status.id)] = status.name;\n});\n\n// Statistics\nlet stats = {\n  totalProducts: 0,\n  productsWithBrand: 0,\n  productsWithoutBrand: 0,\n  missingProductData: 0,\n  uniqueBrands: new Set(),\n  missingProductIds: new Set()\n};\n\n// Process and enrich orders\nconst enrichedOrders = ordersData.map((order, orderIndex) => {\n  // Clone the order to avoid mutation\n  const enrichedOrder = JSON.parse(JSON.stringify(order));\n  \n  // Add status name\n  enrichedOrder.status_name = statusMap[String(order.order_status_id)] || 'Unknown';\n  \n  // Process products in the order\n  if (enrichedOrder.products && Array.isArray(enrichedOrder.products)) {\n    enrichedOrder.products = enrichedOrder.products.map((product, productIndex) => {\n      stats.totalProducts++;\n      \n      const productIdStr = String(product.product_id);\n      const productData = productsData[productIdStr];\n      \n      // Initialize brand as Unknown\n      let brandName = 'Unknown';\n      let categoryName = '';\n      let ean = product.ean || '';\n      \n      if (productData) {\n        // Get manufacturer/brand name\n        if (productData.manufacturer_id) {\n          const manufacturerIdStr = String(productData.manufacturer_id);\n          const manufacturer = manufacturerMap[manufacturerIdStr];\n          \n          if (manufacturer) {\n            brandName = manufacturer;\n            stats.uniqueBrands.add(brandName);\n            stats.productsWithBrand++;\n          } else {\n            console.warn(`Manufacturer ID ${manufacturerIdStr} not found in map for product ${productIdStr}`);\n            stats.productsWithoutBrand++;\n          }\n        } else {\n          stats.productsWithoutBrand++;\n        }\n        \n        // Get category from text_fields\n        if (productData.text_fields) {\n          categoryName = productData.text_fields.extra_field_851 || '';\n        }\n        \n        // Get EAN\n        ean = productData.ean || product.ean || '';\n        \n      } else {\n        stats.missingProductData++;\n        stats.missingProductIds.add(productIdStr);\n        \n        if (orderIndex < 3 && productIndex < 2) {\n          console.warn(`No product data found for ID: ${productIdStr}`);\n        }\n      }\n      \n      // Debug first few products\n      if (orderIndex === 0 && productIndex < 3) {\n        console.log(`\\nSample enrichment - Product ${productIdStr}:`, {\n          originalName: product.name,\n          brand: brandName,\n          hasProductData: !!productData,\n          manufacturerId: productData?.manufacturer_id,\n          category: categoryName ? categoryName.substring(0, 50) + '...' : 'None'\n        });\n      }\n      \n      // Return enriched product\n      return {\n        ...product,\n        brand: brandName,\n        category_name: categoryName,\n        ean: ean\n      };\n    });\n  }\n  \n  return enrichedOrder;\n});\n\n// Calculate coverage\nconst coverage = stats.totalProducts > 0 \n  ? ((stats.productsWithBrand / stats.totalProducts) * 100).toFixed(1)\n  : 0;\n\nconsole.log(`\\n=== ENRICHMENT RESULTS ===`);\nconsole.log(`Total products: ${stats.totalProducts}`);\nconsole.log(`Products with brands: ${stats.productsWithBrand} (${coverage}%)`);\nconsole.log(`Products without brands: ${stats.productsWithoutBrand}`);\nconsole.log(`Products missing data: ${stats.missingProductData}`);\nconsole.log(`Unique brands found: ${stats.uniqueBrands.size}`);\nconsole.log(`Brand names:`, Array.from(stats.uniqueBrands).slice(0, 10).join(', '));\n\nif (stats.missingProductData > 0) {\n  console.warn(`\\n⚠️ Missing product IDs (first 10):`, \n    Array.from(stats.missingProductIds).slice(0, 10).join(', '));\n}\n\n// Return results\nconst output = [];\n\n// Add statistics as first item\noutput.push({\n  json: {\n    _type: 'statistics',\n    _timestamp: new Date().toISOString(),\n    coverage: {\n      total_products: stats.totalProducts,\n      with_brand: stats.productsWithBrand,\n      without_brand: stats.productsWithoutBrand,\n      missing_data: stats.missingProductData,\n      coverage_percent: `${coverage}%`,\n      unique_brands: stats.uniqueBrands.size\n    },\n    brands: Array.from(stats.uniqueBrands),\n    missing_ids: Array.from(stats.missingProductIds).slice(0, 20)\n  }\n});\n\n// Add enriched orders\nenrichedOrders.forEach(order => {\n  output.push({ json: order });\n});\n\nreturn output;"},"id":"aa52a712-23fe-42e2-b71a-414707430739","name":"Merge Product Data","type":"n8n-nodes-base.code","typeVersion":2,"position":[2736,-448]},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getInventoryManufacturers"},{"name":"parameters","value":"{\"inventory_id\":758}"}]},"options":{"timeout":30000}},"id":"8098ddd8-f311-48f1-b7e0-bc13de4a4712","name":"Get Product Manufacturers","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2288,-704],"retryOnFail":true},{"parameters":{"mode":"combine","combineBy":"combineByPosition","numberInputs":4,"options":{}},"type":"n8n-nodes-base.merge","typeVersion":3.2,"position":[2512,-480],"id":"5e3a6a8e-44a2-444a-a3bc-6d162dd4cf94","name":"Merge"},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getOrderStatusList"},{"name":"parameters","value":"{}"}]},"options":{}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2288,256],"id":"e923ab81-95d9-43c5-9271-e76a4fecd0aa","name":"Get Order Status List","retryOnFail":true},{"parameters":{"jsCode":"// Konfiguracja dozwolonych statusów - zgodnie z wymaganiami klienta\n// Tylko statusy końcowe: zrealizowane, anulowane, zwroty\n\nconst allowedStatuses = [\n  // === WYSŁANE/ZREALIZOWANE/DOSTARCZONE ===\n  300027, // WYSŁANE // .PL\n  300028, // WYSŁANE // _PL\n  361867, // DE // Wysłane\n  386333, // SM // WYSŁANE\n  354789, // Dostarczone\n  354788, // Paczka w punkcie\n  300828, // OO // Odebrane\n  \n  // === ANULOWANE ===\n  301588, // Anulowane (przez klienta)\n  302472, // Anulowane (brak wpłaty)\n  302473, // Anulowane (zwrot paczki)\n  302475, // Anulowane (brak towaru)\n  303313, // Anulowane STA\n  304745, // Anulacja przed wysyłką\n  316385, // Anulacje (testowe)\n  359429, // WMS Anulowane\n  302981, // [Optima] Anulacja RO\n  \n  // === ZWROTY ===\n  300346, // ZWR // Przedpłacone\n  303161, // ZWR // Deklaracja zwrotu\n  311537, // SOK // Zwroty paczek\n  354792, // Zwrot kurierski\n  372310, // MolZwrotPrzyjęty [sok] zwrot$\n  374054, // Zwroty MADS (Hebe i Mitago_EU)\n  374055, // Zwroty MAGU Kau+Pigu\n  356655, // [MOL] Zwroty częściowe\n  356674, // EXC zwrócone\n  \n  // === NIEDOSTARCZONE (może być traktowane jako anulowane) ===\n  354791  // Niedostarczone\n];\n\n// Mapowanie kategorii statusów dla lepszego raportowania\nconst statusCategories = {\n  'ZREALIZOWANE': [300027, 300028, 361867, 386333, 354789, 354788, 300828],\n  'ANULOWANE': [301588, 302472, 302473, 302475, 303313, 304745, 316385, 359429, 302981],\n  'ZWROTY': [300346, 303161, 311537, 354792, 372310, 374054, 374055, 356655, 356674],\n  'NIEDOSTARCZONE': [354791]\n};\n\n// Pobierz wszystkie zamówienia\nconst items = $input.all();\n\nconsole.log(\"=== FILTER ORDER STATUS ===\");\nconsole.log(`Total orders received: ${items.length}`);\n\n// Zbierz statystyki\nconst statusStats = {};\nconst categoryStats = {\n  'ZREALIZOWANE': 0,\n  'ANULOWANE': 0,\n  'ZWROTY': 0,\n  'NIEDOSTARCZONE': 0,\n  'OTHER': 0\n};\n\nlet passedCount = 0;\nlet filteredCount = 0;\n\n// Filtruj zamówienia\nconst filteredOrders = items.filter(item => {\n  const order = item.json;\n  const statusId = parseInt(order.order_status_id);\n  const statusName = order.status_name || 'Unknown';\n  \n  // Zbierz statystyki szczegółowe\n  const key = `${statusId} - ${statusName}`;\n  statusStats[key] = (statusStats[key] || 0) + 1;\n  \n  // Określ kategorię statusu\n  let category = 'OTHER';\n  for (const [cat, ids] of Object.entries(statusCategories)) {\n    if (ids.includes(statusId)) {\n      category = cat;\n      break;\n    }\n  }\n  categoryStats[category]++;\n  \n  // Sprawdź czy status jest dozwolony\n  const isAllowed = allowedStatuses.includes(statusId);\n  \n  if (isAllowed) {\n    passedCount++;\n    console.log(`  ✅ Order ${order.order_id}: Status ${statusId} (${statusName}) - ${category}`);\n  } else {\n    filteredCount++;\n    if (filteredCount <= 5) { // Pokaż tylko pierwsze 5 odrzuconych\n      console.log(`  ❌ Order ${order.order_id}: Status ${statusId} (${statusName}) - FILTERED OUT`);\n    }\n  }\n  \n  return isAllowed;\n});\n\n// Raport podsumowujący\nconsole.log(\"\\n=== STATUS CATEGORY SUMMARY ===\");\nconsole.log(`📦 Zrealizowane: ${categoryStats.ZREALIZOWANE} orders`);\nconsole.log(`❌ Anulowane: ${categoryStats.ANULOWANE} orders`);\nconsole.log(`↩️ Zwroty: ${categoryStats.ZWROTY} orders`);\nconsole.log(`⚠️ Niedostarczone: ${categoryStats.NIEDOSTARCZONE} orders`);\nconsole.log(`❓ Inne (odrzucone): ${categoryStats.OTHER} orders`);\n\nconsole.log(\"\\n=== DETAILED STATUS DISTRIBUTION ===\");\n// Sortuj statusy według liczby zamówień\nconst sortedStats = Object.entries(statusStats)\n  .sort((a, b) => b[1] - a[1])\n  .slice(0, 10); // Pokaż top 10\n\nsortedStats.forEach(([status, count]) => {\n  const statusId = parseInt(status.split(' ')[0]);\n  const allowed = allowedStatuses.includes(statusId) ? '✅' : '❌';\n  \n  // Znajdź kategorię\n  let category = 'OTHER';\n  for (const [cat, ids] of Object.entries(statusCategories)) {\n    if (ids.includes(statusId)) {\n      category = cat;\n      break;\n    }\n  }\n  \n  console.log(`  ${allowed} ${status}: ${count} orders [${category}]`);\n});\n\nconsole.log(`\\n=== FINAL SUMMARY ===`);\nconsole.log(`✅ Passed to Synerise: ${passedCount} orders`);\nconsole.log(`❌ Filtered out: ${filteredCount} orders`);\nconsole.log(`📊 Filter efficiency: ${((passedCount/items.length)*100).toFixed(1)}%`);\n\n// Ostrzeżenie jeśli wszystko odrzucone\nif (passedCount === 0 && items.length > 0) {\n  console.error(\"⚠️ WARNING: All orders were filtered out! Check status configuration.\");\n}\n\n// WAŻNE: Zwróć przefiltrowane zamówienia jako items\nreturn filteredOrders;"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[2960,-448],"id":"48ed4ca2-613f-4699-9553-6a59168168e5","name":"Filter Order Status"},{"parameters":{"jsCode":"// Generate CSV with semicolon delimiter (16 columns)\n// UPDATED: Changed from 23 columns (comma) to 16 columns (semicolon)\nconst input = $input.all()[0].json;\nconst csvRows = input.data;\n\n// 16 columns (removed: eventSalt, clientName, clientPhone, productCategory, productEan, source, notes)\nconst headers = [\n  'orderId',\n  'recordedAt',\n  'clientEmail',\n  'status',\n  'statusName',\n  'totalAmount',\n  'currency',\n  'productSku',\n  'productName',\n  'productBrand',\n  'productPrice',\n  'productQuantity',\n  'paymentMethod',\n  'paymentType',\n  'deliveryMethod',\n  'deliveryPrice'\n];\n\n// Function to escape CSV values for semicolon delimiter\nfunction escapeCsvValue(value) {\n  if (value === null || value === undefined) return '';\n  const stringValue = String(value);\n  \n  // If contains semicolon, double quote, or newline, wrap in quotes\n  if (stringValue.includes(';') || stringValue.includes('\"') || stringValue.includes('\\n')) {\n    return `\"${stringValue.replace(/\"/g, '\"\"')}\"`;\n  }\n  return stringValue;\n}\n\n// Generate CSV with SEMICOLON delimiter\nlet csvContent = headers.join(';') + '\\n';\n\n// Add data rows\ncsvRows.forEach(row => {\n  const values = headers.map(header => escapeCsvValue(row[header]));\n  csvContent += values.join(';') + '\\n';\n});\n\nconsole.log(`Generated CSV: ${csvRows.length} rows, 16 columns, semicolon delimiter`);\n\n// Return as binary data\nreturn {\n  json: {\n    filename: input.filename,\n    rowCount: csvRows.length,\n    columnCount: headers.length,\n    delimiter: 'semicolon'\n  },\n  binary: {\n    data: {\n      data: Buffer.from(csvContent, 'utf8').toString('base64'),\n      mimeType: 'text/csv',\n      fileName: input.filename\n    }\n  }\n};"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[3632,-448],"id":"5a820d61-40f2-40eb-8a98-ba40fa84f8d0","name":"Code in JavaScript"},{"parameters":{"fieldToSplitOut":"[\"0\"]","options":{}},"type":"n8n-nodes-base.splitOut","typeVersion":1,"position":[1168,-80],"id":"e9603521-9b07-44ee-8587-503b8cdb1d27","name":"Split Out1"},{"parameters":{"options":{}},"type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[1392,-80],"id":"8880595d-e24f-4e1e-8306-1419e710ba3b","name":"Loop Over Items"},{"parameters":{"options":{}},"type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[2064,-208],"id":"b3546273-74ce-4094-870e-b7aabee04600","name":"Loop Over Items1"},{"parameters":{"rules":{"values":[{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"leftValue":"={{ $json.type }}","rightValue":"ordersData","operator":{"type":"string","operation":"equals"},"id":"16455973-160a-451f-9ce5-d3e00d4ec21c"}],"combinator":"and"},"renameOutput":true,"outputKey":"orders"},{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"id":"d0ff60de-b3c8-45e4-8d2e-b00c0acc1685","leftValue":"={{ $json.type }}","rightValue":"productBatch","operator":{"type":"string","operation":"equals","name":"filter.operator.equals"}}],"combinator":"and"},"renameOutput":true,"outputKey":"product"}]},"options":{}},"type":"n8n-nodes-base.switch","typeVersion":3.3,"position":[1840,-272],"id":"bded8fd0-05ff-4eb6-8d6c-cc30cebe60b9","name":"Switch"},{"parameters":{"aggregate":"aggregateAllItemData","options":{}},"type":"n8n-nodes-base.aggregate","typeVersion":1,"position":[2288,-256],"id":"637e4855-58c9-434e-a41b-86a01d129154","name":"Aggregate"},{"parameters":{"jsCode":"// Filter to include ONLY online shop orders (inclusion-based approach)\n// FIXED: Changed from exclusion list to checking order_source field directly\n// This ensures ALL marketplaces are excluded (Allegro, Kaufland, Hebe, etc.)\n\nconst items = $input.all();\nconst orders = items[0]?.json?.orders || [];\n\nconsole.log('=== FILTER ONLINE SHOP ORDERS (INCLUSION-BASED) ===');\nconsole.log(`Total orders before filtering: ${orders.length}`);\n\n// Statistics\nconst stats = {\n  total: orders.length,\n  onlineShop: 0,\n  marketplace: 0,\n  unknown: 0,\n  bySource: {}\n};\n\n// Filter orders - ONLY include shop orders\nconst filteredOrders = orders.filter(order => {\n  const orderSource = (order.order_source || '').toLowerCase();\n  const orderSourceId = order.order_source_id;\n  \n  // Track statistics by source\n  const sourceName = order.order_source || 'Unknown';\n  stats.bySource[sourceName] = (stats.bySource[sourceName] || 0) + 1;\n  \n  // INCLUSION FILTER: Only accept orders where source is \"shop\"\n  const isShop = orderSource === 'shop' || orderSource.includes('shop');\n  \n  if (isShop) {\n    stats.onlineShop++;\n    if (stats.onlineShop <= 5) {\n      console.log(`  ✅ INCLUDED: Order ${order.order_id} from '${sourceName}' (source_id: ${orderSourceId})`);\n    }\n    return true;\n  } else if (!orderSource || orderSource === 'unknown') {\n    stats.unknown++;\n    if (stats.unknown <= 3) {\n      console.log(`  ⚠️ UNKNOWN: Order ${order.order_id} has unknown/empty source (source_id: ${orderSourceId})`);\n    }\n    return false;\n  } else {\n    stats.marketplace++;\n    if (stats.marketplace <= 5) {\n      console.log(`  ❌ EXCLUDED: Order ${order.order_id} from '${sourceName}' (source_id: ${orderSourceId})`);\n    }\n    return false;\n  }\n});\n\nconsole.log('\\n=== FILTERING RESULTS (INCLUSION-BASED) ===');\nconsole.log(`✅ Online shop orders: ${stats.onlineShop}`);\nconsole.log(`❌ Marketplace orders (excluded): ${stats.marketplace}`);\nconsole.log(`⚠️ Unknown source (excluded): ${stats.unknown}`);\nconsole.log(`📊 Filter efficiency: ${((stats.onlineShop/stats.total)*100).toFixed(1)}% kept`);\n\nconsole.log('\\n=== ORDERS BY SOURCE ===');\nObject.entries(stats.bySource)\n  .sort((a, b) => b[1] - a[1])\n  .forEach(([source, count]) => {\n    const isShop = source.toLowerCase().includes('shop');\n    const emoji = isShop ? '✅' : '❌';\n    console.log(`  ${emoji} ${source}: ${count} orders`);\n  });\n\nconsole.log('\\n=== FILTER APPROACH ===');\nconsole.log('Using INCLUSION-based filter: only order_source=\"shop\" passes');\nconsole.log('This automatically excludes ALL marketplaces:');\nconsole.log('  - Allegro, Kaufland, Hebe, Empik, eMag, etc.');\nconsole.log('  - Any future marketplace integrations');\n\n// Return filtered orders in the same structure\nreturn [{\n  json: {\n    orders: filteredOrders,\n    _filterStats: stats,\n    _filterApproach: 'inclusion-based',\n    _filterCriteria: 'order_source === \"shop\"',\n    _timestamp: new Date().toISOString()\n  }\n}];"},"id":"3312a255-a865-4d0f-b4d4-112045b1767f","name":"Filter Online Shop Orders","type":"n8n-nodes-base.code","typeVersion":2,"position":[1840,16]},{"parameters":{"jsCode":"// Extract unique order IDs from journal\nconst items = $input.all();\nconst logs = items[0]?.json?.logs || [];\n\n// Get unique order IDs\nconst orderIds = [...new Set(logs.map(log => log.order_id).filter(id => id))];\n\n// Przygotuj outputy\nlet output0 = []; // Order IDs do przetworzenia\nlet output1 = []; // Dane do Google Sheets update\n\nif (logs.length > 0) {\n  // Znajdź log z najwyższą datą (najświeższy)\n  const newestLog = logs.reduce((newest, current) => {\n    return current.date > newest.date ? current : newest;\n  }, logs[0]);\n  \n  const newLastLogId = newestLog.log_id;\n  const newestDate = new Date(newestLog.date * 1000).toISOString();\n  \n  console.log(`Found ${logs.length} logs, ${orderIds.length} unique orders`);\n  console.log(`Newest log_id: ${newLastLogId} from date: ${newestDate}`);\n  console.log(`Newest log details:`, newestLog);\n  \n  // Output 0: Order IDs do przetworzenia\n  output0 = orderIds.map((orderId, index) => ({\n    json: {\n      orderId: orderId,\n      totalCount: orderIds.length,\n      currentIndex: index + 1\n    }\n  }));\n  \n  // Output 1: Dane do update Google Sheets (4 wiersze - dodałem newest_date)\n  output1 = [\n    {\n      json: {\n        key: 'last_log_id',\n        value: newLastLogId.toString(),\n        row_number: 2\n      }\n    },\n    {\n      json: {\n        key: 'last_sync',\n        value: new Date().toISOString(),\n        row_number: 3\n      }\n    },\n    {\n      json: {\n        key: 'total_orders',\n        value: orderIds.length.toString(),\n        row_number: 4\n      }\n    },\n    {\n      json: {\n        key: 'newest_log_date',\n        value: newestDate,\n        row_number: 5\n      }\n    }\n  ];\n} else {\n  console.log(\"No new orders to process\");\n}\n\n// Zwróć dane dla obu outputów\nreturn {\n  0: output0,  // Idzie do Loop Over Items\n  1: output1   // Idzie do Google Sheets Update\n};"},"id":"b28a21d8-2e1d-4af5-95fe-313744f427ea","name":"Extract Order IDs & Prepare Update","type":"n8n-nodes-base.code","position":[944,-272],"typeVersion":2},{"parameters":{},"id":"6a75883f-a764-487d-943c-0d9aabb925c2","name":"No Changes - Stop","type":"n8n-nodes-base.noOp","position":[944,112],"typeVersion":1},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":1},"conditions":[{"leftValue":"={{ $json.logs?.length }}","rightValue":0,"operator":{"type":"number","operation":"gt"},"id":"fb5b9b41-36c6-408f-a7ce-41f8c97e3146"}],"combinator":"and"},"options":{}},"id":"f10b58dc-75a8-4be2-8b34-1a4070c97f71","name":"Check If Changes Exist","type":"n8n-nodes-base.if","position":[720,-176],"typeVersion":2},{"parameters":{},"type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[-96,-176],"id":"27acb096-8bae-4f19-9237-5139dd7dc10f","name":"When clicking ‘Execute workflow’"},{"parameters":{"jsCode":"// Set date range for historical retrieval\n  const daysBack = 60; // Last 30 days\n  const dateFrom = Math.floor(Date.now() / 1000) - (daysBack * 24 * 60 * 60);\n  const dateTo = Math.floor(Date.now() / 1000); // Now\n\n  console.log(`Fetching orders from: ${new Date(dateFrom * 1000).toISOString()}`);\n  console.log(`To: ${new Date(dateTo * 1000).toISOString()}`);\n  console.log(`Days back: ${daysBack}`);\n\n  return [{\n    json: {\n      dateFrom: dateFrom,\n      dateTo: dateTo,\n      daysBack: daysBack\n    }\n  }]"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[112,-176],"id":"2e55bcec-9628-4f92-802d-dc31f9d01247","name":"Code in JavaScript1"},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getOrders"},{"name":"parameters","value":"={{ JSON.stringify({ date_confirmed_from: $json.dateFrom, get_unconfirmed_orders: false }) }}"}]},"options":{}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.3,"position":[320,-176],"id":"137fe081-3884-42b0-9c47-4f4ded00479a","name":"Get Historical Orders"},{"parameters":{"jsCode":"// Transform getOrders response to logs format expected by downstream nodes\n  const items = $input.all();\n  const response = items[0]?.json;\n\n  // Get orders from response\n  const orders = response?.orders || [];\n\n  console.log('=== TRANSFORM GETORDERS TO LOGS FORMAT ===');\n  console.log(`Received ${orders.length} orders from getOrders API`);\n\n  if (orders.length === 0) {\n    console.log('⚠️ No orders returned from API');\n    return [{\n      json: {\n        status: \"SUCCESS\",\n        logs: [],\n        _metadata: {\n          source: \"getOrders_historical\",\n          orderCount: 0,\n          processedAt: new Date().toISOString()\n        }\n      }\n    }];\n  }\n\n  // Create synthetic logs array from orders\n  // Each order becomes a log entry\n  const logs = orders.map(order => ({\n    log_id: order.order_id, // Use order_id as log_id\n    log_type: 1, // Type 1 = order event\n    order_id: order.order_id,\n    object_id: 0,\n    date: order.date_confirmed || order.date_add // Use order date\n  }));\n\n  console.log(`✅ Created ${logs.length} log entries from orders`);\n  console.log(`Date range: ${new Date(orders[0].date_confirmed * 1000).toISOString()} to ${new Date(orders[orders.length-1].date_confirmed * 1000).toISOString()}`);\n\n  // Return in format expected by \"Check If Changes Exist\"\n  return [{\n    json: {\n      status: \"SUCCESS\",\n      logs: logs,\n      _metadata: {\n        source: \"getOrders_historical\",\n        orderCount: orders.length,\n        oldestOrder: new Date(orders[0].date_confirmed * 1000).toISOString(),\n        newestOrder: new Date(orders[orders.length-1].date_confirmed * 1000).toISOString(),\n        processedAt: new Date().toISOString()\n      }\n    }\n  }];"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[528,-176],"id":"a0097467-1b8f-409c-b1ab-57fa5a1852ff","name":"Check If Changes Exist1"}],"connections":{"Transform to Synerise Format":{"main":[[{"node":"Prepare CSV Data","type":"main","index":0}]]},"Prepare CSV Data":{"main":[[{"node":"Code in JavaScript","type":"main","index":0}]]},"Extract Product IDs":{"main":[[{"node":"Switch","type":"main","index":0}]]},"Get Product Brands":{"main":[[{"node":"Loop Over Items1","type":"main","index":0}]]},"Merge Product Data":{"main":[[{"node":"Filter Order Status","type":"main","index":0}]]},"Get Product Manufacturers":{"main":[[{"node":"Merge","type":"main","index":0}]]},"Get Order Status List":{"main":[[{"node":"Merge","type":"main","index":3}]]},"Merge":{"main":[[{"node":"Merge Product Data","type":"main","index":0}]]},"Filter Order Status":{"main":[[{"node":"Transform to Synerise Format","type":"main","index":0}]]},"Split Out1":{"main":[[{"node":"Loop Over Items","type":"main","index":0}]]},"Loop Over Items":{"main":[[{"node":"Extract Product IDs","type":"main","index":0}],[{"node":"Get Orders by IDs","type":"main","index":0}]]},"Loop Over Items1":{"main":[[{"node":"Aggregate","type":"main","index":0}],[{"node":"Get Product Brands","type":"main","index":0}]]},"Switch":{"main":[[{"node":"Merge","type":"main","index":1}],[{"node":"Loop Over Items1","type":"main","index":0}]]},"Aggregate":{"main":[[{"node":"Merge","type":"main","index":2}]]},"Get Orders by IDs":{"main":[[{"node":"Filter Online Shop Orders","type":"main","index":0}]]},"Filter Online Shop Orders":{"main":[[{"node":"Loop Over Items","type":"main","index":0}]]},"Extract Order IDs & Prepare Update":{"main":[[{"node":"Split Out1","type":"main","index":0}]]},"Check If Changes Exist":{"main":[[{"node":"Get Order Status List","type":"main","index":0},{"node":"Get Product Manufacturers","type":"main","index":0},{"node":"Extract Order IDs & Prepare Update","type":"main","index":0}],[{"node":"No Changes - Stop","type":"main","index":0}]]},"When clicking ‘Execute workflow’":{"main":[[{"node":"Code in JavaScript1","type":"main","index":0}]]},"Code in JavaScript1":{"main":[[{"node":"Get Historical Orders","type":"main","index":0}]]},"Get Historical Orders":{"main":[[{"node":"Check If Changes Exist1","type":"main","index":0}]]},"Check If Changes Exist1":{"main":[[{"node":"Check If Changes Exist","type":"main","index":0}]]}},"settings":{"saveManualExecutions":true,"executionOrder":"v1","callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":{"node:Schedule Trigger - Every 15 min":{"recurrenceRules":[]}},"meta":{"templateCredsSetupCompleted":true},"pinData":{},"versionId":"6fa2e14e-afc7-4777-83f2-7cb2e286a5fd","versionCounter":15,"triggerCount":1,"shared":[{"updatedAt":"2025-11-20T14:44:24.861Z","createdAt":"2025-11-20T14:44:24.861Z","role":"workflow:owner","workflowId":"ZAZxVj2143XdVvOe","projectId":"jlSr7IBJt4siEqiM","project":{"updatedAt":"2025-01-31T18:30:45.852Z","createdAt":"2025-01-31T18:13:38.763Z","id":"jlSr7IBJt4siEqiM","name":"N8N Estyl <n8n@estyl.pl>","type":"personal","icon":null,"description":null,"projectRelations":[{"updatedAt":"2025-01-31T18:13:38.763Z","createdAt":"2025-01-31T18:13:38.763Z","userId":"5089e5a8-9975-48ff-8451-f5c3fb3e78ac","projectId":"jlSr7IBJt4siEqiM","user":{"updatedAt":"2025-11-19T23:00:50.363Z","createdAt":"2025-01-31T18:13:37.839Z","id":"5089e5a8-9975-48ff-8451-f5c3fb3e78ac","email":"n8n@estyl.pl","firstName":"N8N","lastName":"Estyl","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2025-01-31T18:30:49.691Z","personalization_survey_n8n_version":"1.76.1"},"settings":{"userActivated":true,"firstSuccessfulWorkflowId":"xbn9AqbzxO9Xbruw","userActivatedAt":1739378108329,"easyAIWorkflowOnboarded":true,"npsSurvey":{"responded":true,"lastShownAt":1758812569793}},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2025-11-19","isPending":false}}]}}],"tags":[]}

operations:
  [{"type":"addNode","node":{"id":"init-pagination-code","name":"Initialize Pagination State","type":"n8n-nodes-base.code","typeVersion":2,"position":[112,-400],"parameters":{"mode":"runOnceForAllItems","jsCode":"// Initialize pagination state for July 2020 export\nconst startDate = new Date('2020-07-01T00:00:00Z');\nconst cursor_date = Math.floor(startDate.getTime() / 1000);\n\nreturn [{\n  json: {\n    id: 1,\n    batch_number: 0,\n    total_orders: 0,\n    status: 'ready',\n    last_updated: new Date().toISOString(),\n    cursor_date: cursor_date\n  }\n}];"}}},{"type":"addNode","node":{"id":"upsert-init-state","name":"Upsert Initial State","type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[320,-400],"parameters":{"resource":"row","operation":"upsert","dataTableId":{"mode":"id","value":"pagination_state_july2020"},"filters":{"conditions":[{"keyName":"id","condition":"eq","keyValue":"1"}]},"columns":{"mappingMode":"defineBelow","value":{"id":"={{ $json.id }}","batch_number":"={{ $json.batch_number }}","total_orders":"={{ $json.total_orders }}","status":"={{ $json.status }}","last_updated":"={{ $json.last_updated }}","cursor_date":"={{ $json.cursor_date }}"}}}}},{"type":"addNode","node":{"id":"read-pagination-state","name":"Read Pagination State","type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[528,-400],"parameters":{"resource":"row","operation":"get","dataTableId":{"mode":"id","value":"pagination_state_july2020"},"returnAll":false,"limit":1,"filters":{"conditions":[{"keyName":"id","condition":"eq","keyValue":"1"}]}}}},{"type":"removeConnection"},{"type":"addConnection"},{"type":"addConnection"},{"type":"addConnection"},{"type":"addConnection"}]


workflow_id: ZAZxVj2143XdVvOe
workflow_name: BaseLinker to Synerise CSV Export - historical backfill
trigger: partial_update
created_at: 2025-11-20 15:45:47
workflow_snapshot:
[cell repeats an earlier one in this file: 44321 characters]

operations:
  [{"type":"removeConnection","source":"27acb096-8bae-4f19-9237-5139dd7dc10f","target":"2e55bcec-9628-4f92-802d-dc31f9d01247"},{"type":"addConnection","source":"27acb096-8bae-4f19-9237-5139dd7dc10f","target":"init-pagination-code"},{"type":"addConnection","source":"init-pagination-code","target":"upsert-init-state"},{"type":"addConnection","source":"upsert-init-state","target":"read-pagination-state"},{"type":"addConnection","source":"read-pagination-state","target":"2e55bcec-9628-4f92-802d-dc31f9d01247"}]


workflow_id: ZAZxVj2143XdVvOe
workflow_name: BaseLinker to Synerise CSV Export - historical backfill
trigger: partial_update
created_at: 2025-11-20 15:46:04
workflow_snapshot:
[cell repeats an earlier one in this file: 44321 characters]

operations:
  [{"type":"addNode","node":{"id":"init-pagination-code","name":"Initialize Pagination State","type":"n8n-nodes-base.code","typeVersion":2,"position":[112,-400],"parameters":{"mode":"runOnceForAllItems","jsCode":"// Initialize pagination state for July 2020 export\nconst startDate = new Date('2020-07-01T00:00:00Z');\nconst cursor_date = Math.floor(startDate.getTime() / 1000);\n\nreturn [{\n  json: {\n    id: 1,\n    batch_number: 0,\n    total_orders: 0,\n    status: 'ready',\n    last_updated: new Date().toISOString(),\n    cursor_date: cursor_date\n  }\n}];"}}},{"type":"addNode","node":{"id":"upsert-init-state","name":"Upsert Initial State","type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[320,-400],"parameters":{"resource":"row","operation":"upsert","dataTableId":{"mode":"id","value":"pagination_state_july2020"},"filters":{"conditions":[{"keyName":"id","condition":"eq","keyValue":"1"}]},"columns":{"mappingMode":"defineBelow","value":{"id":"={{ $json.id }}","batch_number":"={{ $json.batch_number }}","total_orders":"={{ $json.total_orders }}","status":"={{ $json.status }}","last_updated":"={{ $json.last_updated }}","cursor_date":"={{ $json.cursor_date }}"}}}}},{"type":"addNode","node":{"id":"read-pagination-state","name":"Read Pagination State","type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[528,-400],"parameters":{"resource":"row","operation":"get","dataTableId":{"mode":"id","value":"pagination_state_july2020"},"returnAll":false,"limit":1,"filters":{"conditions":[{"keyName":"id","condition":"eq","keyValue":"1"}]}}}},{"type":"removeConnection","source":"27acb096-8bae-4f19-9237-5139dd7dc10f","target":"2e55bcec-9628-4f92-802d-dc31f9d01247"},{"type":"addConnection","source":"27acb096-8bae-4f19-9237-5139dd7dc10f","target":"init-pagination-code"},{"type":"addConnection","source":"init-pagination-code","target":"upsert-init-state"},{"type":"addConnection","source":"upsert-init-state","target":"read-pagination-state"},{"type":"addConnection","source":"read-pagination-state","target":"2e55bcec-9628-4f92-802d-dc31f9d01247"}]


workflow_id: ZAZxVj2143XdVvOe
workflow_name: BaseLinker to Synerise CSV Export - historical backfill
trigger: partial_update
created_at: 2025-11-20 15:57:07
workflow_snapshot:
  {"updatedAt":"2025-11-20T15:56:51.505Z","createdAt":"2025-11-20T14:44:24.861Z","id":"ZAZxVj2143XdVvOe","name":"BaseLinker to Synerise CSV Export - historical backfill","description":"","active":false,"isArchived":false,"nodes":[{"parameters":{"jsCode":"// Transform BaseLinker orders to Synerise CSV format\n  // Mode: Run Once for All Items\n  // FIXED: orderId now uses shop_order_id (short format) instead of order_id\n  // FIXED: recordedAt now uses correct UTC timestamps (removed incorrect +2h offset)\n  // FIXED: totalAmount now calculates from products + delivery (not payment_done which is 0 for COD)\n\n  const items = $input.all();\n  const results = [];\n\n  console.log(`Processing ${items.length} orders for Synerise export`);\n\n  // Map status using the enriched status_name\n  const statusMapping = {\n    'ZREALIZOWANE': 'completed',\n    'ANULOWANE': 'cancelled',\n    'ZWROT': 'returned',\n    'WYSŁANE': 'shipped'\n  };\n\n  // Process each order\n  items.forEach((item, index) => {\n    try {\n      const order = item.json;\n\n      // Generate eventSalt\n      const eventSalt = `${order.order_id}_${order.date_add || Date.now()}`;\n\n      // TIMEZONE FIX: BaseLinker returns date_confirmed as Unix timestamp in UTC\n      // No offset needed - UTC is the correct format for data systems\n      // Synerise will handle timezone conversion for display purposes\n      let recordedAt;\n      if (order.date_confirmed) {\n        const timestamp = parseInt(order.date_confirmed);\n        recordedAt = new Date(timestamp * 1000).toISOString();\n\n        // Debug log for first order\n        if (index === 0) {\n          console.log(`Timestamp conversion example:`);\n          console.log(`  Unix timestamp: ${timestamp}`);\n          console.log(`  UTC ISO format: ${recordedAt}`);\n        }\n      } else {\n        recordedAt = new Date().toISOString();\n      }\n\n      // Find appropriate status based on name\n      let orderStatus = 'unknown';\n      const statusNameUpper = (order.status_name || '').toUpperCase();\n      for (const [key, value] of Object.entries(statusMapping)) {\n        if (statusNameUpper.includes(key)) {\n          orderStatus = value;\n          break;\n        }\n      }\n\n      // Determine payment type\n      const paymentType = order.payment_method_cod === '1' ? 'Pobranie' : 'Przelew';\n\n      // Safe parseFloat\n      const safeParseFloat = (value) => {\n        const parsed = parseFloat(value);\n        return isNaN(parsed) ? 0 : parsed;\n      };\n\n      // BUGFIX #3: Calculate totalAmount from products + delivery\n      // BaseLinker API doesn't provide order total - must calculate it\n      // payment_done shows 0 for COD orders (unpaid), but order still has value\n      let totalAmount = 0;\n\n      // Calculate products subtotal\n      if (order.products && Array.isArray(order.products)) {\n        const productsSubtotal = order.products.reduce((sum, product) => {\n          const price = safeParseFloat(product.price_brutto);\n          const quantity = parseInt(product.quantity) || 0;\n          return sum + (price * quantity);\n        }, 0);\n\n        // Add delivery cost\n        totalAmount = productsSubtotal + safeParseFloat(order.delivery_price);\n\n        // Debug log for first order\n        if (index === 0) {\n          console.log(`Total amount calculation example:`);\n          console.log(`  Products subtotal: ${productsSubtotal.toFixed(2)}`);\n          console.log(`  Delivery price: ${safeParseFloat(order.delivery_price).toFixed(2)}`);\n          console.log(`  Total amount: ${totalAmount.toFixed(2)}`);\n          console.log(`  Payment done (old approach): ${safeParseFloat(order.payment_done)}`);\n        }\n      } else {\n        // Fallback: if no products array, use delivery_price only\n        totalAmount = safeParseFloat(order.delivery_price);\n      }\n\n      // Extract client name with fallbacks\n      const clientName = order.delivery_fullname ||\n                        order.invoice_fullname ||\n                        order.delivery_company ||\n                        `${order.delivery_name || ''} ${order.delivery_surname || ''}`.trim() ||\n                        'Unknown Customer';\n\n      // BUGFIX #1: Use shop_order_id (short format) instead of order_id\n      // shop_order_id examples: 184031, 184042 (human-readable short numbers)\n      // order_id examples: 790964970, 790932859 (internal BaseLinker IDs)\n      const orderId = order.shop_order_id || order.order_id; // Fallback to order_id if shop_order_id is missing\n\n      // Debug log for first order to verify fix\n      if (index === 0) {\n        console.log(`Order ID fix example:`);\n        console.log(`  OLD (order_id): ${order.order_id}`);\n        console.log(`  NEW (shop_order_id): ${order.shop_order_id}`);\n        console.log(`  Using: ${orderId}`);\n      }\n\n      // Add transformed order to results\n      results.push({\n        json: {\n          eventSalt: eventSalt,\n          orderId: orderId,\n          recordedAt: recordedAt, // FIXED: Now correct UTC format without offset\n          clientEmail: order.email || '',\n          clientName: clientName,\n          clientPhone: order.phone || '',\n          status: orderStatus,\n          statusName: order.status_name || '',\n          totalAmount: Math.round(totalAmount * 100) / 100, // FIXED: Calculated from products + delivery\n          currency: order.currency || 'PLN',\n          paymentMethod: order.payment_method || '',\n          paymentType: paymentType,\n          deliveryMethod: order.delivery_method || '',\n          deliveryPrice: safeParseFloat(order.delivery_price),\n          products: order.products || [],\n          source: order.order_source || 'BaseLinker',\n          notes: order.admin_comments || ''\n        }\n      });\n\n    } catch (error) {\n      console.error(`Error processing order at index ${index}:`, error);\n      console.error(`Order data:`, JSON.stringify(item.json).substring(0, 500));\n\n      // Add failed order with error info\n      results.push({\n        json: {\n          error: true,\n          errorMessage: error.message,\n          orderId: item.json?.order_id || 'UNKNOWN',\n          originalData: item.json\n        }\n      });\n    }\n  });\n\n  console.log(`Successfully transformed ${results.length} orders`);\n  console.log(`Sample order IDs: ${results.slice(0, 5).map(r => r.json.orderId).join(', ')}`);\n\n  // Return all transformed items\n  return results;\n"},"id":"4dc6cd3a-4e59-45f5-90ce-035791d545b4","name":"Transform to Synerise Format","type":"n8n-nodes-base.code","position":[3184,-448],"typeVersion":2},{"parameters":{"jsCode":"// Flatten nested product data for CSV\n// UPDATED: 16 columns (removed: eventSalt, clientName, clientPhone, productCategory, productEan, source, notes)\nconst items = $input.all();\nconst csvRows = [];\n\nitems.forEach(item => {\n  const order = item.json;\n  \n  // If order has products, create a row for each product\n  if (order.products && order.products.length > 0) {\n    order.products.forEach(product => {\n      csvRows.push({\n        orderId: order.orderId,\n        recordedAt: order.recordedAt,\n        clientEmail: order.clientEmail,\n        status: order.status,\n        statusName: order.statusName,\n        totalAmount: order.totalAmount,\n        currency: order.currency,\n        productSku: product.sku || product.product_id,\n        productName: product.name,\n        productBrand: product.brand || 'Unknown',\n        productPrice: product.price_brutto,\n        productQuantity: product.quantity,\n        paymentMethod: order.paymentMethod,\n        paymentType: order.paymentType,\n        deliveryMethod: order.deliveryMethod,\n        deliveryPrice: order.deliveryPrice\n      });\n    });\n  } else {\n    // Order without products\n    csvRows.push({\n      orderId: order.orderId,\n      recordedAt: order.recordedAt,\n      clientEmail: order.clientEmail,\n      status: order.status,\n      statusName: order.statusName,\n      totalAmount: order.totalAmount,\n      currency: order.currency,\n      productSku: '',\n      productName: '',\n      productBrand: '',\n      productPrice: '',\n      productQuantity: '',\n      paymentMethod: order.paymentMethod,\n      paymentType: order.paymentType,\n      deliveryMethod: order.deliveryMethod,\n      deliveryPrice: order.deliveryPrice\n    });\n  }\n});\n\n// Generate filename with timestamp\nconst now = new Date();\nconst filename = `synerise_export_${now.toISOString().slice(0,10)}_${now.toTimeString().slice(0,5).replace(':','')}.csv`;\n\nreturn [{\n  json: {\n    filename: filename,\n    rowCount: csvRows.length,\n    exportTime: now.toISOString(),\n    data: csvRows\n  }\n}];"},"id":"6c52f816-b911-4c5d-9264-b66c81c5f06a","name":"Prepare CSV Data","type":"n8n-nodes-base.code","position":[3408,-448],"typeVersion":2},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getOrders"},{"name":"parameters","value":"={{ JSON.stringify({ order_id: parseInt($json.json.orderId), get_unconfirmed_orders: false }) }}"}]},"options":{"timeout":30000}},"id":"15ca7ed2-b332-49ee-8168-ae2c30ae43c3","name":"Get Orders by IDs","type":"n8n-nodes-base.httpRequest","position":[1616,-32],"typeVersion":4.2,"retryOnFail":true,"maxTries":5,"waitBetweenTries":5000},{"parameters":{"jsCode":"// Extract Product IDs - WERSJA Z JEDNYM OUTPUTEM I RÓŻNYMI TYPAMI\nconst items = $input.all();\nconst allProductIds = new Set();\nconst allOrders = [];\n\n// Aggregate node zwraca wszystko w polu \"data\"\nconst aggregatedData = items[0]?.json?.data;\n\nif (aggregatedData && Array.isArray(aggregatedData)) {\n  // Sprawdź strukturę danych z Aggregate\n  aggregatedData.forEach(item => {\n    // Jeśli każdy item ma pole \"orders\" (batche z BaseLinker)\n    if (item.orders && Array.isArray(item.orders)) {\n      allOrders.push(...item.orders);\n    } \n    // Jeśli to bezpośrednio zamówienia (spłaszczone)\n    else if (item.order_id) {\n      allOrders.push(item);\n    }\n    // Jeśli to zagnieżdżone w json\n    else if (item.json?.orders) {\n      allOrders.push(...item.json.orders);\n    }\n    else if (item.json?.order_id) {\n      allOrders.push(item.json);\n    }\n  });\n} \n// Fallback - może dane są bezpośrednio w items\nelse {\n  items.forEach(item => {\n    if (item.json?.orders) {\n      allOrders.push(...item.json.orders);\n    } else if (item.json?.order_id) {\n      allOrders.push(item.json);\n    }\n  });\n}\n\n// Zbierz wszystkie unikalne product IDs\nallOrders.forEach(order => {\n  if (order.products && Array.isArray(order.products)) {\n    order.products.forEach(product => {\n      if (product.product_id) {\n        allProductIds.add(parseInt(product.product_id));\n      }\n    });\n  }\n});\n\nconst productIdsArray = Array.from(allProductIds);\n\n// Podziel produkty na batche po 100\nconst BATCH_SIZE = 100;\nconst productBatches = [];\nfor (let i = 0; i < productIdsArray.length; i += BATCH_SIZE) {\n  productBatches.push(productIdsArray.slice(i, i + BATCH_SIZE));\n}\n\nconsole.log('=== EXTRACT PRODUCT IDS ===');\nconsole.log(`Total orders: ${allOrders.length}`);\nconsole.log(`Unique products: ${productIdsArray.length}`);\nconsole.log(`Product batches: ${productBatches.length} (${BATCH_SIZE} per batch)`);\nproductBatches.forEach((batch, index) => {\n  console.log(`  Batch ${index + 1}: ${batch.length} products`);\n});\n\n// Przygotuj output items\nconst outputItems = [];\n\n// PIERWSZY ITEM: Zamówienia (dla Merge)\noutputItems.push({\n  json: {\n    type: 'ordersData',  // Unikalny typ dla Switch\n    orders: allOrders,\n    stats: {\n      batchOrderCount: allOrders.length,\n      uniqueProductCount: allProductIds.size,\n      totalBatches: productBatches.length,\n      avgProductsPerOrder: (allOrders.reduce((sum, o) => sum + (o.products?.length || 0), 0) / allOrders.length).toFixed(2)\n    }\n  }\n});\n\n// KOLEJNE ITEMY: Każdy batch produktów (dla Loop/Get Product Brands)\nproductBatches.forEach((batch, index) => {\n  outputItems.push({\n    json: {\n      type: 'productBatch',  // Unikalny typ dla Switch\n      batchIndex: index + 1,\n      totalBatches: productBatches.length,\n      productIdsToEnrich: batch,\n      batchSize: batch.length\n    }\n  });\n});\n\nconsole.log(`Returning ${outputItems.length} items: 1 ordersData + ${productBatches.length} productBatch(es)`);\n\nreturn outputItems;"},"id":"ec3603bf-23ec-4859-98d3-354391f11e8e","name":"Extract Product IDs","type":"n8n-nodes-base.code","typeVersion":2,"position":[1616,-272]},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getInventoryProductsData"},{"name":"parameters","value":"={{ JSON.stringify({ \n  inventory_id: 758, \n  products: $json.productIdsToEnrich || [] \n}) }}"}]},"options":{"timeout":30000}},"id":"3417e7b6-d432-4e68-8500-6ea48ce52427","name":"Get Product Brands","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2288,-64],"retryOnFail":true},{"parameters":{"jsCode":"// FIXED: Merge Product Data - Correctly extracts data from Merge node\n// This version properly handles the 4 inputs from the Merge node\n\nconst allInputs = $input.all();\nconsole.log(`=== MERGE PRODUCT DATA - DEBUGGING ===`);\nconsole.log(`Total inputs received: ${allInputs.length}`);\n\n// Initialize data storage\nlet manufacturersData = [];\nlet ordersData = [];\nlet productsData = {};\nlet statusesData = [];\n\n// The Merge node combines 4 inputs:\n// Input 0: Product Manufacturers\n// Input 1: Orders (from Switch - ordersData branch)  \n// Input 2: Aggregated Products\n// Input 3: Order Status List\n\n// Process each input and extract the correct data\nallInputs.forEach((item, index) => {\n  const data = item.json;\n  \n  console.log(`\\nInput ${index} analysis:`, {\n    hasManufacturers: !!(data.manufacturers || data.manufacturer),\n    hasOrders: !!(data.orders || data.type === 'ordersData'),\n    hasProducts: !!(data.products || data.data),\n    hasStatuses: !!(data.statuses || data.status),\n    topKeys: Object.keys(data).slice(0, 5),\n    dataType: data.type || 'unknown'\n  });\n  \n  // Extract manufacturers (usually first input from Get Product Manufacturers)\n  if (data.manufacturers && Array.isArray(data.manufacturers)) {\n    manufacturersData = data.manufacturers;\n    console.log(`✓ Found ${manufacturersData.length} manufacturers`);\n  }\n  \n  // Extract orders (from ordersData type)\n  if (data.type === 'ordersData' && data.orders) {\n    ordersData = data.orders;\n    console.log(`✓ Found ${ordersData.length} orders from ordersData`);\n  } else if (data.orders && Array.isArray(data.orders)) {\n    ordersData = data.orders;\n    console.log(`✓ Found ${ordersData.length} orders`);\n  }\n  \n  // Extract products from aggregated data\n  if (data.data && Array.isArray(data.data)) {\n    // This is the aggregated products output\n    console.log(`✓ Found aggregated data with ${data.data.length} items`);\n    data.data.forEach(item => {\n      if (item.products && typeof item.products === 'object') {\n        Object.assign(productsData, item.products);\n      }\n    });\n  } else if (data.products && typeof data.products === 'object') {\n    // Direct products object\n    Object.assign(productsData, data.products);\n  }\n  \n  // Extract statuses\n  if (data.statuses && Array.isArray(data.statuses)) {\n    statusesData = data.statuses;\n    console.log(`✓ Found ${statusesData.length} order statuses`);\n  }\n});\n\n// Create manufacturer mapping (ID → Name)\nconst manufacturerMap = {};\nmanufacturersData.forEach(manufacturer => {\n  const id = String(manufacturer.manufacturer_id);\n  manufacturerMap[id] = manufacturer.name;\n});\n\nconsole.log(`\\n=== DATA SUMMARY ===`);\nconsole.log(`Manufacturers: ${Object.keys(manufacturerMap).length}`);\nconsole.log(`Sample manufacturers:`, Object.entries(manufacturerMap).slice(0, 5));\nconsole.log(`Products with brand data: ${Object.keys(productsData).length}`);\nconsole.log(`Orders to process: ${ordersData.length}`);\nconsole.log(`Statuses: ${statusesData.length}`);\n\n// Create status mapping\nconst statusMap = {};\nstatusesData.forEach(status => {\n  statusMap[String(status.id)] = status.name;\n});\n\n// Statistics\nlet stats = {\n  totalProducts: 0,\n  productsWithBrand: 0,\n  productsWithoutBrand: 0,\n  missingProductData: 0,\n  uniqueBrands: new Set(),\n  missingProductIds: new Set()\n};\n\n// Process and enrich orders\nconst enrichedOrders = ordersData.map((order, orderIndex) => {\n  // Clone the order to avoid mutation\n  const enrichedOrder = JSON.parse(JSON.stringify(order));\n  \n  // Add status name\n  enrichedOrder.status_name = statusMap[String(order.order_status_id)] || 'Unknown';\n  \n  // Process products in the order\n  if (enrichedOrder.products && Array.isArray(enrichedOrder.products)) {\n    enrichedOrder.products = enrichedOrder.products.map((product, productIndex) => {\n      stats.totalProducts++;\n      \n      const productIdStr = String(product.product_id);\n      const productData = productsData[productIdStr];\n      \n      // Initialize brand as Unknown\n      let brandName = 'Unknown';\n      let categoryName = '';\n      let ean = product.ean || '';\n      \n      if (productData) {\n        // Get manufacturer/brand name\n        if (productData.manufacturer_id) {\n          const manufacturerIdStr = String(productData.manufacturer_id);\n          const manufacturer = manufacturerMap[manufacturerIdStr];\n          \n          if (manufacturer) {\n            brandName = manufacturer;\n            stats.uniqueBrands.add(brandName);\n            stats.productsWithBrand++;\n          } else {\n            console.warn(`Manufacturer ID ${manufacturerIdStr} not found in map for product ${productIdStr}`);\n            stats.productsWithoutBrand++;\n          }\n        } else {\n          stats.productsWithoutBrand++;\n        }\n        \n        // Get category from text_fields\n        if (productData.text_fields) {\n          categoryName = productData.text_fields.extra_field_851 || '';\n        }\n        \n        // Get EAN\n        ean = productData.ean || product.ean || '';\n        \n      } else {\n        stats.missingProductData++;\n        stats.missingProductIds.add(productIdStr);\n        \n        if (orderIndex < 3 && productIndex < 2) {\n          console.warn(`No product data found for ID: ${productIdStr}`);\n        }\n      }\n      \n      // Debug first few products\n      if (orderIndex === 0 && productIndex < 3) {\n        console.log(`\\nSample enrichment - Product ${productIdStr}:`, {\n          originalName: product.name,\n          brand: brandName,\n          hasProductData: !!productData,\n          manufacturerId: productData?.manufacturer_id,\n          category: categoryName ? categoryName.substring(0, 50) + '...' : 'None'\n        });\n      }\n      \n      // Return enriched product\n      return {\n        ...product,\n        brand: brandName,\n        category_name: categoryName,\n        ean: ean\n      };\n    });\n  }\n  \n  return enrichedOrder;\n});\n\n// Calculate coverage\nconst coverage = stats.totalProducts > 0 \n  ? ((stats.productsWithBrand / stats.totalProducts) * 100).toFixed(1)\n  : 0;\n\nconsole.log(`\\n=== ENRICHMENT RESULTS ===`);\nconsole.log(`Total products: ${stats.totalProducts}`);\nconsole.log(`Products with brands: ${stats.productsWithBrand} (${coverage}%)`);\nconsole.log(`Products without brands: ${stats.productsWithoutBrand}`);\nconsole.log(`Products missing data: ${stats.missingProductData}`);\nconsole.log(`Unique brands found: ${stats.uniqueBrands.size}`);\nconsole.log(`Brand names:`, Array.from(stats.uniqueBrands).slice(0, 10).join(', '));\n\nif (stats.missingProductData > 0) {\n  console.warn(`\\n⚠️ Missing product IDs (first 10):`, \n    Array.from(stats.missingProductIds).slice(0, 10).join(', '));\n}\n\n// Return results\nconst output = [];\n\n// Add statistics as first item\noutput.push({\n  json: {\n    _type: 'statistics',\n    _timestamp: new Date().toISOString(),\n    coverage: {\n      total_products: stats.totalProducts,\n      with_brand: stats.productsWithBrand,\n      without_brand: stats.productsWithoutBrand,\n      missing_data: stats.missingProductData,\n      coverage_percent: `${coverage}%`,\n      unique_brands: stats.uniqueBrands.size\n    },\n    brands: Array.from(stats.uniqueBrands),\n    missing_ids: Array.from(stats.missingProductIds).slice(0, 20)\n  }\n});\n\n// Add enriched orders\nenrichedOrders.forEach(order => {\n  output.push({ json: order });\n});\n\nreturn output;"},"id":"aa52a712-23fe-42e2-b71a-414707430739","name":"Merge Product Data","type":"n8n-nodes-base.code","typeVersion":2,"position":[2736,-448]},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getInventoryManufacturers"},{"name":"parameters","value":"{\"inventory_id\":758}"}]},"options":{"timeout":30000}},"id":"8098ddd8-f311-48f1-b7e0-bc13de4a4712","name":"Get Product Manufacturers","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2288,-704],"retryOnFail":true},{"parameters":{"mode":"combine","combineBy":"combineByPosition","numberInputs":4,"options":{}},"type":"n8n-nodes-base.merge","typeVersion":3.2,"position":[2512,-480],"id":"5e3a6a8e-44a2-444a-a3bc-6d162dd4cf94","name":"Merge"},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getOrderStatusList"},{"name":"parameters","value":"{}"}]},"options":{}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2288,256],"id":"e923ab81-95d9-43c5-9271-e76a4fecd0aa","name":"Get Order Status List","retryOnFail":true},{"parameters":{"jsCode":"// Konfiguracja dozwolonych statusów - zgodnie z wymaganiami klienta\n// Tylko statusy końcowe: zrealizowane, anulowane, zwroty\n\nconst allowedStatuses = [\n  // === WYSŁANE/ZREALIZOWANE/DOSTARCZONE ===\n  300027, // WYSŁANE // .PL\n  300028, // WYSŁANE // _PL\n  361867, // DE // Wysłane\n  386333, // SM // WYSŁANE\n  354789, // Dostarczone\n  354788, // Paczka w punkcie\n  300828, // OO // Odebrane\n  \n  // === ANULOWANE ===\n  301588, // Anulowane (przez klienta)\n  302472, // Anulowane (brak wpłaty)\n  302473, // Anulowane (zwrot paczki)\n  302475, // Anulowane (brak towaru)\n  303313, // Anulowane STA\n  304745, // Anulacja przed wysyłką\n  316385, // Anulacje (testowe)\n  359429, // WMS Anulowane\n  302981, // [Optima] Anulacja RO\n  \n  // === ZWROTY ===\n  300346, // ZWR // Przedpłacone\n  303161, // ZWR // Deklaracja zwrotu\n  311537, // SOK // Zwroty paczek\n  354792, // Zwrot kurierski\n  372310, // MolZwrotPrzyjęty [sok] zwrot$\n  374054, // Zwroty MADS (Hebe i Mitago_EU)\n  374055, // Zwroty MAGU Kau+Pigu\n  356655, // [MOL] Zwroty częściowe\n  356674, // EXC zwrócone\n  \n  // === NIEDOSTARCZONE (może być traktowane jako anulowane) ===\n  354791  // Niedostarczone\n];\n\n// Mapowanie kategorii statusów dla lepszego raportowania\nconst statusCategories = {\n  'ZREALIZOWANE': [300027, 300028, 361867, 386333, 354789, 354788, 300828],\n  'ANULOWANE': [301588, 302472, 302473, 302475, 303313, 304745, 316385, 359429, 302981],\n  'ZWROTY': [300346, 303161, 311537, 354792, 372310, 374054, 374055, 356655, 356674],\n  'NIEDOSTARCZONE': [354791]\n};\n\n// Pobierz wszystkie zamówienia\nconst items = $input.all();\n\nconsole.log(\"=== FILTER ORDER STATUS ===\");\nconsole.log(`Total orders received: ${items.length}`);\n\n// Zbierz statystyki\nconst statusStats = {};\nconst categoryStats = {\n  'ZREALIZOWANE': 0,\n  'ANULOWANE': 0,\n  'ZWROTY': 0,\n  'NIEDOSTARCZONE': 0,\n  'OTHER': 0\n};\n\nlet passedCount = 0;\nlet filteredCount = 0;\n\n// Filtruj zamówienia\nconst filteredOrders = items.filter(item => {\n  const order = item.json;\n  const statusId = parseInt(order.order_status_id);\n  const statusName = order.status_name || 'Unknown';\n  \n  // Zbierz statystyki szczegółowe\n  const key = `${statusId} - ${statusName}`;\n  statusStats[key] = (statusStats[key] || 0) + 1;\n  \n  // Określ kategorię statusu\n  let category = 'OTHER';\n  for (const [cat, ids] of Object.entries(statusCategories)) {\n    if (ids.includes(statusId)) {\n      category = cat;\n      break;\n    }\n  }\n  categoryStats[category]++;\n  \n  // Sprawdź czy status jest dozwolony\n  const isAllowed = allowedStatuses.includes(statusId);\n  \n  if (isAllowed) {\n    passedCount++;\n    console.log(`  ✅ Order ${order.order_id}: Status ${statusId} (${statusName}) - ${category}`);\n  } else {\n    filteredCount++;\n    if (filteredCount <= 5) { // Pokaż tylko pierwsze 5 odrzuconych\n      console.log(`  ❌ Order ${order.order_id}: Status ${statusId} (${statusName}) - FILTERED OUT`);\n    }\n  }\n  \n  return isAllowed;\n});\n\n// Raport podsumowujący\nconsole.log(\"\\n=== STATUS CATEGORY SUMMARY ===\");\nconsole.log(`📦 Zrealizowane: ${categoryStats.ZREALIZOWANE} orders`);\nconsole.log(`❌ Anulowane: ${categoryStats.ANULOWANE} orders`);\nconsole.log(`↩️ Zwroty: ${categoryStats.ZWROTY} orders`);\nconsole.log(`⚠️ Niedostarczone: ${categoryStats.NIEDOSTARCZONE} orders`);\nconsole.log(`❓ Inne (odrzucone): ${categoryStats.OTHER} orders`);\n\nconsole.log(\"\\n=== DETAILED STATUS DISTRIBUTION ===\");\n// Sortuj statusy według liczby zamówień\nconst sortedStats = Object.entries(statusStats)\n  .sort((a, b) => b[1] - a[1])\n  .slice(0, 10); // Pokaż top 10\n\nsortedStats.forEach(([status, count]) => {\n  const statusId = parseInt(status.split(' ')[0]);\n  const allowed = allowedStatuses.includes(statusId) ? '✅' : '❌';\n  \n  // Znajdź kategorię\n  let category = 'OTHER';\n  for (const [cat, ids] of Object.entries(statusCategories)) {\n    if (ids.includes(statusId)) {\n      category = cat;\n      break;\n    }\n  }\n  \n  console.log(`  ${allowed} ${status}: ${count} orders [${category}]`);\n});\n\nconsole.log(`\\n=== FINAL SUMMARY ===`);\nconsole.log(`✅ Passed to Synerise: ${passedCount} orders`);\nconsole.log(`❌ Filtered out: ${filteredCount} orders`);\nconsole.log(`📊 Filter efficiency: ${((passedCount/items.length)*100).toFixed(1)}%`);\n\n// Ostrzeżenie jeśli wszystko odrzucone\nif (passedCount === 0 && items.length > 0) {\n  console.error(\"⚠️ WARNING: All orders were filtered out! Check status configuration.\");\n}\n\n// WAŻNE: Zwróć przefiltrowane zamówienia jako items\nreturn filteredOrders;"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[2960,-448],"id":"48ed4ca2-613f-4699-9553-6a59168168e5","name":"Filter Order Status"},{"parameters":{"jsCode":"// Generate CSV with semicolon delimiter (16 columns)\n// UPDATED: Changed from 23 columns (comma) to 16 columns (semicolon)\nconst input = $input.all()[0].json;\nconst csvRows = input.data;\n\n// 16 columns (removed: eventSalt, clientName, clientPhone, productCategory, productEan, source, notes)\nconst headers = [\n  'orderId',\n  'recordedAt',\n  'clientEmail',\n  'status',\n  'statusName',\n  'totalAmount',\n  'currency',\n  'productSku',\n  'productName',\n  'productBrand',\n  'productPrice',\n  'productQuantity',\n  'paymentMethod',\n  'paymentType',\n  'deliveryMethod',\n  'deliveryPrice'\n];\n\n// Function to escape CSV values for semicolon delimiter\nfunction escapeCsvValue(value) {\n  if (value === null || value === undefined) return '';\n  const stringValue = String(value);\n  \n  // If contains semicolon, double quote, or newline, wrap in quotes\n  if (stringValue.includes(';') || stringValue.includes('\"') || stringValue.includes('\\n')) {\n    return `\"${stringValue.replace(/\"/g, '\"\"')}\"`;\n  }\n  return stringValue;\n}\n\n// Generate CSV with SEMICOLON delimiter\nlet csvContent = headers.join(';') + '\\n';\n\n// Add data rows\ncsvRows.forEach(row => {\n  const values = headers.map(header => escapeCsvValue(row[header]));\n  csvContent += values.join(';') + '\\n';\n});\n\nconsole.log(`Generated CSV: ${csvRows.length} rows, 16 columns, semicolon delimiter`);\n\n// Return as binary data\nreturn {\n  json: {\n    filename: input.filename,\n    rowCount: csvRows.length,\n    columnCount: headers.length,\n    delimiter: 'semicolon'\n  },\n  binary: {\n    data: {\n      data: Buffer.from(csvContent, 'utf8').toString('base64'),\n      mimeType: 'text/csv',\n      fileName: input.filename\n    }\n  }\n};"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[3632,-448],"id":"5a820d61-40f2-40eb-8a98-ba40fa84f8d0","name":"Code in JavaScript"},{"parameters":{"fieldToSplitOut":"[\"0\"]","options":{}},"type":"n8n-nodes-base.splitOut","typeVersion":1,"position":[1168,-80],"id":"e9603521-9b07-44ee-8587-503b8cdb1d27","name":"Split Out1"},{"parameters":{"options":{}},"type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[1392,-80],"id":"8880595d-e24f-4e1e-8306-1419e710ba3b","name":"Loop Over Items"},{"parameters":{"options":{}},"type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[2064,-208],"id":"b3546273-74ce-4094-870e-b7aabee04600","name":"Loop Over Items1"},{"parameters":{"rules":{"values":[{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"leftValue":"={{ $json.type }}","rightValue":"ordersData","operator":{"type":"string","operation":"equals"},"id":"16455973-160a-451f-9ce5-d3e00d4ec21c"}],"combinator":"and"},"renameOutput":true,"outputKey":"orders"},{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"id":"d0ff60de-b3c8-45e4-8d2e-b00c0acc1685","leftValue":"={{ $json.type }}","rightValue":"productBatch","operator":{"type":"string","operation":"equals","name":"filter.operator.equals"}}],"combinator":"and"},"renameOutput":true,"outputKey":"product"}]},"options":{}},"type":"n8n-nodes-base.switch","typeVersion":3.3,"position":[1840,-272],"id":"bded8fd0-05ff-4eb6-8d6c-cc30cebe60b9","name":"Switch"},{"parameters":{"aggregate":"aggregateAllItemData","options":{}},"type":"n8n-nodes-base.aggregate","typeVersion":1,"position":[2288,-256],"id":"637e4855-58c9-434e-a41b-86a01d129154","name":"Aggregate"},{"parameters":{"jsCode":"// Filter to include ONLY online shop orders (inclusion-based approach)\n// FIXED: Changed from exclusion list to checking order_source field directly\n// This ensures ALL marketplaces are excluded (Allegro, Kaufland, Hebe, etc.)\n\nconst items = $input.all();\nconst orders = items[0]?.json?.orders || [];\n\nconsole.log('=== FILTER ONLINE SHOP ORDERS (INCLUSION-BASED) ===');\nconsole.log(`Total orders before filtering: ${orders.length}`);\n\n// Statistics\nconst stats = {\n  total: orders.length,\n  onlineShop: 0,\n  marketplace: 0,\n  unknown: 0,\n  bySource: {}\n};\n\n// Filter orders - ONLY include shop orders\nconst filteredOrders = orders.filter(order => {\n  const orderSource = (order.order_source || '').toLowerCase();\n  const orderSourceId = order.order_source_id;\n  \n  // Track statistics by source\n  const sourceName = order.order_source || 'Unknown';\n  stats.bySource[sourceName] = (stats.bySource[sourceName] || 0) + 1;\n  \n  // INCLUSION FILTER: Only accept orders where source is \"shop\"\n  const isShop = orderSource === 'shop' || orderSource.includes('shop');\n  \n  if (isShop) {\n    stats.onlineShop++;\n    if (stats.onlineShop <= 5) {\n      console.log(`  ✅ INCLUDED: Order ${order.order_id} from '${sourceName}' (source_id: ${orderSourceId})`);\n    }\n    return true;\n  } else if (!orderSource || orderSource === 'unknown') {\n    stats.unknown++;\n    if (stats.unknown <= 3) {\n      console.log(`  ⚠️ UNKNOWN: Order ${order.order_id} has unknown/empty source (source_id: ${orderSourceId})`);\n    }\n    return false;\n  } else {\n    stats.marketplace++;\n    if (stats.marketplace <= 5) {\n      console.log(`  ❌ EXCLUDED: Order ${order.order_id} from '${sourceName}' (source_id: ${orderSourceId})`);\n    }\n    return false;\n  }\n});\n\nconsole.log('\\n=== FILTERING RESULTS (INCLUSION-BASED) ===');\nconsole.log(`✅ Online shop orders: ${stats.onlineShop}`);\nconsole.log(`❌ Marketplace orders (excluded): ${stats.marketplace}`);\nconsole.log(`⚠️ Unknown source (excluded): ${stats.unknown}`);\nconsole.log(`📊 Filter efficiency: ${((stats.onlineShop/stats.total)*100).toFixed(1)}% kept`);\n\nconsole.log('\\n=== ORDERS BY SOURCE ===');\nObject.entries(stats.bySource)\n  .sort((a, b) => b[1] - a[1])\n  .forEach(([source, count]) => {\n    const isShop = source.toLowerCase().includes('shop');\n    const emoji = isShop ? '✅' : '❌';\n    console.log(`  ${emoji} ${source}: ${count} orders`);\n  });\n\nconsole.log('\\n=== FILTER APPROACH ===');\nconsole.log('Using INCLUSION-based filter: only order_source=\"shop\" passes');\nconsole.log('This automatically excludes ALL marketplaces:');\nconsole.log('  - Allegro, Kaufland, Hebe, Empik, eMag, etc.');\nconsole.log('  - Any future marketplace integrations');\n\n// Return filtered orders in the same structure\nreturn [{\n  json: {\n    orders: filteredOrders,\n    _filterStats: stats,\n    _filterApproach: 'inclusion-based',\n    _filterCriteria: 'order_source === \"shop\"',\n    _timestamp: new Date().toISOString()\n  }\n}];"},"id":"3312a255-a865-4d0f-b4d4-112045b1767f","name":"Filter Online Shop Orders","type":"n8n-nodes-base.code","typeVersion":2,"position":[1840,16]},{"parameters":{"jsCode":"// Extract unique order IDs from journal\nconst items = $input.all();\nconst logs = items[0]?.json?.logs || [];\n\n// Get unique order IDs\nconst orderIds = [...new Set(logs.map(log => log.order_id).filter(id => id))];\n\n// Przygotuj outputy\nlet output0 = []; // Order IDs do przetworzenia\nlet output1 = []; // Dane do Google Sheets update\n\nif (logs.length > 0) {\n  // Znajdź log z najwyższą datą (najświeższy)\n  const newestLog = logs.reduce((newest, current) => {\n    return current.date > newest.date ? current : newest;\n  }, logs[0]);\n  \n  const newLastLogId = newestLog.log_id;\n  const newestDate = new Date(newestLog.date * 1000).toISOString();\n  \n  console.log(`Found ${logs.length} logs, ${orderIds.length} unique orders`);\n  console.log(`Newest log_id: ${newLastLogId} from date: ${newestDate}`);\n  console.log(`Newest log details:`, newestLog);\n  \n  // Output 0: Order IDs do przetworzenia\n  output0 = orderIds.map((orderId, index) => ({\n    json: {\n      orderId: orderId,\n      totalCount: orderIds.length,\n      currentIndex: index + 1\n    }\n  }));\n  \n  // Output 1: Dane do update Google Sheets (4 wiersze - dodałem newest_date)\n  output1 = [\n    {\n      json: {\n        key: 'last_log_id',\n        value: newLastLogId.toString(),\n        row_number: 2\n      }\n    },\n    {\n      json: {\n        key: 'last_sync',\n        value: new Date().toISOString(),\n        row_number: 3\n      }\n    },\n    {\n      json: {\n        key: 'total_orders',\n        value: orderIds.length.toString(),\n        row_number: 4\n      }\n    },\n    {\n      json: {\n        key: 'newest_log_date',\n        value: newestDate,\n        row_number: 5\n      }\n    }\n  ];\n} else {\n  console.log(\"No new orders to process\");\n}\n\n// Zwróć dane dla obu outputów\nreturn {\n  0: output0,  // Idzie do Loop Over Items\n  1: output1   // Idzie do Google Sheets Update\n};"},"id":"b28a21d8-2e1d-4af5-95fe-313744f427ea","name":"Extract Order IDs & Prepare Update","type":"n8n-nodes-base.code","position":[944,-272],"typeVersion":2},{"parameters":{},"id":"6a75883f-a764-487d-943c-0d9aabb925c2","name":"No Changes - Stop","type":"n8n-nodes-base.noOp","position":[944,112],"typeVersion":1},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":1},"conditions":[{"leftValue":"={{ $json.logs?.length }}","rightValue":0,"operator":{"type":"number","operation":"gt"},"id":"fb5b9b41-36c6-408f-a7ce-41f8c97e3146"}],"combinator":"and"},"options":{}},"id":"f10b58dc-75a8-4be2-8b34-1a4070c97f71","name":"Check If Changes Exist","type":"n8n-nodes-base.if","position":[720,-176],"typeVersion":2},{"parameters":{},"type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[-96,-176],"id":"27acb096-8bae-4f19-9237-5139dd7dc10f","name":"When clicking ‘Execute workflow’"},{"parameters":{"jsCode":"// Set date range for historical retrieval\n  const daysBack = 60; // Last 30 days\n  const dateFrom = Math.floor(Date.now() / 1000) - (daysBack * 24 * 60 * 60);\n  const dateTo = Math.floor(Date.now() / 1000); // Now\n\n  console.log(`Fetching orders from: ${new Date(dateFrom * 1000).toISOString()}`);\n  console.log(`To: ${new Date(dateTo * 1000).toISOString()}`);\n  console.log(`Days back: ${daysBack}`);\n\n  return [{\n    json: {\n      dateFrom: dateFrom,\n      dateTo: dateTo,\n      daysBack: daysBack\n    }\n  }]"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[112,-176],"id":"2e55bcec-9628-4f92-802d-dc31f9d01247","name":"Code in JavaScript1"},{"parameters":{"method":"POST","url":"https://api.baselinker.com/connector.php","sendHeaders":true,"headerParameters":{"parameters":[{"name":"X-BLToken","value":"1193-5331-52DFFBYR9Q6NWJXPL8LT3YCBC4KUJAP8L51HFN8Z3K31WGGDJ00T56A4UA69YXJI"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"method","value":"getOrders"},{"name":"parameters","value":"={{ JSON.stringify({ date_confirmed_from: $json.dateFrom, get_unconfirmed_orders: false }) }}"}]},"options":{}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.3,"position":[320,-176],"id":"137fe081-3884-42b0-9c47-4f4ded00479a","name":"Get Historical Orders"},{"parameters":{"jsCode":"// Transform getOrders response to logs format expected by downstream nodes\n  const items = $input.all();\n  const response = items[0]?.json;\n\n  // Get orders from response\n  const orders = response?.orders || [];\n\n  console.log('=== TRANSFORM GETORDERS TO LOGS FORMAT ===');\n  console.log(`Received ${orders.length} orders from getOrders API`);\n\n  if (orders.length === 0) {\n    console.log('⚠️ No orders returned from API');\n    return [{\n      json: {\n        status: \"SUCCESS\",\n        logs: [],\n        _metadata: {\n          source: \"getOrders_historical\",\n          orderCount: 0,\n          processedAt: new Date().toISOString()\n        }\n      }\n    }];\n  }\n\n  // Create synthetic logs array from orders\n  // Each order becomes a log entry\n  const logs = orders.map(order => ({\n    log_id: order.order_id, // Use order_id as log_id\n    log_type: 1, // Type 1 = order event\n    order_id: order.order_id,\n    object_id: 0,\n    date: order.date_confirmed || order.date_add // Use order date\n  }));\n\n  console.log(`✅ Created ${logs.length} log entries from orders`);\n  console.log(`Date range: ${new Date(orders[0].date_confirmed * 1000).toISOString()} to ${new Date(orders[orders.length-1].date_confirmed * 1000).toISOString()}`);\n\n  // Return in format expected by \"Check If Changes Exist\"\n  return [{\n    json: {\n      status: \"SUCCESS\",\n      logs: logs,\n      _metadata: {\n        source: \"getOrders_historical\",\n        orderCount: orders.length,\n        oldestOrder: new Date(orders[0].date_confirmed * 1000).toISOString(),\n        newestOrder: new Date(orders[orders.length-1].date_confirmed * 1000).toISOString(),\n        processedAt: new Date().toISOString()\n      }\n    }\n  }];"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[528,-176],"id":"a0097467-1b8f-409c-b1ab-57fa5a1852ff","name":"Check If Changes Exist1"}],"connections":{"Transform to Synerise Format":{"main":[[{"node":"Prepare CSV Data","type":"main","index":0}]]},"Prepare CSV Data":{"main":[[{"node":"Code in JavaScript","type":"main","index":0}]]},"Extract Product IDs":{"main":[[{"node":"Switch","type":"main","index":0}]]},"Get Product Brands":{"main":[[{"node":"Loop Over Items1","type":"main","index":0}]]},"Merge Product Data":{"main":[[{"node":"Filter Order Status","type":"main","index":0}]]},"Get Product Manufacturers":{"main":[[{"node":"Merge","type":"main","index":0}]]},"Get Order Status List":{"main":[[{"node":"Merge","type":"main","index":3}]]},"Merge":{"main":[[{"node":"Merge Product Data","type":"main","index":0}]]},"Filter Order Status":{"main":[[{"node":"Transform to Synerise Format","type":"main","index":0}]]},"Split Out1":{"main":[[{"node":"Loop Over Items","type":"main","index":0}]]},"Loop Over Items":{"main":[[{"node":"Extract Product IDs","type":"main","index":0}],[{"node":"Get Orders by IDs","type":"main","index":0}]]},"Loop Over Items1":{"main":[[{"node":"Aggregate","type":"main","index":0}],[{"node":"Get Product Brands","type":"main","index":0}]]},"Switch":{"main":[[{"node":"Merge","type":"main","index":1}],[{"node":"Loop Over Items1","type":"main","index":0}]]},"Aggregate":{"main":[[{"node":"Merge","type":"main","index":2}]]},"Get Orders by IDs":{"main":[[{"node":"Filter Online Shop Orders","type":"main","index":0}]]},"Filter Online Shop Orders":{"main":[[{"node":"Loop Over Items","type":"main","index":0}]]},"Extract Order IDs & Prepare Update":{"main":[[{"node":"Split Out1","type":"main","index":0}]]},"Check If Changes Exist":{"main":[[{"node":"Get Order Status List","type":"main","index":0},{"node":"Get Product Manufacturers","type":"main","index":0},{"node":"Extract Order IDs & Prepare Update","type":"main","index":0}],[{"node":"No Changes - Stop","type":"main","index":0}]]},"When clicking ‘Execute workflow’":{"main":[[{"node":"Code in JavaScript1","type":"main","index":0}]]},"Code in JavaScript1":{"main":[[{"node":"Get Historical Orders","type":"main","index":0}]]},"Get Historical Orders":{"main":[[{"node":"Check If Changes Exist1","type":"main","index":0}]]},"Check If Changes Exist1":{"main":[[{"node":"Check If Changes Exist","type":"main","index":0}]]}},"settings":{"saveManualExecutions":true,"executionOrder":"v1","callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":{"node:Schedule Trigger - Every 15 min":{"recurrenceRules":[]}},"meta":{"templateCredsSetupCompleted":true},"pinData":{},"versionId":"ae35cd0a-8690-4119-a102-9fb916c762a0","versionCounter":16,"triggerCount":1,"shared":[{"updatedAt":"2025-11-20T14:44:24.861Z","createdAt":"2025-11-20T14:44:24.861Z","role":"workflow:owner","workflowId":"ZAZxVj2143XdVvOe","projectId":"jlSr7IBJt4siEqiM","project":{"updatedAt":"2025-01-31T18:30:45.852Z","createdAt":"2025-01-31T18:13:38.763Z","id":"jlSr7IBJt4siEqiM","name":"N8N Estyl <n8n@estyl.pl>","type":"personal","icon":null,"description":null,"projectRelations":[{"updatedAt":"2025-01-31T18:13:38.763Z","createdAt":"2025-01-31T18:13:38.763Z","userId":"5089e5a8-9975-48ff-8451-f5c3fb3e78ac","projectId":"jlSr7IBJt4siEqiM","user":{"updatedAt":"2025-11-19T23:00:50.363Z","createdAt":"2025-01-31T18:13:37.839Z","id":"5089e5a8-9975-48ff-8451-f5c3fb3e78ac","email":"n8n@estyl.pl","firstName":"N8N","lastName":"Estyl","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2025-01-31T18:30:49.691Z","personalization_survey_n8n_version":"1.76.1"},"settings":{"userActivated":true,"firstSuccessfulWorkflowId":"xbn9AqbzxO9Xbruw","userActivatedAt":1739378108329,"easyAIWorkflowOnboarded":true,"npsSurvey":{"responded":true,"lastShownAt":1758812569793}},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2025-11-19","isPending":false}}]}}],"tags":[]}

operations:
  [{"type":"addNode","node":{"id":"init-pagination-code","name":"Initialize Pagination State","type":"n8n-nodes-base.code","typeVersion":2,"position":[112,-400],"parameters":{"mode":"runOnceForAllItems","jsCode":"// Initialize pagination state for July 2020 export\nconst startDate = new Date('2020-07-01T00:00:00Z');\nconst cursor_date = Math.floor(startDate.getTime() / 1000);\n\nreturn [{\n  json: {\n    id: 1,\n    batch_number: 0,\n    total_orders: 0,\n    status: 'ready',\n    last_updated: new Date().toISOString(),\n    cursor_date: cursor_date\n  }\n}];"}}},{"type":"addNode","node":{"id":"upsert-init-state","name":"Upsert Initial State","type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[320,-400],"parameters":{"resource":"row","operation":"upsert","dataTableId":{"mode":"id","value":"pagination_state_july2020"},"filters":{"conditions":[{"keyName":"id","condition":"eq","keyValue":"1"}]},"columns":{"mappingMode":"defineBelow","value":{"id":"={{ $json.id }}","batch_number":"={{ $json.batch_number }}","total_orders":"={{ $json.total_orders }}","status":"={{ $json.status }}","last_updated":"={{ $json.last_updated }}","cursor_date":"={{ $json.cursor_date }}"}}}}},{"type":"addNode","node":{"id":"read-pagination-state","name":"Read Pagination State","type":"n8n-nodes-base.dataTable","typeVersion":1,"position":[528,-400],"parameters":{"resource":"row","operation":"get","dataTableId":{"mode":"id","value":"pagination_state_july2020"},"returnAll":false,"limit":1,"filters":{"conditions":[{"keyName":"id","condition":"eq","keyValue":"1"}]}}}},{"type":"removeConnection","source":"27acb096-8bae-4f19-9237-5139dd7dc10f","target":"2e55bcec-9628-4f92-802d-dc31f9d01247"},{"type":"addConnection","source":"27acb096-8bae-4f19-9237-5139dd7dc10f","target":"init-pagination-code"},{"type":"addConnection","source":"init-pagination-code","target":"upsert-init-state"},{"type":"addConnection","source":"upsert-init-state","target":"read-pagination-state"},{"type":"addConnection","source":"read-pagination-state","target":"2e55bcec-9628-4f92-802d-dc31f9d01247"}]


workflow_id: n6B9ybv2HxTJ44Vl
workflow_name: Community Node Demo - BrightData Web Scraper
trigger: partial_update
created_at: 2026-01-07 19:40:16
workflow_snapshot:
  {"updatedAt":"2026-01-07T19:35:04.253Z","createdAt":"2026-01-07T19:35:04.253Z","id":"n6B9ybv2HxTJ44Vl","name":"Community Node Demo - BrightData Web Scraper","description":null,"active":false,"isArchived":false,"nodes":[{"id":"trigger","name":"When clicking 'Test workflow'","type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[240,300],"parameters":{}},{"id":"brightdata","name":"BrightData Scraper","type":"@brightdata/n8n-nodes-preview-brightdata.brightData","typeVersion":1,"position":[460,300],"parameters":{"resource":"webUnlocker","operation":"request"}},{"id":"output","name":"Output Results","type":"n8n-nodes-base.set","typeVersion":3.4,"position":[680,300],"parameters":{"mode":"manual","duplicateItem":false,"assignments":{"assignments":[{"id":"scraped_content","name":"scrapedContent","value":"={{ $json }}","type":"object"},{"id":"scraped_at","name":"scrapedAt","value":"={{ $now.toISO() }}","type":"string"}]}}}],"connections":{"When clicking 'Test workflow'":{"main":[[{"node":"BrightData Scraper","type":"main","index":0}]]},"BrightData Scraper":{"main":[[{"node":"Output Results","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","saveDataErrorExecution":"all","saveDataSuccessExecution":"all","saveManualExecutions":true,"saveExecutionProgress":true,"callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":null,"meta":null,"pinData":null,"versionId":"229d79f6-2921-4818-9dc4-31b434625899","activeVersionId":null,"versionCounter":1,"triggerCount":0,"shared":[{"updatedAt":"2026-01-07T19:35:04.257Z","createdAt":"2026-01-07T19:35:04.257Z","role":"workflow:owner","workflowId":"n6B9ybv2HxTJ44Vl","projectId":"K0HtvGddtPI24Iyt","project":{"updatedAt":"2025-10-03T09:49:33.347Z","createdAt":"2025-10-03T09:48:05.062Z","id":"K0HtvGddtPI24Iyt","name":"Romuald Członkowski <romuald@n8n-mcp.com>","type":"personal","icon":null,"description":null,"creatorId":"ebb1bcfe-d103-4506-a994-9b18ef25e289","projectRelations":[{"updatedAt":"2025-10-03T09:48:05.063Z","createdAt":"2025-10-03T09:48:05.063Z","userId":"ebb1bcfe-d103-4506-a994-9b18ef25e289","projectId":"K0HtvGddtPI24Iyt","user":{"updatedAt":"2026-01-07T10:12:12.000Z","createdAt":"2025-10-03T09:48:04.462Z","id":"ebb1bcfe-d103-4506-a994-9b18ef25e289","email":"romuald@n8n-mcp.com","firstName":"Romuald","lastName":"Członkowski","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2025-10-03T09:49:36.595Z","personalization_survey_n8n_version":"1.113.3"},"settings":{"userActivated":true,"firstSuccessfulWorkflowId":"DWyvSVPDFSSo9jwN","userActivatedAt":1759494619257,"easyAIWorkflowOnboarded":true,"npsSurvey":{"responded":true,"lastShownAt":1765495306831}},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2026-01-06","isPending":false}}]}}],"tags":[],"activeVersion":null}

operations:
  [{"type":"updateNode","nodeName":"BrightData Scraper","updates":{"type":"@brightdata/n8n-nodes-brightdata.brightData"}}]


workflow_id: eVuLZbcmEbg0PFq5
workflow_name: Test_U1-01
trigger: partial_update
created_at: 2026-02-16 04:03:56
workflow_snapshot:
  {"updatedAt":"2026-02-16T04:03:47.662Z","createdAt":"2026-02-16T04:03:47.662Z","id":"eVuLZbcmEbg0PFq5","name":"Test_U1-01","description":null,"active":false,"isArchived":false,"nodes":[{"id":"cal-trigger","name":"Cal.com Booking","type":"n8n-nodes-base.calTrigger","typeVersion":1,"parameters":{"events":["BOOKING_CREATED"]},"position":[0,300]},{"id":"find-candidate","name":"Find Candidate","type":"n8n-nodes-base.postgres","typeVersion":2.5,"parameters":{"query":"SELECT * FROM [REDACTED] WHERE phone = '{{ $json.payload.responses.phone.value }}' OR email = '{{ $json.payload.responses.email.value }}' LIMIT 1","options":{},"operation":"executeQuery"},"position":[250,300]},{"id":"has-candidate","name":"Candidate Found?","type":"n8n-nodes-base.if","typeVersion":2.2,"parameters":{"options":{},"conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"check-id","operator":{"type":"string","operation":"exists"},"leftValue":"={{ $json.id }}"}]}},"position":[500,300]},{"id":"update-stage","name":"Update to Stage 4","type":"n8n-nodes-base.postgres","typeVersion":2.5,"parameters":{"query":"UPDATE [REDACTED] SET current_stage = 4, interview_time = '{{ $('Cal.com Booking').item.json.payload.startTime }}', updated_at = NOW() WHERE id = '{{ $json.id }}'","options":{},"operation":"executeQuery"},"position":[750,200]},{"id":"send-confirm","name":"Send Confirmation SMS","type":"n8n-nodes-base.httpRequest","typeVersion":4,"parameters":{"url":"https://[domain]/conversations/messages","method":"POST","options":{},"jsonBody":"={\n  \"type\": \"SMS\",\n  \"contactId\": \"{{ $('Find Candidate').item.json.ghl_contact_id }}\",\n  \"message\": \"Awesome! Your interview is confirmed for {{ $('Cal.com Booking').item.json.payload.startTime }}. We'll send a reminder the day before. See you then!\"\n}","sendBody":true,"sendHeaders":true,"specifyBody":"json","authentication":"[REDACTED]","headerParameters":{"parameters":[{"name":"Version","value":"2021-07-28"}]},"nodeCredentialType":"[REDACTED]"},"position":[1000,200]},{"id":"log-booking","name":"Log Booking","type":"n8n-nodes-base.postgres","typeVersion":2.5,"parameters":{"query":"INSERT INTO [REDACTED] (candidate_id, direction, message, stage_at_time) VALUES ('{{ $('Find Candidate').item.json.id }}', 'system', 'Interview booked via Cal.com for {{ $('Cal.com Booking').item.json.payload.startTime }}', 4)","options":{},"operation":"executeQuery"},"position":[1250,200]}],"connections":{"Find Candidate":{"main":[[{"node":"Candidate Found?","type":"main","index":0}]]},"Cal.com Booking":{"main":[[{"node":"Find Candidate","type":"main","index":0}]]},"Candidate Found?":{"main":[[{"node":"Update to Stage 4","type":"main","index":0}],[]]},"Update to Stage 4":{"main":[[{"node":"Send Confirmation SMS","type":"main","index":0}]]},"Send Confirmation SMS":{"main":[[{"node":"Log Booking","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","saveDataErrorExecution":"all","saveDataSuccessExecution":"all","saveManualExecutions":true,"saveExecutionProgress":true,"callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":null,"meta":null,"pinData":null,"versionId":"d15bd3a0-5c20-43c2-9233-81ac3071108c","activeVersionId":null,"versionCounter":1,"triggerCount":0,"shared":[{"updatedAt":"2026-02-16T04:03:47.662Z","createdAt":"2026-02-16T04:03:47.662Z","role":"workflow:owner","workflowId":"eVuLZbcmEbg0PFq5","projectId":"IEtLGz5ifmZLQuqt","project":{"updatedAt":"2026-01-27T07:13:02.277Z","createdAt":"2026-01-27T07:09:56.520Z","id":"IEtLGz5ifmZLQuqt","name":"a a <czlonkowski@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"b6749013-2e56-48f1-a40e-1f6178895b17","projectRelations":[{"updatedAt":"2026-01-27T07:09:56.520Z","createdAt":"2026-01-27T07:09:56.520Z","userId":"b6749013-2e56-48f1-a40e-1f6178895b17","projectId":"IEtLGz5ifmZLQuqt","user":{"updatedAt":"2026-02-16T03:05:32.000Z","createdAt":"2026-01-27T07:09:56.466Z","id":"b6749013-2e56-48f1-a40e-1f6178895b17","email":"czlonkowski@gmail.com","firstName":"a","lastName":"a","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2026-01-27T07:13:08.058Z","personalization_survey_n8n_version":"2.4.6"},"settings":{"userActivated":false},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2026-02-16","isPending":false}}]}}],"tags":[],"activeVersion":null}

operations:
  [{"type":"updateNode","nodeName":"Find Candidate","updates":{"parameters":{"query":"SELECT * FROM events_v2 WHERE phone = '{{ $json.payload.responses.phone.value }}' OR email = '{{ $json.payload.responses.email.value }}' LIMIT 1"}}}]


workflow_id: s8e9xLeviAVdGbpZ
workflow_name: Test_U1-03
trigger: partial_update
created_at: 2026-02-16 04:04:00
workflow_snapshot:
  {"updatedAt":"2026-02-16T04:03:54.625Z","createdAt":"2026-02-16T04:03:54.625Z","id":"s8e9xLeviAVdGbpZ","name":"Test_U1-03","description":null,"active":false,"isArchived":false,"nodes":[{"id":"manual-trigger","name":"Inicio Manual","type":"n8n-nodes-base.manualTrigger","typeVersion":1,"parameters":{},"position":[240,300]},{"id":"set-user-idea","name":"Entrada de Texto","type":"n8n-nodes-base.set","typeVersion":3.4,"parameters":{"options":{},"assignments":{"assignments":[{"id":"user_idea_field","name":"user_idea","type":"string","value":"Un astronauta caminando en Marte al atardecer"}]}},"position":[460,300]},{"id":"http-openai-enhance","name":"Mejorar Prompt con IA","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"parameters":{"url":"https://[domain]/v1/chat/completions","method":"POST","options":{},"jsonBody":"={\n  \"model\": \"gpt-4o-mini\",\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": \"Eres un experto en crear prompts detallados para generadores de video IA. Toma la idea del usuario y conviértela en un prompt descriptivo en inglés, agregando detalles de iluminación, estilo cinematográfico, movimiento de cámara y atmósfera. Responde SOLO con el prompt mejorado, sin explicaciones adicionales.\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": \"{{ $json.user_idea }}\"\n    }\n  ],\n  \"temperature\": 0.7,\n  \"max_tokens\": 200\n}","sendBody":true,"specifyBody":"json","authentication":"[REDACTED]","nodeCredentialType":"[REDACTED]"},"position":[680,300]},{"id":"extract-prompt","name":"Extraer Prompt Generado","type":"n8n-nodes-base.set","typeVersion":3.4,"parameters":{"options":{},"assignments":{"assignments":[{"id":"enhanced_prompt","name":"enhanced_prompt","type":"string","value":"={{ $json.choices[0].message.content }}"}]}},"position":[900,300]},{"id":"http-nanobanana","name":"Generar Video en Nanobanana","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"parameters":{"url":"https://[domain]/generate","method":"POST","options":{"timeout":120000},"jsonBody":"={\n  \"prompt\": \"{{ $json.enhanced_prompt }}\",\n  \"duration\": 5,\n  \"resolution\": \"1280x720\",\n  \"style\": \"cinematic\"\n}","sendBody":true,"specifyBody":"json","authentication":"[REDACTED]","genericAuthType":"[REDACTED]"},"position":[1120,300]},{"id":"resultado-final","name":"Resultado Final","type":"n8n-nodes-base.noOp","typeVersion":1,"parameters":{},"position":[1340,300]}],"connections":{"Inicio Manual":{"main":[[{"node":"Entrada de Texto","type":"main","index":0}]]},"Entrada de Texto":{"main":[[{"node":"Mejorar Prompt con IA","type":"main","index":0}]]},"Mejorar Prompt con IA":{"main":[[{"node":"Extraer Prompt Generado","type":"main","index":0}]]},"Extraer Prompt Generado":{"main":[[{"node":"Generar Video en Nanobanana","type":"main","index":0}]]},"Generar Video en Nanobanana":{"main":[[{"node":"Resultado Final","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","saveDataErrorExecution":"all","saveDataSuccessExecution":"all","saveManualExecutions":true,"saveExecutionProgress":true,"callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":null,"meta":null,"pinData":null,"versionId":"df2f9f94-0485-4e1d-81d3-3bc3d041251d","activeVersionId":null,"versionCounter":1,"triggerCount":0,"shared":[{"updatedAt":"2026-02-16T04:03:54.626Z","createdAt":"2026-02-16T04:03:54.626Z","role":"workflow:owner","workflowId":"s8e9xLeviAVdGbpZ","projectId":"IEtLGz5ifmZLQuqt","project":{"updatedAt":"2026-01-27T07:13:02.277Z","createdAt":"2026-01-27T07:09:56.520Z","id":"IEtLGz5ifmZLQuqt","name":"a a <czlonkowski@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"b6749013-2e56-48f1-a40e-1f6178895b17","projectRelations":[{"updatedAt":"2026-01-27T07:09:56.520Z","createdAt":"2026-01-27T07:09:56.520Z","userId":"b6749013-2e56-48f1-a40e-1f6178895b17","projectId":"IEtLGz5ifmZLQuqt","user":{"updatedAt":"2026-02-16T03:05:32.000Z","createdAt":"2026-01-27T07:09:56.466Z","id":"b6749013-2e56-48f1-a40e-1f6178895b17","email":"czlonkowski@gmail.com","firstName":"a","lastName":"a","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2026-01-27T07:13:08.058Z","personalization_survey_n8n_version":"2.4.6"},"settings":{"userActivated":false},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2026-02-16","isPending":false}}]}}],"tags":[],"activeVersion":null}

operations:
  [{"type":"updateNode","nodeName":"Mejorar Prompt con IA","updates":{"parameters":{"options":{"headers":{"X-API-Version":"2"}}}}}]


workflow_id: G2nswpEGCKlr6HPc
workflow_name: Test_U1-04
trigger: partial_update
operations: [{"type":"updateNode","nodeName":"Search Hotels","updates":{"parameters":{"method":"POST"}}}]
created_at: 2026-02-16 04:04:01
workflow_snapshot:
  {"updatedAt":"2026-02-16T04:03:57.363Z","createdAt":"2026-02-16T04:03:57.363Z","id":"G2nswpEGCKlr6HPc","name":"Test_U1-04","description":null,"active":false,"isArchived":false,"nodes":[{"id":"schedule-trigger","name":"Schedule Trigger","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1,"parameters":{"rule":{"interval":[{"field":"days","seconds":86400}]}},"position":[0,0]},{"id":"get-recent-flights","name":"Get Recent Flights","type":"n8n-nodes-base.supabase","typeVersion":1,"parameters":{"filters":{"created_at":{"value":"={{$today.minus(1, 'day').format('yyyy-MM-dd')}}","operator":">="}},"tableId":"flight_deals","operation":"getAll","returnAll":true},"position":[200,0]},{"id":"split-in-batches","name":"SplitInBatches","type":"n8n-nodes-base.splitInBatches","typeVersion":1,"parameters":{"options":{},"batchSize":1},"position":[400,0]},{"id":"get-destination-details","name":"Get Destination Details","type":"n8n-nodes-base.supabase","typeVersion":1,"parameters":{"filters":{"iata_code":{"value":"={{$node[\"SplitInBatches\"].json[\"destination\"]}}","operator":"eq"}},"tableId":"destinations","operation":"getAll","returnAll":false},"position":[600,0]},{"id":"search-hotels","name":"Search Hotels","type":"n8n-nodes-base.httpRequest","typeVersion":1,"parameters":{"url":"https://[domain]/search","method":"GET","options":{},"sendQuery":true,"queryParameters":{"parameters":[{"name":"engine","value":"google_hotels"},{"name":"q","value":"={{$node[\"Get Destination Details\"].json[\"name\"]}}"},{"name":"check_in_date","value":"={{$node[\"SplitInBatches\"].json[\"departure_date\"]}}"},{"name":"check_out_date","value":"={{$node[\"SplitInBatches\"].json[\"return_date\"]}}"},{"name":"api_key","value":"YOUR_SERPAPI_KEY"},{"name":"currency","value":"EUR"}]}},"position":[800,0]},{"id":"process-hotels","name":"Process Hotels","type":"n8n-nodes-base.code","typeVersion":1,"parameters":{"jsCode":"const results = [];\nconst destId = $node[\"Get Destination Details\"].json.id;\nconst hotels = $input.all()[0].json.properties || [];\n\nfor (const hotel of hotels) {\n  results.push({\n    destination_id: destId,\n    name: hotel.name,\n    type: 'hotel',\n    price_per_night: hotel.rate_per_night ? hotel.rate_per_night.lowest : 0,\n    rating: hotel.overall_rating,\n    booking_url: hotel.link,\n    image_url: hotel.images ? hotel.images[0].thumbnail : null\n  });\n}\nreturn results;"},"position":[1000,0]},{"id":"save-accommodations","name":"Save Accommodations","type":"n8n-nodes-base.supabase","typeVersion":1,"parameters":{"columns":"destination_id, name, type, price_per_night, rating, booking_url, image_url","tableId":"accommodations","operation":"upsert"},"position":[1200,0]}],"connections":{"Search Hotels":{"main":[[{"node":"Process Hotels","type":"main","index":0}]]},"Process Hotels":{"main":[[{"node":"Save Accommodations","type":"main","index":0}]]},"SplitInBatches":{"main":[[{"node":"Get Destination Details","type":"main","index":0}]]},"Schedule Trigger":{"main":[[{"node":"Get Recent Flights","type":"main","index":0}]]},"Get Recent Flights":{"main":[[{"node":"SplitInBatches","type":"main","index":0}]]},"Save Accommodations":{"main":[[{"node":"SplitInBatches","type":"main","index":0}]]},"Get Destination Details":{"main":[[{"node":"Search Hotels","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","saveDataErrorExecution":"all","saveDataSuccessExecution":"all","saveManualExecutions":true,"saveExecutionProgress":true,"callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":null,"meta":null,"pinData":null,"versionId":"3144c24e-455f-4b37-8402-cb0658d95871","activeVersionId":null,"versionCounter":1,"triggerCount":0,"shared":[{"updatedAt":"2026-02-16T04:03:57.363Z","createdAt":"2026-02-16T04:03:57.363Z","role":"workflow:owner","workflowId":"G2nswpEGCKlr6HPc","projectId":"IEtLGz5ifmZLQuqt","project":{"updatedAt":"2026-01-27T07:13:02.277Z","createdAt":"2026-01-27T07:09:56.520Z","id":"IEtLGz5ifmZLQuqt","name":"a a <czlonkowski@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"b6749013-2e56-48f1-a40e-1f6178895b17","projectRelations":[{"updatedAt":"2026-01-27T07:09:56.520Z","createdAt":"2026-01-27T07:09:56.520Z","userId":"b6749013-2e56-48f1-a40e-1f6178895b17","projectId":"IEtLGz5ifmZLQuqt","user":{"updatedAt":"2026-02-16T03:05:32.000Z","createdAt":"2026-01-27T07:09:56.466Z","id":"b6749013-2e56-48f1-a40e-1f6178895b17","email":"czlonkowski@gmail.com","firstName":"a","lastName":"a","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2026-01-27T07:13:08.058Z","personalization_survey_n8n_version":"2.4.6"},"settings":{"userActivated":false},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2026-02-16","isPending":false}}]}}],"tags":[],"activeVersion":null}


workflow_id: bQ2E4eWdKRMYAAdh
workflow_name: Test_U1-02
trigger: partial_update
created_at: 2026-02-16 04:04:06
workflow_snapshot:
  {"updatedAt":"2026-02-16T04:03:49.978Z","createdAt":"2026-02-16T04:03:49.978Z","id":"bQ2E4eWdKRMYAAdh","name":"Test_U1-02","description":null,"active":false,"isArchived":false,"nodes":[{"id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","name":"Manual Trigger","type":"n8n-nodes-base.manualTrigger","typeVersion":1,"parameters":{},"position":[240,300]},{"id":"b2c3d4e5-f6g7-8901-bcde-f12345678901","name":"Scrape Homepage Brands","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"parameters":{"url":"https://[domain]/en/","options":{},"sendHeaders":true,"headerParameters":{"parameters":[{"name":"User-Agent","value":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}]}},"position":[460,300]},{"id":"c3d4e5f6-g7h8-9012-cdef-123456789012","name":"Extract Brand Links","type":"n8n-nodes-base.code","typeVersion":2,"parameters":{"jsCode":"// OPTION A : Liste temporaire de 3 marques pour test\nconst naBrands = [\n  { name: 'Toyota', id: 'toyota-12', minYear: 1960 },\n  { name: 'Honda', id: 'honda-12', minYear: 1973 },\n  { name: 'Mazda', id: 'mazda-12', minYear: 1971 }\n];\n\nconsole.log(`✅ Returning ${naBrands.length} test brands`);\n\nreturn naBrands.map(brand => ({\n  json: {\n    brandUrl: `https://www.auto-data.net/en/${brand.id}`,\n    brandName: brand.name.toLowerCase(),\n    minYear: brand.minYear\n  }\n}));"},"position":[680,300]},{"id":"d4e5f6g7-h8i9-0123-defg-234567890123","name":"Scrape Brand Page","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"parameters":{"url":"={{ $json.brandUrl }}","options":{},"sendHeaders":true,"headerParameters":{"parameters":[{"name":"User-Agent","value":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}]}},"position":[900,300]},{"id":"e5f6g7h8-i9j0-1234-efgh-345678901234","name":"Extract Models from Brand","type":"n8n-nodes-base.code","typeVersion":2,"parameters":{"jsCode":"// OPTION A : 1 modèle fixe par marque pour test\nconst brandData = $('Extract Brand Links').item.json;\nconst brandName = brandData.brandName;\nconst minYear = brandData.minYear;\n\nconsole.log(\"=== Extract Models (Test Mode) ===\");\nconsole.log(\"Brand:\", brandName);\n\nconst testModels = {\n  'toyota': [\n    { url: '/en/[REDACTED_TOKEN]', name: 'Toyota Camry 2024', year: 2024 }\n  ],\n  'honda': [\n    { url: '/en/[REDACTED_TOKEN]', name: 'Honda Accord 2024', year: 2024 }\n  ],\n  'mazda': [\n    { url: '/en/[REDACTED_TOKEN]', name: 'Mazda3 2024', year: 2024 }\n  ]\n};\n\nconst brandKey = brandName ? brandName.toLowerCase() : '';\nconst models = testModels[brandKey] || [];\n\nconsole.log(`✅ Returning ${models.length} test model(s) for ${brandName}`);\n\nif (models.length === 0) {\n  return [];\n}\n\nreturn models.map(model => ({\n  json: {\n    modelUrl: 'https://www.auto-data.net' + model.url,\n    modelName: model.name,\n    brand: brandName,\n    year: model.year\n  }\n}));"},"position":[1120,300]},{"id":"f6g7h8i9-j0k1-2345-fghi-456789012345","name":"Scrape Model Details","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"parameters":{"url":"={{ $json.modelUrl }}","options":{},"sendHeaders":true,"headerParameters":{"parameters":[{"name":"User-Agent","value":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}]}},"position":[1340,300]},{"id":"g7h8i9j0-k1l2-3456-ghij-567890123456","name":"Extract 33 Technical Specs","type":"n8n-nodes-base.code","typeVersion":2,"parameters":{"jsCode":"// Extraction des 33 specs techniques\nconst scrapedData = $input.first().json;\nconst modelData = $('Extract Models from Brand').item.json;\n\nconsole.log(\"=== Extract 33 Technical Specs ===\");\nconsole.log(\"Model:\", modelData.modelName);\nconsole.log(\"HTML available:\", !!scrapedData.data);\n\nconst html = scrapedData.data || '';\n\nif (!html || html.length < 100) {\n  console.log(\"⚠️ No HTML data received\");\n  return [{\n    json: {\n      model_name: modelData.modelName,\n      version_name: 'N/A',\n      year_range: modelData.year ? modelData.year.toString() : 'N/A',\n      is_multi_year: false,\n      start_of_production: 'N/A',\n      [REDACTED]: 'N/A',\n      body_type: 'N/A',\n      seats: 'N/A',\n      doors: 'N/A',\n      [REDACTED]: 'N/A',\n      [REDACTED]: 'N/A',\n      [REDACTED]: 'N/A',\n      fuel_type: 'N/A',\n      fuel_tank_capacity: 'N/A',\n      acceleration: 'N/A',\n      max_speed: 'N/A',\n      power: 'N/A',\n      torque: 'N/A',\n      max_engine_speed: 'N/A',\n      engine_layout: 'N/A',\n      engine_displacement: 'N/A',\n      cylinders: 'N/A',\n      [REDACTED]: 'N/A',\n      valves_per_cylinder: 'N/A',\n      fuel_injection: 'N/A',\n      engine_aspiration: 'N/A',\n      curb_weight: 'N/A',\n      trunk_space: 'N/A',\n      length: 'N/A',\n      width: 'N/A',\n      height: 'N/A',\n      drive_wheel: 'N/A',\n      transmission: 'N/A',\n      tire_size: 'N/A',\n      data_source_url: modelData.modelUrl,\n      scrape_date: new Date().toISOString().split('T')[0],\n      data_completeness: 0\n    }\n  }];\n}\n\nfunction extractValue(labels) {\n  for (const label of labels) {\n    const regex = new RegExp(`${label}[^<]*?([\\\\d.,]+\\\\s*[a-zA-Z/°]+)`, 'i');\n    const match = html.match(regex);\n    if (match) return match[1].trim();\n  }\n  return 'N/A';\n}\n\nfunction combineUnits(labels) {\n  const values = [];\n  for (const label of labels) {\n    const regex = new RegExp(`${label}[^<]*?([\\\\d.,]+\\\\s*[a-zA-Z/°]+)`, 'gi');\n    let match;\n    while ((match = regex.exec(html)) !== null) {\n      const value = match[1].trim();\n      if (!values.includes(value)) values.push(value);\n    }\n  }\n  return values.length > 0 ? values.join(' / ') : 'N/A';\n}\n\nconst specs = {\n  model_name: modelData.modelName,\n  version_name: extractValue(['Version', 'Trim', 'Model variant']),\n  year_range: modelData.year ? modelData.year.toString() : 'N/A',\n  is_multi_year: false,\n  start_of_production: extractValue(['Start of production', 'Production start']),\n  [REDACTED]: extractValue(['Powertrain', 'Powertrain architecture']),\n  body_type: extractValue(['Body type', 'Body']),\n  seats: extractValue(['Seats', 'Seating capacity']),\n  doors: extractValue(['Doors', 'Number of doors']),\n  [REDACTED]: combineUnits(['Fuel consumption.*urban', 'City fuel economy']),\n  [REDACTED]: combineUnits(['Fuel consumption.*extra', 'Highway fuel economy']),\n  [REDACTED]: combineUnits(['Fuel consumption.*combined', 'Combined fuel economy']),\n  fuel_type: extractValue(['Fuel type', 'Fuel']),\n  fuel_tank_capacity: combineUnits(['Fuel tank capacity', 'Tank capacity']),\n  acceleration: combineUnits(['Acceleration 0.*100', 'Acceleration', '0-100']),\n  max_speed: combineUnits(['Maximum speed', 'Max speed', 'Top speed']),\n  power: combineUnits(['Power', 'Maximum power', 'Horsepower', 'HP']),\n  torque: combineUnits(['Torque', 'Maximum torque']),\n  max_engine_speed: combineUnits(['Maximum engine speed', 'Max RPM']),\n  engine_layout: extractValue(['Engine layout', 'Engine position']),\n  engine_displacement: combineUnits(['Engine displacement', 'Displacement']),\n  cylinders: extractValue(['Number of cylinders', 'Cylinders']),\n  [REDACTED]: extractValue(['Engine configuration', 'Configuration']),\n  valves_per_cylinder: extractValue(['Valves per cylinder', 'Valves']),\n  fuel_injection: extractValue(['Fuel injection', 'Injection system']),\n  engine_aspiration: extractValue(['Engine aspiration', 'Aspiration']),\n  curb_weight: combineUnits(['Curb weight', 'Weight', 'Kerb weight']),\n  trunk_space: combineUnits(['Trunk.*space', 'Boot space', 'Cargo']),\n  length: combineUnits(['Length']),\n  width: combineUnits(['Width']),\n  height: combineUnits(['Height']),\n  drive_wheel: extractValue(['Drive wheel', 'Drivetrain', 'Drive type']),\n  transmission: extractValue(['Transmission', 'Gearbox']),\n  tire_size: extractValue(['Tire size', 'Tyre size']),\n  data_source_url: modelData.modelUrl,\n  scrape_date: new Date().toISOString().split('T')[0]\n};\n\nconst totalFields = 33;\nconst filledFields = Object.values(specs).filter(v => v && v !== 'N/A' && v !== '').length;\nspecs.data_completeness = Math.round((filledFields / totalFields) * 100);\n\nconsole.log(`✅ Extracted ${filledFields}/${totalFields} specs (${specs.data_completeness}% complete)`);\n\nreturn [{ json: specs }];"},"position":[1560,300]}],"connections":{"Manual Trigger":{"main":[[{"node":"Scrape Homepage Brands","type":"main","index":0}]]},"Scrape Brand Page":{"main":[[{"node":"Extract Models from Brand","type":"main","index":0}]]},"Extract Brand Links":{"main":[[{"node":"Scrape Brand Page","type":"main","index":0}]]},"Scrape Model Details":{"main":[[{"node":"Extract 33 Technical Specs","type":"main","index":0}]]},"Scrape Homepage Brands":{"main":[[{"node":"Extract Brand Links","type":"main","index":0}]]},"Extract Models from Brand":{"main":[[{"node":"Scrape Model Details","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","saveDataErrorExecution":"all","saveDataSuccessExecution":"all","saveManualExecutions":true,"saveExecutionProgress":true,"callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":null,"meta":null,"pinData":null,"versionId":"b058182d-bcdc-46e5-9ca0-644235cd2d31","activeVersionId":null,"versionCounter":1,"triggerCount":0,"shared":[{"updatedAt":"2026-02-16T04:03:49.978Z","createdAt":"2026-02-16T04:03:49.978Z","role":"workflow:owner","workflowId":"bQ2E4eWdKRMYAAdh","projectId":"IEtLGz5ifmZLQuqt","project":{"updatedAt":"2026-01-27T07:13:02.277Z","createdAt":"2026-01-27T07:09:56.520Z","id":"IEtLGz5ifmZLQuqt","name":"a a <czlonkowski@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"b6749013-2e56-48f1-a40e-1f6178895b17","projectRelations":[{"updatedAt":"2026-01-27T07:09:56.520Z","createdAt":"2026-01-27T07:09:56.520Z","userId":"b6749013-2e56-48f1-a40e-1f6178895b17","projectId":"IEtLGz5ifmZLQuqt","user":{"updatedAt":"2026-02-16T03:05:32.000Z","createdAt":"2026-01-27T07:09:56.466Z","id":"b6749013-2e56-48f1-a40e-1f6178895b17","email":"czlonkowski@gmail.com","firstName":"a","lastName":"a","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2026-01-27T07:13:08.058Z","personalization_survey_n8n_version":"2.4.6"},"settings":{"userActivated":false},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2026-02-16","isPending":false}}]}}],"tags":[],"activeVersion":null}

operations:
  [{"type":"updateNode","nodeName":"Scrape Homepage Brands","updates":{"parameters":{"method":"POST"}}}]


workflow_id: bIjSz2B9d5fKc0SQ
workflow_name: Test_U1-07
trigger: partial_update
created_at: 2026-02-16 04:04:16
workflow_snapshot:
  {"updatedAt":"2026-02-16T04:04:11.369Z","createdAt":"2026-02-16T04:04:11.369Z","id":"bIjSz2B9d5fKc0SQ","name":"Test_U1-07","description":null,"active":false,"isArchived":false,"nodes":[{"id":"slack-webhook-1","name":"Slack Webhook","type":"n8n-nodes-base.webhook","typeVersion":2,"parameters":{"path":"slack-pdf-upload","options":{},"httpMethod":"POST","responseMode":"lastNode"},"position":[250,300]},{"id":"read-pdf-1","name":"Read PDF","type":"n8n-nodes-base.readPDF","typeVersion":1,"parameters":{"encrypted":false,"binaryPropertyName":"data"},"position":[450,300]},{"id":"extract-info-1","name":"Information Extractor","type":"@n8n/n8n-nodes-langchain.informationExtractor","typeVersion":1.2,"parameters":{"text":"={{ $json.text }}","schemaType":"fromJson","jsonSchemaExample":"{\n  \"工事名\": \"〇〇マンション改修工事\",\n  \"工事項目\": [\n    {\n      \"工事内容\": \"壁紙張替\",\n      \"数量\": 50,\n      \"単位\": \"㎡\",\n      \"単価\": 1500,\n      \"備考\": \"リビング・寝室\"\n    }\n  ]\n}"},"position":[650,300]},{"id":"split-items-1","name":"Split Out Items","type":"n8n-nodes-base.code","typeVersion":2,"parameters":{"jsCode":"// 抽出されたデータから工事項目を分割\nconst items = $input.all();\nconst result = [];\n\nfor (const item of items) {\n  const 工事名 = item.json.工事名 || '';\n  const 工事項目 = item.json.工事項目 || [];\n  \n  // 各工事項目を個別のアイテムとして出力\n  for (const 項目 of 工事項目) {\n    result.push({\n      json: {\n        工事名: 工事名,\n        摘要: 項目.工事内容 || '',\n        数量: parseFloat(項目.数量) || 0,\n        単位: 項目.単位 || '',\n        単価: parseFloat(項目.単価) || 0,\n        備考: 項目.備考 || ''\n      }\n    });\n  }\n}\n\nreturn result;"},"position":[850,300]},{"id":"create-notion-1","name":"Create Notion Page","type":"n8n-nodes-base.notion","typeVersion":2.2,"parameters":{"title":"={{ $json.摘要 }}","resource":"databasePage","operation":"create","databaseId":"[REDACTED]","propertiesUi":{"propertyValues":[{"key":"数量","numberValue":"={{ $json.数量 }}"},{"key":"単位","richTextValue":"={{ $json.単位 }}"},{"key":"単価","numberValue":"={{ $json.単価 }}"},{"key":"備考","richTextValue":"={{ $json.備考 }}"}]}},"position":[1050,300]}],"connections":{"Read PDF":{"main":[[{"node":"Information Extractor","type":"main","index":0}]]},"Slack Webhook":{"main":[[{"node":"Read PDF","type":"main","index":0}]]},"Split Out Items":{"main":[[{"node":"Create Notion Page","type":"main","index":0}]]},"Information Extractor":{"main":[[{"node":"Split Out Items","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","saveDataErrorExecution":"all","saveDataSuccessExecution":"all","saveManualExecutions":true,"saveExecutionProgress":true,"callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":null,"meta":null,"pinData":null,"versionId":"501b951d-fc7e-4640-80bf-6d4a667f28bc","activeVersionId":null,"versionCounter":1,"triggerCount":0,"shared":[{"updatedAt":"2026-02-16T04:04:11.370Z","createdAt":"2026-02-16T04:04:11.370Z","role":"workflow:owner","workflowId":"bIjSz2B9d5fKc0SQ","projectId":"IEtLGz5ifmZLQuqt","project":{"updatedAt":"2026-01-27T07:13:02.277Z","createdAt":"2026-01-27T07:09:56.520Z","id":"IEtLGz5ifmZLQuqt","name":"a a <czlonkowski@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"b6749013-2e56-48f1-a40e-1f6178895b17","projectRelations":[{"updatedAt":"2026-01-27T07:09:56.520Z","createdAt":"2026-01-27T07:09:56.520Z","userId":"b6749013-2e56-48f1-a40e-1f6178895b17","projectId":"IEtLGz5ifmZLQuqt","user":{"updatedAt":"2026-02-16T03:05:32.000Z","createdAt":"2026-01-27T07:09:56.466Z","id":"b6749013-2e56-48f1-a40e-1f6178895b17","email":"czlonkowski@gmail.com","firstName":"a","lastName":"a","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2026-01-27T07:13:08.058Z","personalization_survey_n8n_version":"2.4.6"},"settings":{"userActivated":false},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2026-02-16","isPending":false}}]}}],"tags":[],"activeVersion":null}

operations:
  [{"type":"updateNode","nodeName":"Slack Webhook","updates":{"parameters":{"path":"/api/v2/webhooks"}}}]


workflow_id: VsHquiYkAf1IMNhM
workflow_name: Test_U1-05
trigger: partial_update
created_at: 2026-02-16 04:04:20
workflow_snapshot:
  {"updatedAt":"2026-02-16T04:04:07.615Z","createdAt":"2026-02-16T04:04:07.615Z","id":"VsHquiYkAf1IMNhM","name":"Test_U1-05","description":null,"active":false,"isArchived":false,"nodes":[{"id":"trigger-start","name":"On /start Command","type":"n8n-nodes-base.telegramTrigger","typeVersion":1.2,"parameters":{"updates":["message"],"additionalFields":{}},"position":[0,0]},{"id":"filter-start-cmd","name":"Filter /start","type":"n8n-nodes-base.filter","typeVersion":2.1,"parameters":{"conditions":{"options":{"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"cmd-check","operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.message.text }}","rightValue":"/start"}]}},"position":[200,0]},{"id":"read-all-roles","name":"Read All Users","type":"n8n-nodes-base.googleSheets","typeVersion":4.7,"parameters":{"options":{},"operation":"read","sheetName":{"__rl":true,"mode":"id","value":"gid=1256748375"},"documentId":{"__rl":true,"mode":"id","value":"[REDACTED]"}},"position":[400,0]},{"id":"check-user-logic","name":"Check User Logic","type":"n8n-nodes-base.code","typeVersion":2,"parameters":{"jsCode":"const telegramData = $input.first().json.message;\nconst existingUsers = $('Read All Users').all();\n\nconst currentUsername = telegramData.from.username || '';\nconst currentChatId = telegramData.chat.id.toString();\nconst firstName = telegramData.from.first_name || '';\nconst lastName = telegramData.from.last_name || '';\n\n// Ищем пользователя по username\nconst existingUser = existingUsers.find(user => \n  user.json.tg_username === currentUsername\n);\n\nif (existingUser) {\n  // Пользователь существует\n  const needsUpdate = existingUser.json.Chat_id !== currentChatId;\n  \n  return [{\n    json: {\n      action: needsUpdate ? 'update' : 'exists',\n      username: currentUsername,\n      chatId: currentChatId,\n      firstName: firstName,\n      lastName: lastName,\n      existingRow: existingUser.json,\n      rowNumber: existingUsers.indexOf(existingUser) + 2 // +2 для учета header и индексации с 1\n    }\n  }];\n} else {\n  // Новый пользователь\n  return [{\n    json: {\n      action: 'add',\n      username: currentUsername,\n      chatId: currentChatId,\n      firstName: firstName,\n      lastName: lastName,\n      role: 'User',\n      lastUpdate: new Date().toISOString()\n    }\n  }];\n}"},"position":[600,0]},{"id":"route-action","name":"Route by Action","type":"n8n-nodes-base.switch","typeVersion":3.2,"parameters":{"mode":"expression","rules":{"values":[{"id":"route-add","outputKey":"add","conditions":{"options":{"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.action }}","rightValue":"add"}]}},{"id":"route-update","outputKey":"update","conditions":{"options":{"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.action }}","rightValue":"update"}]}},{"id":"route-exists","outputKey":"exists","conditions":{"options":{"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.action }}","rightValue":"exists"}]}}]},"options":{}},"position":[800,0]},{"id":"add-new-user","name":"Add New User","type":"n8n-nodes-base.googleSheets","typeVersion":4.7,"parameters":{"columns":{"value":{"Text":"","role":"={{ $json.role }}","Chat_id":"={{ $json.chatId }}","Last_update":"={{ $json.lastUpdate }}","tg_username":"={{ $json.username }}"},"schema":[],"mappingMode":"defineBelow","matchingColumns":[]},"options":{},"operation":"append","sheetName":{"__rl":true,"mode":"id","value":"gid=1256748375"},"documentId":{"__rl":true,"mode":"id","value":"[REDACTED]"}},"position":[1000,-100]},{"id":"update-chat-id","name":"Update Chat ID","type":"n8n-nodes-base.googleSheets","typeVersion":4.7,"parameters":{"columns":{"value":{"Chat_id":"={{ $json.chatId }}","Last_update":"={{ $now }}"},"schema":[],"mappingMode":"defineBelow","matchingColumns":["tg_username"]},"options":{},"operation":"update","sheetName":{"__rl":true,"mode":"id","value":"gid=1256748375"},"documentId":{"__rl":true,"mode":"id","value":"[REDACTED]"}},"position":[1000,0]},{"id":"send-welcome-new","name":"Welcome New User","type":"n8n-nodes-base.telegram","typeVersion":1.2,"parameters":{"text":"Добро пожаловать! 👋\\n\\nВы успешно зарегистрированы в системе VORSH Echo.\\nВаша роль: User\\n\\nТеперь вы будете получать уведомления.","chatId":"={{ $json.chatId }}","additionalFields":{}},"position":[1200,-100]},{"id":"send-welcome-updated","name":"Welcome Updated","type":"n8n-nodes-base.telegram","typeVersion":1.2,"parameters":{"text":"С возвращением! 👋\\n\\nВаш Chat ID обновлен.\\nРоль: {{ $json.existingRow.role }}\\n\\nВсе готово для получения уведомлений!","chatId":"={{ $json.chatId }}","additionalFields":{}},"position":[1200,0]},{"id":"send-welcome-exists","name":"Welcome Existing","type":"n8n-nodes-base.telegram","typeVersion":1.2,"parameters":{"text":"Привет! 👋\\n\\nВы уже зарегистрированы.\\nРоль: {{ $json.existingRow.role }}\\n\\nПродолжайте получать уведомления!","chatId":"={{ $json.chatId }}","additionalFields":{}},"position":[1200,100]}],"connections":{"Add New User":{"main":[[{"node":"Welcome New User","type":"main","index":0}]]},"Filter /start":{"main":[[{"node":"Read All Users","type":"main","index":0}]]},"Read All Users":{"main":[[{"node":"Check User Logic","type":"main","index":0}]]},"Update Chat ID":{"main":[[{"node":"Welcome Updated","type":"main","index":0}]]},"Route by Action":{"main":[[{"node":"Add New User","type":"main","index":0}],[{"node":"Update Chat ID","type":"main","index":0}],[{"node":"Welcome Existing","type":"main","index":0}]]},"Check User Logic":{"main":[[{"node":"Route by Action","type":"main","index":0}]]},"On /start Command":{"main":[[{"node":"Filter /start","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","saveDataErrorExecution":"all","saveDataSuccessExecution":"all","saveManualExecutions":true,"saveExecutionProgress":true,"callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":null,"meta":null,"pinData":null,"versionId":"7c57ef25-6256-4b9f-8430-65607865282f","activeVersionId":null,"versionCounter":1,"triggerCount":0,"shared":[{"updatedAt":"2026-02-16T04:04:07.615Z","createdAt":"2026-02-16T04:04:07.615Z","role":"workflow:owner","workflowId":"VsHquiYkAf1IMNhM","projectId":"IEtLGz5ifmZLQuqt","project":{"updatedAt":"2026-01-27T07:13:02.277Z","createdAt":"2026-01-27T07:09:56.520Z","id":"IEtLGz5ifmZLQuqt","name":"a a <czlonkowski@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"b6749013-2e56-48f1-a40e-1f6178895b17","projectRelations":[{"updatedAt":"2026-01-27T07:09:56.520Z","createdAt":"2026-01-27T07:09:56.520Z","userId":"b6749013-2e56-48f1-a40e-1f6178895b17","projectId":"IEtLGz5ifmZLQuqt","user":{"updatedAt":"2026-02-16T03:05:32.000Z","createdAt":"2026-01-27T07:09:56.466Z","id":"b6749013-2e56-48f1-a40e-1f6178895b17","email":"czlonkowski@gmail.com","firstName":"a","lastName":"a","personalizationAnswers":{"version":"v4","personalization_survey_submitted_at":"2026-01-27T07:13:08.058Z","personalization_survey_n8n_version":"2.4.6"},"settings":{"userActivated":false},"disabled":false,"mfaEnabled":false,"lastActiveAt":"2026-02-16","isPending":false}}]}}],"tags":[],"activeVersion":null}

operations:
  [{"type":"updateNode","nodeName":"Filter /start","updates":{"parameters":{"conditions":{"options":{"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"cmd-check","operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.message.text }}","rightValue":"/start"},{"id":"status-check","operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.status }}","rightValue":"active"}]}}}}]


