# workflow_versions

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"}]


