commit e14003d64b4dee774f28a76bee1dbe2a31d8d0e3 Author: adminjeroen Date: Thu Jul 2 21:55:13 2026 +0200 Upload files to "palletways" Toevoegen van Collections aan de Palletways database diff --git a/palletways/Palletways BI Collections.json b/palletways/Palletways BI Collections.json new file mode 100644 index 0000000..58a0f4b --- /dev/null +++ b/palletways/Palletways BI Collections.json @@ -0,0 +1,420 @@ +{ + "name": "Palletways BI Collections", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "minutes" + } + ] + } + }, + "id": "595a3d6f-600d-4570-9969-d091b3edf125", + "name": "Schedule Trigger", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.1, + "position": [ + -7728, + -736 + ] + }, + { + "parameters": { + "options": { + "explicitArray": false, + "mergeAttrs": true + } + }, + "id": "f981fd53-c214-4d4b-9890-cc0ebe68eb4f", + "name": "Parse XML Response", + "type": "n8n-nodes-base.xml", + "typeVersion": 1, + "position": [ + -6832, + -640 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "e1fc6f17-b059-4b78-a365-c2428d3874f6", + "name": "Process Each Consignment", + "type": "n8n-nodes-base.splitInBatches", + "typeVersion": 3, + "position": [ + -6160, + -656 + ] + }, + { + "parameters": { + "url": "=https://api.palletways.com/getconsignment/{{ $json.trackingid }}?apikey=SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus%3D", + "options": { + "response": { + "response": { + "responseFormat": "text" + } + }, + "timeout": 10000 + } + }, + "id": "74c9bff6-64f0-4f7b-bda8-09c2d180bf8e", + "name": "Get Consignment Details", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [ + -5920, + -640 + ], + "continueOnFail": true + }, + { + "parameters": { + "jsCode": "// Fixed timestamp parser - handles Redis data under \"propertyName\"\n\n// Helper function to format timestamps in UK timezone\nfunction formatUKTimestamp(date) {\n const options = {\n timeZone: 'Europe/London',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false\n };\n \n const formatter = new Intl.DateTimeFormat('en-CA', options);\n const parts = formatter.formatToParts(date);\n \n const year = parts.find(p => p.type === 'year').value;\n const month = parts.find(p => p.type === 'month').value;\n const day = parts.find(p => p.type === 'day').value;\n const hour = parts.find(p => p.type === 'hour').value;\n const minute = parts.find(p => p.type === 'minute').value;\n const second = parts.find(p => p.type === 'second').value;\n \n return {\n timestamp: `${year}-${month}-${day}T${hour}:${minute}:${second}`,\n date: `${year}-${month}-${day}`,\n time: `${hour}:${minute}:${second}`\n };\n}\n\n// Generate current UK timestamp (this will be saved for the NEXT run)\nconst now = new Date();\nconst currentUK = formatUKTimestamp(now);\n\n// Initialize variables for the PREVIOUS run timestamp\nlet lastRunTimestamp, lastRunDate, lastRunTime;\nlet source = 'unknown';\n\ntry {\n // Get the Redis response\n const input = $input.first()?.json;\n console.log('Full input received:', JSON.stringify(input, null, 2));\n \n // The Redis data is coming in under \"propertyName\" as a JSON string\n let parsedData = null;\n \n if (input?.propertyName) {\n // Parse the JSON string from propertyName\n parsedData = JSON.parse(input.propertyName);\n source = 'input.propertyName';\n console.log('Found and parsed Redis data from propertyName');\n } else if (input?.data) {\n // Fallback: check data property\n if (typeof input.data === 'string') {\n parsedData = JSON.parse(input.data);\n source = 'input.data-parsed';\n } else {\n parsedData = input.data;\n source = 'input.data-object';\n }\n console.log('Found Redis data in input.data');\n } else if (input?.lastRunTimestamp) {\n // Direct object\n parsedData = input;\n source = 'direct-input';\n console.log('Found Redis data directly in input');\n }\n \n console.log('Parsed Redis data:', JSON.stringify(parsedData, null, 2));\n \n if (!parsedData) {\n throw new Error('No Redis data found');\n }\n \n // Extract the previous timestamp - use exactly as stored in Redis\n if (parsedData.lastRunTimestamp) {\n lastRunTimestamp = parsedData.lastRunTimestamp; // Use exact value from Redis\n lastRunDate = parsedData.lastRunDate; // Use exact value from Redis\n lastRunTime = parsedData.lastRunTime; // Use exact value from Redis\n \n console.log(`āœ… Successfully loaded PREVIOUS timestamp from Redis:`);\n console.log(` Timestamp: ${lastRunTimestamp}`);\n console.log(` Date: ${lastRunDate}`);\n console.log(` Time: ${lastRunTime}`);\n source = 'redis-success';\n \n } else {\n throw new Error('No lastRunTimestamp found in Redis data');\n }\n \n} catch (error) {\n // Fallback: Use 24 hours ago as the \"previous\" run time\n console.log(`āŒ Error loading from Redis: ${error.message}`);\n console.log('Using 24-hour fallback for first run');\n \n const yesterday = new Date();\n yesterday.setHours(yesterday.getHours() - 24);\n const fallbackUK = formatUKTimestamp(yesterday);\n \n lastRunTimestamp = fallbackUK.timestamp;\n lastRunDate = fallbackUK.date;\n lastRunTime = fallbackUK.time;\n source = 'fallback-24h';\n \n console.log(`šŸ“… Using fallback timestamp:`);\n console.log(` Timestamp: ${lastRunTimestamp}`);\n console.log(` Date: ${lastRunDate}`);\n console.log(` Time: ${lastRunTime}`);\n}\n\n// Log the final results\nconsole.log('\\nšŸ”„ FINAL RESULTS:');\nconsole.log(`šŸ“ PREVIOUS run (to use for queries): ${lastRunTimestamp}`);\nconsole.log(`šŸ• CURRENT run (to save for next time): ${currentUK.timestamp}`);\nconsole.log(`šŸ“Š Data source: ${source}`);\n\n// Return both timestamps with clear separation\nreturn [{\n json: {\n // PREVIOUS run timestamps (use these for your data queries)\n lastRunTimestamp: lastRunTimestamp,\n lastRunDate: lastRunDate,\n lastRunTime: lastRunTime,\n \n // CURRENT run timestamps (save these to Redis for next run)\n currentTimestamp: currentUK.timestamp,\n currentDate: currentUK.date,\n currentTime: currentUK.time,\n \n // Metadata\n workflowId: $workflow.id,\n executionId: $execution.id,\n dataSource: source,\n processedAt: new Date().toISOString()\n }\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -7280, + -736 + ], + "id": "7318a9b6-f3b7-4422-ad54-68837318c243", + "name": "Parse Last Timestamp" + }, + { + "parameters": { + "operation": "get", + "key": "collection_lastrun_timestamp", + "options": {} + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + -7504, + -736 + ], + "id": "4f2df54f-0ec5-4b87-b777-a5a0821e8adc", + "name": "Get Last Timestamp from Redis", + "credentials": { + "redis": { + "id": "uPJ4dFmf6tm25qHY", + "name": "Redis account 2" + } + }, + "continueOnFail": true + }, + { + "parameters": { + "operation": "set", + "key": "collection_lastrun_timestamp", + "value": "={{ $json.redisValue }}" + }, + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [ + -5440, + -848 + ], + "id": "ee8ec58b-9840-437c-b481-1f6ca200ff7a", + "name": "Save Timestamp to Redis", + "credentials": { + "redis": { + "id": "uPJ4dFmf6tm25qHY", + "name": "Redis account 2" + } + } + }, + { + "parameters": { + "jsCode": "// Sla de CURRENT execution time pas op NADAT de collections succesvol zijn verwerkt.\n// De waarden komen uit de node \"Parse Last Timestamp\".\n\nconst input = $items('Parse Last Timestamp')[0]?.json;\n\nif (!input) {\n throw new Error('Kon data uit node \"Parse Last Timestamp\" niet vinden.');\n}\n\nconst timestampData = {\n lastRunTimestamp: input.currentTimestamp,\n lastRunDate: input.currentDate,\n lastRunTime: input.currentTime,\n workflowId: input.workflowId,\n executionId: input.executionId,\n savedAt: new Date().toISOString(),\n version: '3.2'\n};\n\nreturn [{\n json: {\n success: true,\n timestampData,\n redisValue: JSON.stringify(timestampData),\n summary: {\n whatWeUsedThisRun: {\n lastRunTimestamp: input.lastRunTimestamp,\n source: input.dataSource\n },\n whatWeSaveForNextRun: {\n lastRunTimestamp: timestampData.lastRunTimestamp,\n note: 'Timestamp is opgeslagen na succesvolle verwerking of na een controle zonder nieuwe collection zendingen.'\n }\n }\n }\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -5680, + -848 + ], + "id": "aa388c2e-22fd-4895-be5d-618b925c7e94", + "name": "Prepare Timestamp Data" + }, + { + "parameters": { + "url": "=https://api.palletways.com/conColDepot/{{$now.setZone('Europe/Amsterdam').minus({ days: 3 }).toFormat('yyyy-MM-dd')}}/{{$now.setZone('Europe/Amsterdam').toFormat('yyyy-MM-dd')}}?apikey=SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus%3D", + "options": { + "response": { + "response": { + "responseFormat": "text" + } + }, + "timeout": 30000 + } + }, + "id": "3dc98464-f24e-4291-bca8-2f425a192bb8", + "name": "Get Collections", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [ + -7056, + -640 + ] + }, + { + "parameters": { + "jsCode": "const OUR_DEPOT = 464;\nconst out = [];\n\nconst asArray = (v) => {\n if (v === undefined || v === null || v === '') return [];\n return Array.isArray(v) ? v : [v];\n};\n\nconst text = (v) => {\n if (v === undefined || v === null) return null;\n const s = String(v).trim();\n return s === '' ? null : s;\n};\n\nconst num = (v) => {\n const s = text(v);\n if (s === null) return null;\n const n = Number(String(s).replace(',', '.'));\n return Number.isFinite(n) ? n : null;\n};\n\nconst intVal = (v) => {\n const n = num(v);\n return n === null ? null : Math.trunc(n);\n};\n\nconst yesNoBool = (v) => {\n const s = String(v ?? '').trim().toLowerCase();\n if (['yes', 'true', '1', 'ja', 'y', 'on'].includes(s)) return true;\n if (['no', 'false', '0', 'nee', 'n', 'off'].includes(s)) return false;\n return null;\n};\n\nconst normalizeCountry = (v) => {\n const s = text(v);\n if (!s) return null;\n const u = s.toUpperCase();\n return u === 'UK' ? 'GB' : u;\n};\n\nconst decodeXml = (s) => String(s ?? '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\");\n\nconst tag = (xml, name) => {\n if (!xml) return null;\n const re = new RegExp(`<${name}(?:\\\\s[^>]*)?>([\\\\s\\\\S]*?)<\\\\/${name}>`, 'i');\n const m = String(xml).match(re);\n return m ? text(decodeXml(m[1])) : null;\n};\n\nconst blocks = (xml, name) => {\n if (!xml) return [];\n const re = new RegExp(`<${name}(?:\\\\s[^>]*)?>([\\\\s\\\\S]*?)<\\\\/${name}>`, 'gi');\n const result = [];\n let m;\n while ((m = re.exec(String(xml))) !== null) {\n result.push(m[1]);\n }\n return result;\n};\n\nconst firstBlock = (xml, name) => blocks(xml, name)[0] || '';\n\nconst safeNodeItem = (name) => {\n try {\n return $(name).item.json || {};\n } catch (e) {\n try {\n return $items(name)?.[0]?.json || {};\n } catch (e2) {\n return {};\n }\n }\n};\n\nconst makeDateTime = (dateValue, timeValue) => {\n const d = text(dateValue);\n const t = text(timeValue);\n if (!d) return null;\n\n // Already datetime\n if (d.includes('T') || /^\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}/.test(d)) {\n return d.replace(' ', 'T');\n }\n\n return `${d}T${t || '00:00:00'}`;\n};\n\nconst dateOnly = (v) => {\n const s = text(v);\n if (!s) return null;\n return s.substring(0, 10);\n};\n\nconst statusFlags = (statusValue, statusDateTime) => {\n const status = text(statusValue);\n const s = String(status || '').toLowerCase();\n\n const isDelivered =\n s.includes('delivered') ||\n s.includes('afgeleverd') ||\n s.includes('pod');\n\n const isCollected =\n s.includes('collected') ||\n s.includes('picked up') ||\n s.includes('pickup complete') ||\n s.includes('collection complete') ||\n s.includes('opgehaald') ||\n s.includes('afgehaald');\n\n const isDeleted =\n s.includes('deleted') ||\n s.includes('removed') ||\n s.includes('cancelled') ||\n s.includes('canceled') ||\n s.includes('verwijderd') ||\n s.includes('geannuleerd');\n\n return {\n current_status: status,\n current_status_at: status ? statusDateTime : null,\n is_delivered: isDelivered,\n delivered_at: isDelivered ? statusDateTime : null,\n is_collected: isCollected,\n collected_at: isCollected ? statusDateTime : null,\n is_deleted: isDeleted,\n deleted_at: isDeleted ? statusDateTime : null,\n status_source: status ? 'PALLETWAYS_API' : null,\n };\n};\n\nfunction addressFromXml(consignmentXml, type) {\n const addressBlocks = blocks(consignmentXml, 'Address');\n const wanted = addressBlocks.find(a => String(tag(a, 'Type') || '').toLowerCase() === type.toLowerCase()) || '';\n return {\n contact: tag(wanted, 'ContactName'),\n company: tag(wanted, 'CompanyName'),\n addr1: tag(wanted, 'Addr1'),\n addr2: tag(wanted, 'Addr2'),\n addr3: tag(wanted, 'Addr3'),\n addr4: tag(wanted, 'Addr4'),\n town: tag(wanted, 'Town'),\n county: tag(wanted, 'County'),\n country: normalizeCountry(tag(wanted, 'Country')),\n postcode: tag(wanted, 'PostCode'),\n };\n}\n\nfunction getAddressFromJson(consignment, type) {\n const addresses = asArray(consignment.Address);\n return addresses.find(a => String(a.Type || '').toLowerCase() === type.toLowerCase()) || {};\n}\n\nfunction getServiceFromXml(consignmentXml, type) {\n const serviceBlocks = blocks(consignmentXml, 'Service');\n return serviceBlocks.find(s => String(tag(s, 'Type') || '').toLowerCase() === type.toLowerCase()) || '';\n}\n\nfunction getServiceFromJson(consignment, type) {\n const services = asArray(consignment.Service);\n return services.find(s => String(s?.Type || '').toLowerCase() === type.toLowerCase()) || {};\n}\n\nfunction countPalletCodesXml(consignmentXml) {\n return blocks(consignmentXml, 'Pallet')\n .map(p => text(decodeXml(p)))\n .filter(Boolean).length;\n}\n\nfunction countPalletCodesJson(palletValue) {\n return asArray(palletValue).map(p => text(p)).filter(Boolean).length;\n}\n\nfunction choosePrimaryService(parts, collectionDepot, deliveryDepot) {\n const collectionCode = text(parts.collection_service_code);\n const deliveryCode = text(parts.delivery_service_code);\n\n if (collectionDepot === OUR_DEPOT && collectionCode) {\n return {\n service_type: 'Collection',\n service_code: collectionCode,\n service_surcharge: text(parts.collection_service_surcharge),\n };\n }\n\n if (deliveryDepot === OUR_DEPOT && deliveryCode) {\n return {\n service_type: 'Delivery',\n service_code: deliveryCode,\n service_surcharge: text(parts.delivery_service_surcharge),\n };\n }\n\n // fallback voor zendingen die wij alleen aanmelden\n if (collectionCode) {\n return {\n service_type: 'Collection',\n service_code: collectionCode,\n service_surcharge: text(parts.collection_service_surcharge),\n };\n }\n\n if (deliveryCode) {\n return {\n service_type: 'Delivery',\n service_code: deliveryCode,\n service_surcharge: text(parts.delivery_service_surcharge),\n };\n }\n\n return {\n service_type: text(parts.service_type),\n service_code: text(parts.service_code),\n service_surcharge: text(parts.service_surcharge),\n };\n}\n\nfunction pushRowFromParts(parts) {\n const payingDepot = intVal(parts.paying_depot);\n const collectionDepot = intVal(parts.collection_depot);\n const deliveryDepot = intVal(parts.delivery_depot);\n const trackingId = text(parts.tracking_id);\n if (!trackingId) return;\n\n const primaryService = choosePrimaryService(parts, collectionDepot, deliveryDepot);\n\n const isOurPayingDepot = Boolean(parts.is_our_paying_depot) || payingDepot === OUR_DEPOT;\n const isCollectionByUs = Boolean(parts.is_collection_by_us) || collectionDepot === OUR_DEPOT;\n const isDeliveryByUs = Boolean(parts.is_delivery_by_us) || deliveryDepot === OUR_DEPOT;\n const isManifested = Boolean(parts.is_manifested) || isOurPayingDepot || text(parts.manifested_at) !== null;\n\n const statusInfo = statusFlags(parts.current_status, parts.current_status_at);\n\n out.push({\n json: {\n tracking_id: trackingId,\n\n consignment_number: text(parts.consignment_number),\n reference: text(parts.reference),\n collection_reference: text(parts.collection_reference),\n\n manifest_date: text(parts.manifest_date),\n manifest_time: text(parts.manifest_time),\n\n paying_depot: payingDepot,\n collection_depot: collectionDepot,\n delivery_depot: deliveryDepot,\n\n is_our_paying_depot: isOurPayingDepot,\n is_collection_by_us: isCollectionByUs,\n is_delivery_by_us: isDeliveryByUs,\n is_manifested: isManifested,\n\n account_name: text(parts.account_name),\n account_code: text(parts.account_code),\n\n consignment_type: text(parts.consignment_type),\n classification: text(parts.classification),\n\n service_type: primaryService.service_type,\n service_code: primaryService.service_code,\n service_surcharge: primaryService.service_surcharge,\n\n collection_service_code: text(parts.collection_service_code),\n collection_service_surcharge: text(parts.collection_service_surcharge),\n delivery_service_code: text(parts.delivery_service_code),\n delivery_service_surcharge: text(parts.delivery_service_surcharge),\n\n due_date: text(parts.due_date),\n due_time: text(parts.due_time),\n\n planned_collection_date: dateOnly(parts.planned_collection_date),\n planned_delivery_date: dateOnly(parts.planned_delivery_date),\n delivery_list_date: dateOnly(parts.delivery_list_date),\n collection_list_date: dateOnly(parts.collection_list_date),\n manifested_at: text(parts.manifested_at),\n\n lifts: num(parts.lifts),\n weight_kg: num(parts.weight_kg),\n\n bill_unit_type: text(parts.bill_unit_type),\n bill_unit_amount: num(parts.bill_unit_amount),\n\n pallet_count: intVal(parts.pallet_count),\n\n handball: yesNoBool(parts.handball),\n tail_lift: yesNoBool(parts.tail_lift),\n adr_goods: yesNoBool(parts.adr_goods),\n limited_quantity_goods: yesNoBool(parts.limited_quantity_goods),\n booked_in: yesNoBool(parts.booked_in),\n book_in_request: yesNoBool(parts.book_in_request),\n\n collection_company: text(parts.collection_company),\n collection_contact: text(parts.collection_contact),\n collection_addr1: text(parts.collection_addr1),\n collection_addr2: text(parts.collection_addr2),\n collection_addr3: text(parts.collection_addr3),\n collection_addr4: text(parts.collection_addr4),\n collection_town: text(parts.collection_town),\n collection_county: text(parts.collection_county),\n collection_country: normalizeCountry(parts.collection_country),\n collection_postcode: text(parts.collection_postcode),\n\n delivery_company: text(parts.delivery_company),\n delivery_contact: text(parts.delivery_contact),\n delivery_addr1: text(parts.delivery_addr1),\n delivery_addr2: text(parts.delivery_addr2),\n delivery_addr3: text(parts.delivery_addr3),\n delivery_addr4: text(parts.delivery_addr4),\n delivery_town: text(parts.delivery_town),\n delivery_county: text(parts.delivery_county),\n delivery_country: normalizeCountry(parts.delivery_country),\n delivery_postcode: text(parts.delivery_postcode),\n\n hub_sequence: intVal(parts.hub_sequence),\n hub_name: text(parts.hub_name),\n\n current_status: statusInfo.current_status,\n current_status_at: statusInfo.current_status_at,\n is_delivered: statusInfo.is_delivered,\n delivered_at: statusInfo.delivered_at,\n is_deleted: statusInfo.is_deleted,\n deleted_at: statusInfo.deleted_at,\n is_collected: statusInfo.is_collected,\n collected_at: statusInfo.collected_at,\n status_source: statusInfo.status_source,\n\n source_first_seen: text(parts.source_name),\n source_last_seen: text(parts.source_name),\n last_api_seen_at: new Date().toISOString(),\n\n raw_json: parts.raw_json ?? {},\n },\n });\n}\n\nfor (const item of items) {\n const root = item.json || {};\n\n const ctxTracking = safeNodeItem('Extract TrackingIDs');\n const ctxCollections = safeNodeItem('Extract Collections or No Work');\n const context = Object.keys(ctxTracking).length ? ctxTracking : (Object.keys(ctxCollections).length ? ctxCollections : {});\n\n const lastWorkday = (() => {\n try { return $items('Last workday1')?.[0]?.json?.lastWorkday || null; } catch (e) { return null; }\n })();\n\n const xml = typeof root.data === 'string'\n ? root.data\n : typeof root.body === 'string'\n ? root.body\n : typeof root === 'string'\n ? root\n : null;\n\n // Route 1: HTTP Request returned XML as json.data/body\n if (xml && xml.includes(' {\n if (v === undefined || v === null || v === '') return [];\n return Array.isArray(v) ? v : [v];\n};\n\nconst text = (v) => {\n if (v === undefined || v === null) return '';\n return String(v).trim();\n};\n\nconst lastRunTimestamp =\n $items(\"Parse Last Timestamp\")[0]?.json?.lastRunTimestamp;\n\nif (!lastRunTimestamp) {\n throw new Error('lastRunTimestamp ontbreekt vanuit Parse Last Timestamp');\n}\n\nconst lastRunDate = new Date(lastRunTimestamp);\nconst response = $json.Response;\n\nconst statusCode = response?.Status?.Code;\nconst description = response?.Status?.Description;\nconst count = Number(response?.Status?.Count ?? 0);\nconst data = response?.Detail?.Data;\n\nif (statusCode === 'NO_RESULTS_FOUND' || count === 0 || !data) {\n return [{\n json: {\n no_work: true,\n reason: description || 'Geen collections gevonden in Palletways response',\n palletways_status_code: statusCode || null,\n palletways_count: count,\n lastRunTimestamp,\n checked_at: new Date().toISOString()\n }\n }];\n}\n\nif (statusCode !== 'OK') {\n throw new Error(`Palletways response niet OK: ${statusCode || 'onbekend'} - ${description || ''}`);\n}\n\nconst dataItems = asArray(data);\n\nfor (const entry of dataItems) {\n const manifest = entry?.Manifest;\n if (!manifest) continue;\n\n const dateStr = text(manifest.Date);\n const timeStr = text(manifest.Time);\n if (!dateStr || !timeStr) continue;\n\n const manifestDateTime = `${dateStr}T${timeStr}`;\n const manifestDate = new Date(manifestDateTime);\n\n if (!(manifestDate > lastRunDate)) {\n continue;\n }\n\n const accounts = asArray(manifest?.Depot?.Account);\n\n for (const account of accounts) {\n const consignments = asArray(account?.Consignment);\n\n for (const c of consignments) {\n const trackingid = text(c?.TrackingID);\n if (!trackingid || seen.has(trackingid)) continue;\n seen.add(trackingid);\n\n out.push({\n json: {\n trackingid,\n source_name: 'PW_COLLECTION_FOR_US',\n collection_list_date: dateStr,\n manifestDateTime,\n manifest_date: dateStr,\n manifest_time: timeStr,\n planned_collection_date: c?.ColDueDate || c?.DueDate || null,\n collection_depot: c?.CollectionDepot ?? null,\n delivery_depot: c?.DeliveryDepot ?? null,\n paying_depot: manifest?.Depot?.Number ?? null\n }\n });\n }\n }\n}\n\nif (out.length === 0) {\n return [{\n json: {\n no_work: true,\n reason: 'Geen nieuwe collections na lastRunTimestamp',\n lastRunTimestamp,\n checked_at: new Date().toISOString()\n }\n }];\n}\n\nreturn out;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -6608, + -640 + ], + "id": "5c00f764-7042-43eb-b12a-cb7b8fc7fd39", + "name": "Extract Collections or No Work" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "824aea27-1485-4cd4-a3e5-061873540857", + "leftValue": "={{ $json.trackingid ? 'yes' : 'no' }}", + "rightValue": "yes", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + -6384, + -640 + ], + "id": "be067f44-a440-4acc-a544-61f56bb2d708", + "name": "Has TrackingID?" + } + ], + "pinData": {}, + "connections": { + "Schedule Trigger": { + "main": [ + [ + { + "node": "Get Last Timestamp from Redis", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get Last Timestamp from Redis": { + "main": [ + [ + { + "node": "Parse Last Timestamp", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse Last Timestamp": { + "main": [ + [ + { + "node": "Get Collections", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get Collections": { + "main": [ + [ + { + "node": "Parse XML Response", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse XML Response": { + "main": [ + [ + { + "node": "Extract Collections or No Work", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract Collections or No Work": { + "main": [ + [ + { + "node": "Has TrackingID?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Has TrackingID?": { + "main": [ + [ + { + "node": "Process Each Consignment", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Prepare Timestamp Data", + "type": "main", + "index": 0 + } + ] + ] + }, + "Process Each Consignment": { + "main": [ + [ + { + "node": "Prepare Timestamp Data", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Get Consignment Details", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get Consignment Details": { + "main": [ + [ + { + "node": "Normalize Palletways Consignment", + "type": "main", + "index": 0 + } + ] + ] + }, + "Normalize Palletways Consignment": { + "main": [ + [ + { + "node": "Upsert Palletways BI", + "type": "main", + "index": 0 + } + ] + ] + }, + "Upsert Palletways BI": { + "main": [ + [ + { + "node": "Process Each Consignment", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepare Timestamp Data": { + "main": [ + [ + { + "node": "Save Timestamp to Redis", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "timezone": "Europe/London", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false, + "timeSavedMode": "fixed", + "saveDataSuccessExecution": "all" + }, + "versionId": "aa30be27-5af1-4866-9117-cfae9ad48a7d", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "bef8d409866a58c0777dfe7cca1b9c2400fd051c056d361501393ab423006b5f" + }, + "nodeGroups": [], + "id": "joBBlyPunxnCOu4R", + "tags": [ + { + "updatedAt": "2025-11-11T04:59:39.416Z", + "createdAt": "2025-11-11T04:59:39.416Z", + "id": "ClOZrAjJKoGLFvaO", + "name": "Palletways" + } + ] +} \ No newline at end of file