Files
n8n-workflows/palletways/Palletways Collections.json

546 lines
45 KiB
JSON

{
"name": "Palletways Collections",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "minutes"
}
]
}
},
"id": "13481794-6f1a-4d51-abec-a844e4a07af9",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.1,
"position": [
-448,
208
]
},
{
"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": "041b1245-b466-4376-bc98-624db2e30509",
"name": "Get Palletways Consignments",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
224,
304
]
},
{
"parameters": {
"options": {
"explicitArray": false,
"mergeAttrs": true
}
},
"id": "399d5813-c556-4a7a-ae50-4e068adcc4fa",
"name": "Parse XML Response",
"type": "n8n-nodes-base.xml",
"typeVersion": 1,
"position": [
448,
304
]
},
{
"parameters": {
"jsCode": "const items = [];\n// $items(\"testTimestamp\")[0]?.json?.lastRunTimestamp || \n// Get the lastRunTimestamp from the previous node's data\nconst lastRunTimestamp = $items(\"Parse Last Timestamp\")[0]?.json?.lastRunTimestamp;\n\n// Pak de Data array (of enkel object) uit de XML Response\nconst data = $json.Response?.Detail?.Data;\n\n// Zorg dat we altijd met een array werken\nconst manifests = Array.isArray(data) ? data : [data];\n\nfor (const entry of manifests) {\n const manifest = entry.Manifest;\n\n // combineer <Date> en <Time> naar ISO formaat\n const dateStr = manifest.Date; // bv. 2025-08-08\n const timeStr = manifest.Time; // bv. 14:41:06\n\n // Bouw ISO string in het gevraagde formaat\n const iso = `${dateStr}T${timeStr}`;\n\n // Voeg nieuwe veld toe EN de lastRunTimestamp\n items.push({\n json: {\n manifestDateTime: iso,\n lastRunTimestamp: lastRunTimestamp, // Add this line!\n ...manifest\n }\n });\n}\n\nreturn items;"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
896,
288
],
"id": "3ee66d6d-4732-48fb-9436-944eb6d8f447",
"name": "Split Manifests"
},
{
"parameters": {
"jsCode": "// Haal de lastRunTimestamp op uit de workflow data\nconst lastRunTimestamp = $json.lastRunTimestamp || $items(\"Parse Last Timestamp\")[0].json.lastRunTimestamp;\n\nconst items = [];\n\nfor (const item of $input.all()) {\n const manifest = item.json;\n\n // Zet beide tijden om naar Date objecten\n const manifestDate = new Date(manifest.manifestDateTime);\n const lastRunDate = new Date(lastRunTimestamp);\n\n // Vergelijk\n if (manifestDate > lastRunDate) {\n items.push(item); // alleen doorgeven als nieuwer\n }\n}\n\nreturn items;\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1104,
288
],
"id": "739a489c-5f8c-433b-b99a-2da066e097c6",
"name": "Filter New Manifests"
},
{
"parameters": {
"options": {}
},
"id": "00953524-f7a4-4a84-8dc6-4c8d88cb5eda",
"name": "Process Each Consignment",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
1440,
288
]
},
{
"parameters": {
"url": "=https://api.palletways.com/lookupDepotNo/{{ $json.Depot.Number }}?apikey=SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus%3D",
"options": {
"response": {
"response": {
"responseFormat": "text"
}
},
"timeout": 10000
}
},
"id": "ae76a60f-3be1-409d-a762-71b984a5fb53",
"name": "Get Paying Depot Address",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
1696,
304
],
"continueOnFail": true
},
{
"parameters": {
"options": {
"explicitArray": false,
"mergeAttrs": true
}
},
"id": "4c975d34-c2e9-4a08-88ff-14633bbd5253",
"name": "Parse Paying Depot XML",
"type": "n8n-nodes-base.xml",
"typeVersion": 1,
"position": [
1904,
304
]
},
{
"parameters": {
"mode": "jsonToxml",
"options": {
"attrkey": "_attributes",
"charkey": "_text",
"headless": false,
"rootName": "import"
}
},
"id": "b059a773-6bae-4116-a57d-cacb18570f1f",
"name": "Generate Transpas XML",
"type": "n8n-nodes-base.xml",
"typeVersion": 1,
"position": [
2608,
304
]
},
{
"parameters": {
"jsCode": "// Combine Consignment + Depot - robust + XML-safe version\n// Paste this into a Code (v2) node's \"JS Code\" field.\n\nconst debug = (label, v) => {\n try { console.log(label, v); } catch (e) {}\n};\n\n// --- XML escaping (zonder dubbel-escapen van bestaande entities) ---\nconst AMP_FIX_RE = /&(?!amp;|lt;|gt;|quot;|apos;|#\\d+;|#x[0-9A-Fa-f]+;)/g;\n\nconst xmlEsc = (s) =>\n String(s ?? '')\n .replace(AMP_FIX_RE, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n\n// Recursief alle strings in object/array XML-safe maken\nfunction sanitizeForXml(value, seen = new WeakSet()) {\n if (value == null) return value;\n\n const t = typeof value;\n\n if (t === 'string') return xmlEsc(value);\n if (t === 'number' || t === 'boolean' || t === 'bigint') return value;\n\n // Dates -> string\n if (value instanceof Date) return xmlEsc(value.toISOString());\n\n // Arrays\n if (Array.isArray(value)) {\n return value.map(v => sanitizeForXml(v, seen));\n }\n\n // Objects (let op circular refs)\n if (t === 'object') {\n if (seen.has(value)) return value; // voorkom infinite loops\n seen.add(value);\n\n const out = {};\n for (const [k, v] of Object.entries(value)) {\n out[k] = sanitizeForXml(v, seen);\n }\n return out;\n }\n\n // functions / symbols etc.\n return value;\n}\n\nlet depotJson = null;\nlet consJson = null;\n\n// 1) Try to read from the multiple incoming inputs ($input.all())\ntry {\n if (typeof $input !== 'undefined' && typeof $input.all === 'function') {\n const allInputs = $input.all();\n debug('allInputs length', allInputs.length);\n for (const inputArray of allInputs) {\n if (!Array.isArray(inputArray) || inputArray.length === 0) continue;\n const candidate = (inputArray[0] && inputArray[0].json) ? inputArray[0].json : inputArray[0];\n if (!candidate) continue;\n\n const looksLikeDepot =\n candidate && candidate.Response && candidate.Response.Detail && candidate.Response.Detail.Data;\n\n const looksLikeConsignment =\n candidate && (candidate.TrackingID || candidate.Depot || candidate.Reference || candidate.Weight || candidate.Address);\n\n if (looksLikeDepot && !depotJson) depotJson = candidate;\n else if (looksLikeConsignment && !consJson) consJson = candidate;\n else {\n if (!consJson) consJson = candidate;\n else if (!depotJson) depotJson = candidate;\n }\n }\n }\n} catch (e) {\n debug('input.all() failed', e.message || e);\n}\n\n// 2) Fallback: try $items('Node Name')\ntry {\n if (!consJson && typeof $items === 'function') {\n const consItems = $items('Process Each Consignment') || [];\n if (consItems.length) consJson = (consItems[consItems.length - 1].json || consItems[consItems.length - 1]);\n }\n if (!depotJson && typeof $items === 'function') {\n const depotItems = $items('Parse Paying Depot XML') || [];\n if (depotItems.length) depotJson = (depotItems[0].json || depotItems[0]);\n }\n} catch (e) {\n debug('$items(...) failed', e.message || e);\n}\n\n// 3) Fallback: try $node['Name'].json\ntry {\n if (!consJson && typeof $node !== 'undefined' && $node['Process Each Consignment']) {\n const nodeVal = $node['Process Each Consignment'].json;\n if (nodeVal && Object.keys(nodeVal).length) consJson = nodeVal;\n }\n if (!depotJson && typeof $node !== 'undefined' && $node['Parse Paying Depot XML']) {\n const nodeVal = $node['Parse Paying Depot XML'].json;\n if (nodeVal && Object.keys(nodeVal).length) depotJson = nodeVal;\n }\n} catch (e) {\n debug('$node[...] failed', e.message || e);\n}\n\n// Ensure we return objects\nconsJson = consJson || {};\ndepotJson = depotJson || {};\n\n// Final fallback: if both empty, try use the first incoming item found\ntry {\n if (\n Object.keys(consJson).length === 0 &&\n Object.keys(depotJson).length === 0 &&\n typeof $input !== 'undefined' &&\n typeof $input.all === 'function'\n ) {\n const allInputs = $input.all();\n if (allInputs.length && allInputs[0].length && allInputs[0][0].json) {\n consJson = allInputs[0][0].json;\n }\n }\n} catch (e) {}\n\n// ✅ Make everything XML-safe (all nested strings)\nconst consXmlSafe = sanitizeForXml(consJson);\nconst depotXmlSafe = sanitizeForXml(depotJson);\n\n// Return merged object\nreturn [{\n json: {\n consignment: consXmlSafe,\n depot: depotXmlSafe\n }\n}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2144,
304
],
"id": "1f9da4cd-4535-4e34-aad0-88b6d0636bda",
"name": "Combine Consignment + Depot"
},
{
"parameters": {
"jsCode": "// Defensive Transform to Transpas Format (Code v3 - Updated)\n// Paste this into the JS Code field of your Transform node.\n\n// ---------- Helpers ----------\nconst get = (obj, path) => {\n if (!obj || !path) return undefined;\n const parts = path.split('.');\n let cur = obj;\n for (const p of parts) {\n if (cur === undefined || cur === null) return undefined;\n cur = cur[p];\n }\n return cur;\n};\n\nconst isEmptyVal = (v) => {\n return v === undefined || v === null ||\n (typeof v === 'string' && v.trim() === '') ||\n (Array.isArray(v) && v.length === 0) ||\n (typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length === 0);\n};\n\nconst findFirst = (obj, paths) => {\n for (const p of paths) {\n const v = get(obj, p);\n if (!isEmptyVal(v)) return v;\n }\n return undefined;\n};\n\nconst ensureArray = (v) => Array.isArray(v) ? v : (v === undefined || v === null ? [] : [v]);\n\nconst generateTag = () => {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {\n const r = Math.random() * 16 | 0;\n const v = c === 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n }).toUpperCase();\n};\n\nconst getCountryCode = (country) => {\n if (!country) return 'NL';\n const countryUpper = country.toString().toUpperCase();\n return countryUpper === 'GB' || countryUpper === 'UK' ? 'GB' : countryUpper;\n};\n\nconst toNumber = (v, fallback = 0) => {\n if (v === null || v === undefined) return fallback;\n const n = parseFloat(String(v).replace(',', '.'));\n return Number.isFinite(n) ? n : fallback;\n};\n\n// ---------- Pallet dimension mapping ----------\nconst palletDimensions = {\n \"FP\": { length: 120, width: 100, height: 200 },\n \"LP\": { length: 120, width: 100, height: 220 },\n \"EPL\": { length: 120, width: 80, height: 220 },\n \"HP\": { length: 120, width: 100, height: 120 },\n \"QP\": { length: 120, width: 100, height: 80 },\n \"MQP\": { length: 120, width: 100, height: 60 }\n};\n\n// ---------- Inputs ----------\nlet input = $json || {};\nconst consFromCombine = input.consignment || input.Consignment;\nconst depotFromCombine = input.depot || input.Depot;\n\nlet consRoot = consFromCombine || input || {};\nconst nestedConsCandidate = findFirst(consRoot, [\n 'Depot.Account.Consignment',\n 'Account.Consignment',\n 'Consignment',\n 'Depot.Account',\n 'Account'\n]);\nif (nestedConsCandidate && typeof nestedConsCandidate === 'object' && Object.keys(nestedConsCandidate).length > 0) {\n const maybeT = findFirst(nestedConsCandidate, ['TrackingID','Reference','Weight']);\n if (!isEmptyVal(maybeT)) consRoot = nestedConsCandidate;\n}\n\nconst collectionReference = findFirst(consRoot, [\n 'CollectionReference',\n 'pickupaddress.reference',\n 'Pickup.Reference',\n 'Consignment.CollectionReference'\n]) || '';\n\nconst depotData = depotFromCombine || findFirst(input, ['depot','Depot','Response.Detail.Data','Response.Detail','Detail.Data','Data']) || {};\nconst depotAddress = depotData?.Response?.Detail?.Data || depotData || {};\n\n// ---------- Field resolution ----------\nconst trackingId = findFirst(consRoot, [\n 'TrackingID','trackingId','Reference','reference',\n 'Consignment.TrackingID','Depot.Account.Consignment.TrackingID',\n 'Account.Consignment.TrackingID','Consignment.Reference'\n]) || '';\n\nconst reference = findFirst(consRoot, ['Reference','reference','Ref','Consignment.Reference']) || trackingId || '';\n\nconst weight = toNumber(findFirst(consRoot, ['Weight','weight','Consignment.Weight','TotalWeight']), 0);\n\nconst palletDetails = findFirst(consRoot, [\n 'PalletDetail','PalletDetails','Pallets','Consignment.PalletDetail','Consignment.Pallets'\n]);\n\nconst billUnit = findFirst(consRoot, ['BillUnit','BillUnits','Consignment.BillUnit']);\n\nconst addressesCandidate = findFirst(consRoot, [\n 'Address','Addresses','Consignment.Address','Consignment.Addresses',\n 'Depot.Account.Consignment.Address','AddressList','Addresses.Address'\n]);\nlet addresses = ensureArray(addressesCandidate);\n\nlet collectionAddress = addresses.find(a => a && (a.Type === 'Collection' || (a.Type && a.Type.toLowerCase().includes('collect')))) || {};\nlet deliveryAddress = addresses.find(a => a && (a.Type === 'Delivery' || (a.Type && a.Type.toLowerCase().includes('deliver')))) || {};\nif (isEmptyVal(collectionAddress) && addresses.length >= 1) collectionAddress = addresses[0] || {};\nif (isEmptyVal(deliveryAddress) && addresses.length >= 2) deliveryAddress = addresses[1] || {};\nif (isEmptyVal(deliveryAddress) && !isEmptyVal(collectionAddress)) deliveryAddress = collectionAddress;\n\nconst deliveryDepot = findFirst(consRoot, ['DeliveryDepot','deliveryDepot','Depot.DeliveryDepot']) || findFirst(consRoot, ['Depot','Depot.Number','DepotNo']) || undefined;\n\nconst lifts = toNumber(findFirst(consRoot, ['Lifts','lifts']), 1);\nconst tailLift = findFirst(consRoot, ['TailLift','tailLift','Tail_Lift']) || false;\nconst bookInRequest = findFirst(consRoot, ['BookInRequest','bookinrequest','bookInRequest']) || false;\nconst colDueDate = findFirst(consRoot, ['ColDueDate','CollectionDueDate','ColDueDate']) || (new Date().toISOString().split('T')[0]);\nconst bookInContact = findFirst(consRoot, ['BookInContactName','BookInContact','BookIn.ContactName']) || '';\nconst bookInEmail = findFirst(consRoot, ['BookInEmailAddress','BookInEmail','BookIn.Email']) || '';\nconst bookInReference = findFirst(consRoot, ['BookInReference','BookIn.Ref','BookInReference']) || '';\nconst manifestNote = findFirst(consRoot, ['_manifest.ManifestNote','ManifestNote','Manifest.Note','_manifest?.ManifestNote']) || 'pending';\n\n// ---------- Build goods lines ----------\nlet goodslines = [];\nif (!isEmptyVal(palletDetails)) {\n const palletArr = ensureArray(palletDetails);\n const count = palletArr.length || 1;\n const totalWeight = weight;\n goodslines = palletArr.map((p, i) => ({\n sequence: (i + 1),\n unitamount: toNumber(p.Amount || p.amount || 1, 1),\n unit_id: 'HP',\n weight: (count > 0 ? totalWeight / count : 0),\n product_id: 123,\n productdescription: 'Goodss',\n length: toNumber(p.Length || p.length || 120, 120),\n width: toNumber(p.Width || p.width || 80, 80),\n height: toNumber(p.Height || p.height || 150, 150),\n tag: generateTag()\n }));\n} else if (!isEmptyVal(billUnit)) {\n const buArray = ensureArray(billUnit)\n .map(bu => ({ \n Type: (bu.Type || bu.type || '').toString().trim(), \n Amount: toNumber(bu.Amount || bu.amount || 0, 0) \n }))\n .filter(bu => bu.Type !== '' && bu.Amount > 0);\n\n const grouped = buArray.reduce((acc, bu) => {\n acc[bu.Type] = (acc[bu.Type] || 0) + bu.Amount;\n return acc;\n }, {});\n\n const totalUnits = Object.values(grouped).reduce((a, n) => a + n, 0) || 1;\n\n goodslines = Object.entries(grouped).map(([type, amount], idx) => {\n const share = amount / totalUnits;\n const dims = palletDimensions[type] || { length: 120, width: 80, height: 150 };\n return {\n sequence: idx + 1,\n unitamount: amount,\n unit_id: {\n _attributes: { matchmode: '1' },\n _text: type\n },\n weight: weight * share,\n product_id: 123,\n productdescription: 'Goods',\n length: dims.length,\n width: dims.width,\n height: dims.height,\n tag: generateTag()\n };\n });\n} else {\n goodslines = [{\n sequence: 1,\n unitamount: 1,\n unit_id: 'HP',\n weight: weight,\n product_id: 123,\n productdescription: 'Goods',\n length: 120,\n width: 80,\n height: 150,\n tag: generateTag()\n }];\n}\n\n// ---------- Final delivery address ----------\nlet finalDeliveryAddress = {};\nif (deliveryDepot === '464' || Number(deliveryDepot) === 464) {\n finalDeliveryAddress = {\n name: deliveryAddress.CompanyName || deliveryAddress.Company || '',\n address1: deliveryAddress.Addr1 || deliveryAddress.AddressLine1 || deliveryAddress.address1 || '',\n address2: deliveryAddress.Addr2 || deliveryAddress.AddressLine2 || '',\n zipcode: deliveryAddress.PostCode || deliveryAddress.Postcode || deliveryAddress.zipcode || '',\n city_id: deliveryAddress.Town || deliveryAddress.City || '',\n country: getCountryCode(deliveryAddress.Country || 'NL'),\n contact: bookInContact || deliveryAddress.ContactName || deliveryAddress.Contact || '',\n phone: deliveryAddress.Telephone || deliveryAddress.Phone || '',\n email: bookInEmail || deliveryAddress.Email || '',\n deliveryaddress: bookInReference || '',\n driverinfo: manifestNote\n };\n} else {\n finalDeliveryAddress = {\n name: 'Palletways (UK) Ltd',\n address1: 'Bijsterhuizen 1103 A',\n address2: '',\n zipcode: '6546 AS',\n city_id: 'Nijmegen',\n country: 'NL',\n contact: '',\n phone: '',\n email: '',\n deliveryaddress: '',\n driverinfo: manifestNote\n };\n}\n\n// ---------- Build Transpas XML ----------\nconst today = new Date().toISOString().split('T')[0];\n\n// address_id must be an empty element with matchmode=\"5\"\nconst makeAddressId = () => ({\n _attributes: { matchmode: '5' },\n _text: ''\n});\n\nconst transpasXml = {\n transportbookings: {\n transportbooking: {\n autoacceptmethod: 0,\n customer_id: 55107,\n department_id: 1,\n currency_id: 1,\n tag: generateTag(),\n shipments: {\n shipment: {\n edireference: trackingId || '',\n reference: trackingId || '',\n plangroup_id: 33,\n debtor_id: 55107,\n tag: generateTag(),\n\n sender: {\n name: collectionAddress.CompanyName || collectionAddress.Company || '',\n address1: collectionAddress.Addr1 || collectionAddress.AddressLine1 || collectionAddress.address1 || '',\n address2: collectionAddress.Addr2 || collectionAddress.AddressLine2 || '',\n zipcode: collectionAddress.PostCode || collectionAddress.Postcode || collectionAddress.zipcode || '',\n address_id: makeAddressId(),\n city_id: {\n _attributes: { matchmode: '4' },\n _text: collectionAddress.Town || collectionAddress.City || ''\n },\n country_id: {\n _attributes: { matchmode: '2' },\n _text: getCountryCode(collectionAddress.Country || 'NL')\n },\n contact: collectionAddress.ContactName || collectionAddress.Contact || '',\n phone: collectionAddress.Telephone || collectionAddress.Phone || '',\n email: '',\n driverinfo: manifestNote\n },\n\n receiver: {\n name: deliveryAddress.CompanyName || deliveryAddress.Company || '',\n address1: deliveryAddress.Addr1 || deliveryAddress.AddressLine1 || '',\n address2: deliveryAddress.Addr2 || deliveryAddress.AddressLine2 || '',\n zipcode: deliveryAddress.PostCode || deliveryAddress.Postcode || '',\n address_id: makeAddressId(),\n city_id: {\n _attributes: { matchmode: '4' },\n _text: deliveryAddress.Town || deliveryAddress.City || ''\n },\n country_id: {\n _attributes: { matchmode: '2' },\n _text: getCountryCode(deliveryAddress.Country || 'NL')\n },\n contact: deliveryAddress.ContactName || deliveryAddress.Contact || '',\n phone: deliveryAddress.Telephone || deliveryAddress.Phone || '',\n email: ''\n },\n\n pickupaddress: {\n date: today,\n datetill: colDueDate || today,\n reference: collectionReference || '',\n name: collectionAddress.CompanyName || collectionAddress.Company || '',\n address1: collectionAddress.Addr1 || collectionAddress.AddressLine1 || '',\n address2: collectionAddress.Addr2 || collectionAddress.AddressLine2 || '',\n zipcode: collectionAddress.PostCode || collectionAddress.Postcode || '',\n address_id: makeAddressId(),\n city_id: {\n _attributes: { matchmode: '4' },\n _text: collectionAddress.Town || collectionAddress.City || ''\n },\n country_id: {\n _attributes: { matchmode: '2' },\n _text: getCountryCode(collectionAddress.Country || 'NL')\n },\n contact: collectionAddress.ContactName || collectionAddress.Contact || '',\n phone: collectionAddress.Telephone || collectionAddress.Phone || '',\n email: '',\n driverinfo: manifestNote\n },\n\n deliveryaddress: {\n date: today,\n datetill: colDueDate || today,\n name: finalDeliveryAddress.name,\n address1: finalDeliveryAddress.address1,\n address2: finalDeliveryAddress.address2,\n zipcode: finalDeliveryAddress.zipcode,\n address_id: makeAddressId(),\n city_id: {\n _attributes: { matchmode: '4' },\n _text: finalDeliveryAddress.city_id\n },\n country_id: {\n _attributes: { matchmode: '2' },\n _text: finalDeliveryAddress.country\n },\n contact: finalDeliveryAddress.contact || '',\n phone: finalDeliveryAddress.phone || '',\n email: finalDeliveryAddress.email || '',\n deliveryaddress: finalDeliveryAddress.deliveryaddress || '',\n driverinfo: finalDeliveryAddress.driverinfo\n },\n\n references: {\n reference: [\n { referencekind_id: 15, description: trackingId || '', tag: generateTag() },\n { referencekind_id: 16, description: reference || '', tag: generateTag() }\n ]\n },\n\n parties: {\n party: [\n {\n partytype_id: 5,\n name: depotAddress.CompanyName || depotAddress.DepotName || depotAddress.Company || 'Palletways (UK) Ltd',\n address1: depotAddress.AddressLine1 || depotAddress.AddressLine || depotAddress.address1 || '',\n address2: depotAddress.AddressLine2 || '',\n zipcode: depotAddress.Postcode || depotAddress.PostCode || '',\n address_id: makeAddressId(),\n country_id: {\n _attributes: { matchmode: '2' },\n _text: getCountryCode(depotAddress.Countrycode || 'NL')\n },\n cityname: depotAddress.Town || depotAddress.City || '',\n contact: '',\n phone: depotAddress.Phone || depotAddress.Telephone || '',\n email: depotAddress.Email || '',\n tag: generateTag()\n },\n {\n partytype_id: 3,\n name: collectionAddress.CompanyName || collectionAddress.Company || '',\n address1: collectionAddress.Addr1 || collectionAddress.AddressLine1 || collectionAddress.address1 || '',\n zipcode: collectionAddress.PostCode || collectionAddress.Postcode || '',\n address_id: makeAddressId(),\n country_id: {\n _attributes: { matchmode: '2' },\n _text: getCountryCode(collectionAddress.Country || 'NL')\n },\n cityname: collectionAddress.Town || collectionAddress.City || '',\n contact: collectionAddress.ContactName || collectionAddress.Contact || '',\n phone: collectionAddress.Telephone || collectionAddress.Phone || '',\n email: '',\n tag: generateTag()\n },\n {\n partytype_id: 4,\n name: deliveryAddress.CompanyName || deliveryAddress.Company || '',\n address1: deliveryAddress.Addr1 || deliveryAddress.AddressLine1 || '',\n zipcode: deliveryAddress.PostCode || deliveryAddress.Postcode || '',\n address_id: makeAddressId(),\n country_id: {\n _attributes: { matchmode: '2' },\n _text: getCountryCode(deliveryAddress.Country || 'NL')\n },\n cityname: deliveryAddress.Town || deliveryAddress.City || '',\n contact: deliveryAddress.ContactName || deliveryAddress.Contact || '',\n phone: deliveryAddress.Telephone || deliveryAddress.Phone || '',\n email: '',\n tag: generateTag()\n }\n ]\n },\n\n cargo: {\n unitamount: (lifts || 1),\n unit_id: 591,\n product_id: 123,\n productdescription: 'General Cargo',\n weight: toNumber(weight, 0),\n bool1: (tailLift === 'yes' || tailLift === true) ? 'true' : 'false',\n bool2: (bookInRequest === 'yes' || bookInRequest === true) ? 'true' : 'false',\n goodslines: { goodsline: goodslines }\n }\n }\n }\n }\n }\n};\n\n// ---------- Output ----------\nreturn [{ json: transpasXml }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2400,
304
],
"id": "70c37aca-7402-4003-9efb-5dbcb090c6b2",
"name": "Transform to Transpas Format v2"
},
{
"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": [
0,
208
],
"id": "00609dcf-a55c-4897-90fc-dd2cac741ad1",
"name": "Parse Last Timestamp"
},
{
"parameters": {
"operation": "get",
"key": "consconfirmed_lastrun_timestamp",
"options": {}
},
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
-224,
208
],
"id": "dc991d4e-3f8b-431b-aec0-feddc7d15a91",
"name": "Get Last Timestamp from Redis",
"credentials": {
"redis": {
"id": "uPJ4dFmf6tm25qHY",
"name": "Redis account 2"
}
},
"continueOnFail": true
},
{
"parameters": {
"operation": "set",
"key": "consconfirmed_lastrun_timestamp",
"value": "={{ $json.redisValue }}"
},
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
448,
112
],
"id": "7607461d-7a5e-42d5-ab61-16581e6f36af",
"name": "Save Timestamp to Redis",
"credentials": {
"redis": {
"id": "uPJ4dFmf6tm25qHY",
"name": "Redis account 2"
}
}
},
{
"parameters": {
"jsCode": "// Prepare the timestamp data structure for Redis storage\n// This saves the CURRENT execution time for the NEXT run to use\n\nconst input = $input.first().json;\n\n// Create the complete data structure\nconst timestampData = {\n lastRunTimestamp: input.currentTimestamp, // Current becomes \"last\" for next run\n lastRunDate: input.currentDate,\n lastRunTime: input.currentTime,\n workflowId: input.workflowId,\n executionId: input.executionId,\n savedAt: new Date().toISOString(),\n version: '3.0'\n};\n\nconsole.log('💾 Preparing to save CURRENT execution time for NEXT run:');\nconsole.log(` Timestamp: ${timestampData.lastRunTimestamp}`);\nconsole.log(` Date: ${timestampData.lastRunDate}`);\nconsole.log(` Time: ${timestampData.lastRunTime}`);\nconsole.log(` Workflow ID: ${timestampData.workflowId}`);\nconsole.log(` Execution ID: ${timestampData.executionId}`);\n\nreturn [{\n json: {\n success: true,\n timestampData: 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: 'This current execution time becomes the lastRunTimestamp for next run'\n }\n }\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
224,
112
],
"id": "fc0e98ee-1665-4d1c-acad-b22eafeac46c",
"name": "Prepare Timestamp Data"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "9cca43eb-c3b7-49c7-a927-3fd6a406e43d",
"leftValue": "={{ $('Parse XML Response').item.json.Response.Status.Code }}",
"rightValue": "OK",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
672,
304
],
"id": "6ea6353d-c5b9-436a-be99-3f83284403b8",
"name": "Check for shipments"
},
{
"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": [
-48,
-272
],
"id": "07894f61-7c94-4679-870b-fa2ee94bce85",
"name": "Parse Last Timestamp1"
},
{
"parameters": {
"operation": "upload",
"path": "=/prod/ToTP/palletways/collection_palletways_{{ $('Filter New Manifests').item.json.Depot.Account.Consignment.TrackingID }}.xml",
"binaryData": false,
"fileContent": "={{ $json.data }}",
"options": {}
},
"type": "n8n-nodes-base.ftp",
"typeVersion": 1,
"position": [
2880,
352
],
"id": "f86e20a1-e613-4711-85e0-df579481d8f2",
"name": "FTP",
"credentials": {
"ftp": {
"id": "oLAZ4OgmkOopMHAq",
"name": "FTP De Wit Transport"
}
}
},
{
"parameters": {
"content": "## Timezone staat ingesteld op London",
"height": 96,
"width": 336
},
"type": "n8n-nodes-base.stickyNote",
"position": [
192,
-224
],
"typeVersion": 1,
"id": "28b3620a-627d-42ae-ae14-777caff8e347",
"name": "Sticky Note"
}
],
"pinData": {},
"connections": {
"Schedule Trigger": {
"main": [
[
{
"node": "Get Last Timestamp from Redis",
"type": "main",
"index": 0
}
]
]
},
"Get Palletways Consignments": {
"main": [
[
{
"node": "Parse XML Response",
"type": "main",
"index": 0
}
]
]
},
"Parse XML Response": {
"main": [
[
{
"node": "Check for shipments",
"type": "main",
"index": 0
}
]
]
},
"Split Manifests": {
"main": [
[
{
"node": "Filter New Manifests",
"type": "main",
"index": 0
}
]
]
},
"Filter New Manifests": {
"main": [
[
{
"node": "Process Each Consignment",
"type": "main",
"index": 0
}
]
]
},
"Process Each Consignment": {
"main": [
[],
[
{
"node": "Get Paying Depot Address",
"type": "main",
"index": 0
}
]
]
},
"Get Paying Depot Address": {
"main": [
[
{
"node": "Parse Paying Depot XML",
"type": "main",
"index": 0
}
]
]
},
"Parse Paying Depot XML": {
"main": [
[
{
"node": "Combine Consignment + Depot",
"type": "main",
"index": 0
}
]
]
},
"Generate Transpas XML": {
"main": [
[
{
"node": "FTP",
"type": "main",
"index": 0
}
]
]
},
"Combine Consignment + Depot": {
"main": [
[
{
"node": "Transform to Transpas Format v2",
"type": "main",
"index": 0
}
]
]
},
"Transform to Transpas Format v2": {
"main": [
[
{
"node": "Generate Transpas XML",
"type": "main",
"index": 0
}
]
]
},
"Get Last Timestamp from Redis": {
"main": [
[
{
"node": "Parse Last Timestamp",
"type": "main",
"index": 0
}
]
]
},
"Parse Last Timestamp": {
"main": [
[
{
"node": "Get Palletways Consignments",
"type": "main",
"index": 0
},
{
"node": "Prepare Timestamp Data",
"type": "main",
"index": 0
}
]
]
},
"Prepare Timestamp Data": {
"main": [
[
{
"node": "Save Timestamp to Redis",
"type": "main",
"index": 0
}
]
]
},
"Check for shipments": {
"main": [
[
{
"node": "Split Manifests",
"type": "main",
"index": 0
}
]
]
},
"FTP": {
"main": [
[
{
"node": "Process Each Consignment",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"timezone": "Europe/London",
"callerPolicy": "workflowsFromSameOwner",
"availableInMCP": false,
"timeSavedMode": "fixed"
},
"versionId": "ee388549-6fee-4232-a128-8efc62e2b246",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "bef8d409866a58c0777dfe7cca1b9c2400fd051c056d361501393ab423006b5f"
},
"nodeGroups": [],
"id": "o5mTWc7qVewVHLf5",
"tags": [
{
"updatedAt": "2025-11-11T04:59:39.416Z",
"createdAt": "2025-11-11T04:59:39.416Z",
"id": "ClOZrAjJKoGLFvaO",
"name": "Palletways"
}
]
}