{ "name": "Mazet status --> TLN", "nodes": [ { "parameters": { "options": {} }, "type": "n8n-nodes-base.xml", "typeVersion": 1, "position": [ 880, -192 ], "id": "692aaacf-8e4a-493f-a7b8-33cfff6f6d18", "name": "XML" }, { "parameters": { "operation": "xml", "options": {} }, "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1.1, "position": [ 704, -192 ], "id": "16cfe218-09f4-4e0a-bfcf-d1176677f8d0", "name": "Extract from File1" }, { "parameters": { "jsCode": "// n8n Code node\n// Zet op: Run once for all items\n\nconst xmlEsc = (s) =>\n String(s ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n\nconst first = (...vals) =>\n vals.find(v => v !== undefined && v !== null && String(v).trim() !== '');\n\nconst get = (obj, path) =>\n path.split('.').reduce((a, k) => (a && a[k] !== undefined ? a[k] : undefined), obj);\n\nfunction toAT(dateStr, timeStr) {\n const d = String(dateStr ?? '').trim();\n const t = String(timeStr ?? '').trim();\n\n const dm = d.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{4})$/);\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n\n if (!dm) {\n throw new Error(`Ongeldige datum: \"${d}\"`);\n }\n\n const day = dm[1];\n const month = dm[2];\n const year = dm[3];\n\n const hour = tm ? tm[1] : '00';\n const minute = tm ? tm[2] : '00';\n const second = tm && tm[3] ? tm[3] : '00';\n\n return `${year}${month}${day}${hour}${minute}${second}`;\n}\n\nfunction toATFromYMD(dateStr, timeStr = '') {\n const d = String(dateStr ?? '').trim();\n const t = String(timeStr ?? '').trim();\n\n const dm = d.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n\n if (!dm) return '';\n\n const year = dm[1];\n const month = dm[2];\n const day = dm[3];\n\n const hour = tm ? tm[1] : '00';\n const minute = tm ? tm[2] : '00';\n const second = tm && tm[3] ? tm[3] : '00';\n\n return `${year}${month}${day}${hour}${minute}${second}`;\n}\n\nfunction parseCombinedToAT(value) {\n const s = String(value ?? '').trim();\n if (!s) return '';\n\n if (/^\\d{14}$/.test(s)) return s;\n\n let m = s.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{4})(?:\\s+(\\d{2}):(\\d{2})(?::(\\d{2}))?)?$/);\n if (m) {\n const [, day, month, year, hh = '00', mm = '00', ss = '00'] = m;\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n m = s.match(/^(\\d{4})-(\\d{2})-(\\d{2})(?:[T\\s](\\d{2}):(\\d{2})(?::(\\d{2}))?)?$/);\n if (m) {\n const [, year, month, day, hh = '00', mm = '00', ss = '00'] = m;\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n const digits = s.replace(/\\D/g, '');\n if (digits.length >= 14) return digits.slice(0, 14);\n\n return '';\n}\n\nfunction dmyToYmd(dateStr) {\n const s = String(dateStr ?? '').trim();\n const m = s.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{4})$/);\n if (!m) return '';\n return `${m[3]}-${m[2]}-${m[1]}`;\n}\n\nfunction ymdToDmy(dateStr) {\n const s = String(dateStr ?? '').trim();\n const m = s.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n if (!m) return s;\n return `${m[3]}/${m[2]}/${m[1]}`;\n}\n\nfunction normalizeEventAT(dateStr, timeStr) {\n return toATFromYMD(dateStr, timeStr);\n}\n\nconst STATUS_MAP = {\n // leeg laten als Transpas dit zelf mapt\n};\n\nconst inputItems = $input.all();\n\nconst rows = inputItems.flatMap(item => {\n if (Array.isArray(item.json)) return item.json;\n return [item.json];\n});\n\nreturn rows.map((row, index) => {\n const data = row?.TrackingResponse?.Data ?? row?.Data ?? row;\n const mazetBody = row?.body ?? {};\n const mazetPosition = mazetBody?.position ?? {};\n const events = Array.isArray(mazetBody?.evenements) ? mazetBody.evenements : [];\n\n const consignmentNumber = first(data?.ConsignmentNumber);\n const sourceStatus = first(data?.CurrentStatus);\n const date = first(data?.Date);\n const time = first(data?.Time);\n const originalAdditionalText = first(data?.AdditionalText, '');\n const mappedStatus = STATUS_MAP[sourceStatus] ?? sourceStatus;\n\n if (!consignmentNumber) {\n throw new Error(`Item ${index + 1}: ConsignmentNumber ontbreekt`);\n }\n if (!sourceStatus) {\n throw new Error(`Item ${index + 1}: CurrentStatus ontbreekt`);\n }\n if (!date) {\n throw new Error(`Item ${index + 1}: Date ontbreekt`);\n }\n\n const atDateTime = toAT(date, time);\n const sourceDateYmd = dmyToYmd(date);\n\n // Zoek exact passend event:\n // 1. status + datum + tijd\n // 2. status + datum\n // 3. status\n const matchedEvent =\n events.find(ev =>\n String(ev?.Codeevenement || '') === String(sourceStatus) &&\n normalizeEventAT(ev?.Date, ev?.Heure) === atDateTime\n ) ||\n events.find(ev =>\n String(ev?.Codeevenement || '') === String(sourceStatus) &&\n String(ev?.Date || '') === sourceDateYmd\n ) ||\n events.find(ev =>\n String(ev?.Codeevenement || '') === String(sourceStatus)\n ) ||\n null;\n\n // RDV / ETA vanuit matching event\n const rdvDate = first(\n matchedEvent?.Daterdv,\n mazetPosition?.Daterdv,\n mazetPosition?.Datelivdemandee,\n ''\n );\n\n const rdvDateDmy = rdvDate ? ymdToDmy(rdvDate) : '';\n\n let additionalText = originalAdditionalText;\n if (rdvDateDmy && !/\\bRDV\\s*:/i.test(additionalText)) {\n additionalText = additionalText\n ? `RDV: ${rdvDateDmy} - ${additionalText}`\n : `RDV: ${rdvDateDmy}`;\n }\n\n // ETA: eerst RDV uit Mazet, anders bestaande ETA-logica\n let etaAT = '';\n if (rdvDate) {\n etaAT = toATFromYMD(rdvDate);\n } else {\n const etaDate = first(\n data?.EtaDate,\n data?.ETADate,\n data?.EstimatedDeliveryDate,\n data?.EstimatedArrivalDate,\n get(data, 'ETA.Date')\n );\n\n const etaTime = first(\n data?.EtaTime,\n data?.ETATime,\n data?.EstimatedDeliveryTime,\n data?.EstimatedArrivalTime,\n get(data, 'ETA.Time')\n );\n\n const etaCombined = first(\n data?.Eta,\n data?.ETA,\n data?.EstimatedArrival,\n data?.EstimatedDelivery,\n get(data, 'ETA.Value')\n );\n\n if (etaDate) {\n etaAT = toAT(etaDate, etaTime);\n } else if (etaCombined) {\n etaAT = parseCombinedToAT(etaCombined);\n }\n }\n\n // Document / signature\n const documentName = first(\n data?.DocumentName,\n data?.Name,\n data?.SignedBy,\n ''\n );\n\n const signatureBase64 = first(\n data?.DocumentBase64,\n data?.SignatureBase64,\n data?.Signature,\n data?.PodBase64,\n ''\n );\n\n const signatureType = first(\n data?.DocumentType,\n data?.SignatureType,\n data?.PodType,\n 'POD'\n );\n\n const signatureFileTypeRaw = first(\n data?.DocumentFileType,\n data?.FileType,\n data?.SignatureFileType,\n data?.DocumentExtension,\n 'pdf'\n );\n\n const signatureFileType = String(signatureFileTypeRaw).replace(/^\\./, '').toLowerCase();\n\n const noteXml = additionalText\n ? `\\n ${xmlEsc(additionalText)}`\n : '';\n\n const etaXml = etaAT\n ? `\\n ${xmlEsc(etaAT)}`\n : '';\n\n const nameXml = documentName\n ? `\\n ${xmlEsc(documentName)}`\n : '';\n\n const signatureXml = signatureBase64\n ? `\\n ${xmlEsc(signatureBase64)}`\n : '';\n\n const xml = `\n\n ${xmlEsc(atDateTime)}\n TLN\n \n ${xmlEsc(consignmentNumber)}\n ${xmlEsc(mappedStatus)}\n ${xmlEsc(atDateTime)}${noteXml}${etaXml}${nameXml}${signatureXml}\n \n`;\n\n return {\n json: {\n consignmentNumber,\n customerReference: data?.CustomerReference ?? '',\n sourceStatus,\n mappedStatus,\n atDateTime,\n additionalText,\n originalAdditionalText,\n rdvDate,\n rdvDateDmy,\n etaAT,\n matchedEventCode: matchedEvent?.Codeevenement ?? '',\n matchedEventDate: matchedEvent?.Date ?? '',\n matchedEventTime: matchedEvent?.Heure ?? '',\n documentName,\n signatureType,\n signatureFileType,\n hasSignature: !!signatureBase64,\n xml,\n }\n };\n});" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1680, -272 ], "id": "b3a95298-1dad-43d7-8bf1-3d4ba54a30e3", "name": "Create XML" }, { "parameters": { "rules": { "values": [ { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "leftValue": "={{ $json.name }}", "rightValue": ".xml", "operator": { "type": "string", "operation": "contains" }, "id": "d8d8aa35-a3e1-4de0-a2ec-7e04c05f47cf" } ], "combinator": "and" }, "renameOutput": true, "outputKey": "XML" } ] }, "options": { "fallbackOutput": "extra", "renameFallbackOutput": "overige bestanden" } }, "type": "n8n-nodes-base.switch", "typeVersion": 3.4, "position": [ 480, -80 ], "id": "8d9f23c4-be58-475a-8df5-0ec1fd89c4fc", "name": "Switch" }, { "parameters": { "operation": "list", "options": {} }, "type": "n8n-nodes-base.ftp", "typeVersion": 1, "position": [ 80, -64 ], "id": "c3bf6ffb-ced1-41b7-8249-c28bb5418a48", "name": "List files", "credentials": { "ftp": { "id": "uephvxdWHFMhBKiZ", "name": "FTP Transport-info Mazet" } } }, { "parameters": { "path": "={{ $json.path }}", "options": {} }, "type": "n8n-nodes-base.ftp", "typeVersion": 1, "position": [ 272, -64 ], "id": "2a332b35-0315-4836-b63e-5ef810e86fa2", "name": "Download files", "credentials": { "ftp": { "id": "uephvxdWHFMhBKiZ", "name": "FTP Transport-info Mazet" } } }, { "parameters": { "operation": "delete", "path": "={{ $('Switch').item.json.path }}", "options": {} }, "type": "n8n-nodes-base.ftp", "typeVersion": 1, "position": [ 896, -368 ], "id": "e7c72f1c-cfb0-4951-afbc-a87f3920ceee", "name": "FTP", "credentials": { "ftp": { "id": "uephvxdWHFMhBKiZ", "name": "FTP Transport-info Mazet" } } }, { "parameters": { "operation": "upload", "path": "=/prod/ToTP/TLN/{{ $json.atDateTime }}-{{ $json.consignmentNumber }}.xml", "binaryData": false, "fileContent": "={{ $json.xml }}", "options": {} }, "type": "n8n-nodes-base.ftp", "typeVersion": 1, "position": [ 1904, -272 ], "id": "0bc52a13-edb9-41e6-820f-e1e6ca83ca6e", "name": "FTP1", "credentials": { "ftp": { "id": "oLAZ4OgmkOopMHAq", "name": "FTP De Wit Transport" } } }, { "parameters": { "operation": "upload", "path": "=/prod/ToTP/TLN/{{ $json.consignmentNumber }}.xml", "binaryData": false, "fileContent": "={{ $json.xml }}", "options": {} }, "type": "n8n-nodes-base.ftp", "typeVersion": 1, "position": [ 1104, 64 ], "id": "305018fd-4e19-4ffe-b8e8-051f16fb000e", "name": "FTP2", "credentials": { "ftp": { "id": "oLAZ4OgmkOopMHAq", "name": "FTP De Wit Transport" } } }, { "parameters": { "jsCode": "const { PDFDocument } = require('pdf-lib');\n\nconst items = $input.all();\nconst BIN_PROP = 'data';\n\nfunction replaceExtWithPdf(name) {\n const fileName = String(name || 'document');\n return /\\.[^.]+$/.test(fileName)\n ? fileName.replace(/\\.[^.]+$/, '') + '.pdf'\n : fileName + '.pdf';\n}\n\nfunction basename(path) {\n return String(path || '').split('/').pop();\n}\n\nfunction getExtension(name, mimeType = '') {\n const fileName = String(name || '').toLowerCase();\n const match = fileName.match(/\\.([^.]+)$/);\n\n if (match) {\n return match[1]; // zonder punt, dus: pdf, jpg, tif\n }\n\n const mime = String(mimeType || '').toLowerCase();\n\n if (mime === 'application/pdf') return 'pdf';\n if (mime === 'image/jpeg' || mime === 'image/jpg') return 'jpg';\n if (mime === 'image/png') return 'png';\n if (mime === 'image/tiff' || mime === 'image/tif') return 'tif';\n\n return '';\n}\n\nconst out = [];\n\nfor (let i = 0; i < items.length; i++) {\n const item = items[i];\n const bin = item.binary?.[BIN_PROP];\n if (!bin) continue;\n\n const originalName =\n bin.fileName ||\n item.json?.filename ||\n basename(item.json?.path) ||\n `file_${i + 1}`;\n\n const lowerName = originalName.toLowerCase();\n const mimeType = String(bin.mimeType || '').toLowerCase();\n const inputExtension = getExtension(originalName, mimeType);\n\n const isPdf =\n mimeType === 'application/pdf' || lowerName.endsWith('.pdf');\n\n const isJpg =\n mimeType === 'image/jpeg' ||\n mimeType === 'image/jpg' ||\n lowerName.endsWith('.jpg') ||\n lowerName.endsWith('.jpeg');\n\n const isPng =\n mimeType === 'image/png' || lowerName.endsWith('.png');\n\n const isTif =\n mimeType === 'image/tiff' ||\n mimeType === 'image/tif' ||\n lowerName.endsWith('.tif') ||\n lowerName.endsWith('.tiff');\n\n // TIF/TIFF niet omzetten, maar wel gewoon doorgeven\n if (isTif) {\n out.push({\n json: {\n ...item.json,\n filename: originalName,\n fileExtension: inputExtension,\n originalFileExtension: inputExtension,\n outputFileExtension: inputExtension,\n converted: false,\n passedThrough: true,\n },\n binary: {\n [BIN_PROP]: bin,\n },\n });\n continue;\n }\n\n // PDF ongewijzigd doorgeven\n if (isPdf) {\n out.push({\n json: {\n ...item.json,\n filename: originalName,\n fileExtension: inputExtension,\n originalFileExtension: inputExtension,\n outputFileExtension: inputExtension,\n converted: false,\n passedThrough: true,\n },\n binary: {\n [BIN_PROP]: bin,\n },\n });\n continue;\n }\n\n // Alleen JPG/PNG omzetten naar PDF\n if (!isJpg && !isPng) {\n continue;\n }\n\n try {\n const buf = await this.helpers.getBinaryDataBuffer(i, BIN_PROP);\n\n if (!buf || !buf.length) {\n throw new Error('Leeg bestand');\n }\n\n const pdfDoc = await PDFDocument.create();\n\n let image;\n if (isJpg) {\n image = await pdfDoc.embedJpg(buf);\n } else {\n image = await pdfDoc.embedPng(buf);\n }\n\n const size = image.size();\n const width = Number(size.width);\n const height = Number(size.height);\n\n if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {\n throw new Error(`Ongeldige afbeeldingsgrootte: width=${width}, height=${height}`);\n }\n\n const page = pdfDoc.addPage();\n page.setSize(width, height);\n\n page.drawImage(image, {\n x: 0,\n y: 0,\n width,\n height,\n });\n\n const pdfBytes = await pdfDoc.save();\n const outputFileName = replaceExtWithPdf(originalName);\n\n const outBin = await this.helpers.prepareBinaryData(\n Buffer.from(pdfBytes),\n outputFileName,\n 'application/pdf'\n );\n\n out.push({\n json: {\n ...item.json,\n filename: outputFileName,\n originalFilename: originalName,\n fileExtension: 'pdf',\n originalFileExtension: inputExtension,\n outputFileExtension: 'pdf',\n converted: true,\n passedThrough: false,\n },\n binary: {\n [BIN_PROP]: outBin,\n },\n });\n } catch (err) {\n out.push({\n json: {\n ...item.json,\n filename: originalName,\n fileExtension: inputExtension,\n originalFileExtension: inputExtension,\n outputFileExtension: inputExtension,\n converted: false,\n passedThrough: false,\n error: err.message || String(err),\n },\n });\n }\n}\n\nreturn out;" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 720, 64 ], "id": "7f48755c-97ac-4f1e-9603-a9a72f45ab4f", "name": "image to pdf1" }, { "parameters": { "operation": "delete", "path": "={{ $json.path }}", "options": {} }, "type": "n8n-nodes-base.ftp", "typeVersion": 1, "position": [ 1296, 64 ], "id": "0fa1334d-5a29-4d0f-816a-ce2be2b84989", "name": "FTP3", "credentials": { "ftp": { "id": "uephvxdWHFMhBKiZ", "name": "FTP Transport-info Mazet" } } }, { "parameters": { "jsCode": "// n8n Code node\n// Zet op: Run once for all items\n\nconst xmlEsc = (s) =>\n String(s ?? '')\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n\nconst first = (...vals) =>\n vals.find(v => v !== undefined && v !== null && String(v).trim() !== '');\n\nconst get = (obj, path) =>\n path.split('.').reduce((a, k) => (a && a[k] !== undefined ? a[k] : undefined), obj);\n\nfunction basename(path) {\n return String(path || '').split('/').pop();\n}\n\nfunction stripExtension(name) {\n return String(name || '').replace(/\\.[^.]+$/, '');\n}\n\nfunction toAT(dateStr, timeStr) {\n const d = String(dateStr ?? '').trim();\n const t = String(timeStr ?? '').trim();\n\n const dm = d.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{4})$/);\n const tm = t.match(/^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/);\n\n if (!dm) {\n throw new Error(`Ongeldige datum: \"${d}\"`);\n }\n\n const day = dm[1];\n const month = dm[2];\n const year = dm[3];\n\n const hour = tm ? tm[1] : '00';\n const minute = tm ? tm[2] : '00';\n const second = tm && tm[3] ? tm[3] : '00';\n\n return `${year}${month}${day}${hour}${minute}${second}`;\n}\n\nfunction nowAT() {\n const d = new Date();\n const pad = (n) => String(n).padStart(2, '0');\n return (\n d.getFullYear() +\n pad(d.getMonth() + 1) +\n pad(d.getDate()) +\n pad(d.getHours()) +\n pad(d.getMinutes()) +\n pad(d.getSeconds())\n );\n}\n\nfunction parseCombinedToAT(value) {\n const s = String(value ?? '').trim();\n if (!s) return '';\n\n if (/^\\d{14}$/.test(s)) return s;\n\n let m = s.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{4})(?:\\s+(\\d{2}):(\\d{2})(?::(\\d{2}))?)?$/);\n if (m) {\n const [, day, month, year, hh = '00', mm = '00', ss = '00'] = m;\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n m = s.match(/^(\\d{4})-(\\d{2})-(\\d{2})(?:[T\\s](\\d{2}):(\\d{2})(?::(\\d{2}))?)?$/);\n if (m) {\n const [, year, month, day, hh = '00', mm = '00', ss = '00'] = m;\n return `${year}${month}${day}${hh}${mm}${ss}`;\n }\n\n const digits = s.replace(/\\D/g, '');\n if (digits.length >= 14) return digits.slice(0, 14);\n\n return '';\n}\n\nconst inputItems = $input.all();\nconst out = [];\n\nfor (let index = 0; index < inputItems.length; index++) {\n const item = inputItems[index];\n const row = item.json ?? {};\n const data = row?.TrackingResponse?.Data ?? row?.Data ?? row;\n\n const documentName = first(\n row?.filename,\n row?.name,\n item.binary?.data?.fileName,\n basename(row?.path),\n ''\n );\n\n const consignmentNumber = first(\n data?.ConsignmentNumber,\n stripExtension(row?.filename),\n stripExtension(row?.name),\n stripExtension(item.binary?.data?.fileName),\n stripExtension(basename(row?.path))\n );\n\n if (!consignmentNumber) {\n throw new Error(`Item ${index + 1}: consignmentNumber kon niet uit bestandsnaam worden gehaald`);\n }\n\n const sourceStatus = first(\n data?.CurrentStatus,\n row?.CurrentStatus,\n row?.status,\n '900'\n );\n\n const date = first(data?.Date, row?.Date, '');\n const time = first(data?.Time, row?.Time, '');\n const atDateTime = date ? toAT(date, time) : nowAT();\n\n const additionalText = first(\n data?.AdditionalText,\n row?.AdditionalText,\n ''\n );\n\n const etaDate = first(\n data?.EtaDate,\n data?.ETADate,\n data?.EstimatedDeliveryDate,\n data?.EstimatedArrivalDate,\n row?.EtaDate,\n get(data, 'ETA.Date')\n );\n\n const etaTime = first(\n data?.EtaTime,\n data?.ETATime,\n data?.EstimatedDeliveryTime,\n data?.EstimatedArrivalTime,\n row?.EtaTime,\n get(data, 'ETA.Time')\n );\n\n const etaCombined = first(\n data?.Eta,\n data?.ETA,\n data?.EstimatedArrival,\n data?.EstimatedDelivery,\n row?.Eta,\n get(data, 'ETA.Value')\n );\n\n let etaAT = '';\n if (etaDate) {\n etaAT = toAT(etaDate, etaTime);\n } else if (etaCombined) {\n etaAT = parseCombinedToAT(etaCombined);\n }\n\n // HIER zit de echte fix:\n let signatureBase64 = '';\n try {\n const buf = await this.helpers.getBinaryDataBuffer(index, 'data');\n if (buf?.length) {\n signatureBase64 = buf.toString('base64');\n }\n } catch (e) {\n signatureBase64 = '';\n }\n\n const signatureType = 'Pod-Mazet';\n\nlet signatureFileType = String(\n first(\n row?.fileExtension,\n row?.outputFileExtension,\n row?.originalFileExtension,\n 'pdf'\n )\n)\n .toLowerCase()\n .replace(/^\\./, '');\n\nif (signatureFileType === 'tiff') {\n signatureFileType = 'tif';\n}\n\n const noteXml = additionalText\n ? `\\n ${xmlEsc(additionalText)}`\n : '';\n\n const etaXml = etaAT\n ? `\\n ${xmlEsc(etaAT)}`\n : '';\n\n const nameXml = documentName\n ? `\\n ${xmlEsc(documentName)}`\n : '';\n\n const signatureXml = signatureBase64\n ? `\\n ${signatureBase64}`\n : '';\n\n const xml = `\n\n ${xmlEsc(atDateTime)}\n TLN\n \n ${xmlEsc(consignmentNumber)}\n ${xmlEsc(sourceStatus)}\n ${xmlEsc(atDateTime)}${noteXml}${etaXml}${nameXml}${signatureXml}\n \n`;\n\n out.push({\n json: {\n ...row,\n consignmentNumber,\n documentName,\n sourceStatus,\n atDateTime,\n additionalText,\n etaAT,\n signatureType,\n signatureFileType,\n hasSignature: !!signatureBase64,\n signatureLength: signatureBase64.length,\n xml,\n }\n });\n}\n\nreturn out;" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 912, 64 ], "id": "202f257b-8bec-41ed-bf01-7d092ffb772f", "name": "Create XML1" }, { "parameters": { "rule": { "interval": [ { "field": "minutes", "minutesInterval": 15 } ] } }, "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1.3, "position": [ -112, -64 ], "id": "a5c5aabf-40e0-407b-8ccb-0d45da809389", "name": "Schedule Trigger" }, { "parameters": { "jsCode": "// n8n Code node\n// Zet op: Run once for all items\n\nfunction getBetween(text, start, end) {\n const s = String(text || '');\n const startIndex = s.indexOf(start);\n if (startIndex === -1) return '';\n const from = startIndex + start.length;\n const endIndex = s.indexOf(end, from);\n if (endIndex === -1) return s.slice(from);\n return s.slice(from, endIndex);\n}\n\nfunction decodeBase64(value) {\n try {\n return Buffer.from(String(value || ''), 'base64').toString('utf8');\n } catch (e) {\n return '';\n }\n}\n\nconst inputItems = $input.all();\n\nconst rows = inputItems.flatMap(item => {\n if (Array.isArray(item.json)) return item.json;\n return [item.json];\n});\n\nreturn rows.map((row, index) => {\n const out = JSON.parse(JSON.stringify(row));\n const data = out?.TrackingResponse?.Data ?? out?.Data ?? out;\n\n const trackingUrl = String(data?.TrackingUrl || '').trim();\n\n const encodedPart =\n getBetween(trackingUrl, 'idref=', '&lang=') ||\n getBetween(trackingUrl, 'idref=', '&');\n\n const decodedPart = decodeBase64(encodedPart);\n const parts = decodedPart.split('||');\n\n data.TrackingUrlEncodedPart = encodedPart;\n data.TrackingUrlDecodedPart = decodedPart;\n data.Code1 = parts[0] || '';\n data.Code2 = parts[1] || '';\n data.Code3 = parts[2] || '';\n\n if (!data.Code1 || !data.Code2 || !data.Code3) {\n throw new Error(\n `Item ${index + 1}: TrackingUrl kon niet correct worden gesplitst. Decoded: \"${decodedPart}\"`\n );\n }\n\n return { json: out };\n});" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1040, -192 ], "id": "0e392330-b62c-46da-9c1e-1d60d097101d", "name": "Prepare Mazet Request" }, { "parameters": { "url": "={{ 'https://extranet.mazet.fr/ws/v1/mes/getTracking?token=f905430afa7abd9480b660faaba8411f09097f75&agence=' + $json.TrackingResponse.Data.Code1 + '&client=' + $json.TrackingResponse.Data.Code2 + '&reference=' + encodeURIComponent($json.TrackingResponse.Data.Code3) }}", "options": { "response": { "response": { "fullResponse": true, "responseFormat": "json" } } } }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.3, "position": [ 1264, -160 ], "id": "55fffad6-418e-423a-877c-327a853d85eb", "name": "HTTP Request" }, { "parameters": { "mode": "combine", "combineBy": "combineByPosition", "options": {} }, "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ 1472, -272 ], "id": "c3794325-9836-4a92-9e4c-c813ab1beca7", "name": "Merge" }, { "parameters": { "jsCode": "const { PDFDocument } = require('pdf-lib');\n\nconst items = $input.all();\nconst BIN_PROP = 'data';\n\nfunction replaceExtWithPdf(name) {\n const fileName = String(name || 'document');\n return /\\.[^.]+$/.test(fileName)\n ? fileName.replace(/\\.[^.]+$/, '') + '.pdf'\n : fileName + '.pdf';\n}\n\nfunction basename(path) {\n return String(path || '').split('/').pop();\n}\n\nconst out = [];\n\nfor (let i = 0; i < items.length; i++) {\n const item = items[i];\n const bin = item.binary?.[BIN_PROP];\n if (!bin) continue;\n\n const originalName =\n bin.fileName ||\n item.json?.filename ||\n basename(item.json?.path) ||\n `file_${i + 1}`;\n\n const lowerName = originalName.toLowerCase();\n const mimeType = String(bin.mimeType || '').toLowerCase();\n\n const isPdf =\n mimeType === 'application/pdf' || lowerName.endsWith('.pdf');\n\n const isJpg =\n mimeType === 'image/jpeg' ||\n mimeType === 'image/jpg' ||\n lowerName.endsWith('.jpg') ||\n lowerName.endsWith('.jpeg');\n\n const isPng =\n mimeType === 'image/png' || lowerName.endsWith('.png');\n\n const isTif =\n mimeType === 'image/tiff' ||\n mimeType === 'image/tif' ||\n lowerName.endsWith('.tif') ||\n lowerName.endsWith('.tiff');\n\n // tif overslaan\n if (isTif) {\n continue;\n }\n\n // pdf ongewijzigd doorgeven\n if (isPdf) {\n out.push({\n json: {\n ...item.json,\n filename: originalName,\n },\n binary: {\n [BIN_PROP]: bin,\n },\n });\n continue;\n }\n\n // alleen jpg/png omzetten\n if (!isJpg && !isPng) {\n continue;\n }\n\n try {\n const buf = await this.helpers.getBinaryDataBuffer(i, BIN_PROP);\n\n if (!buf || !buf.length) {\n throw new Error('Leeg bestand');\n }\n\n const pdfDoc = await PDFDocument.create();\n\n let image;\n if (isJpg) {\n image = await pdfDoc.embedJpg(buf);\n } else {\n image = await pdfDoc.embedPng(buf);\n }\n\n // Gebruik size() uit pdf-lib\n const size = image.size();\n const width = Number(size.width);\n const height = Number(size.height);\n\n if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {\n throw new Error(`Ongeldige afbeeldingsgrootte: width=${width}, height=${height}`);\n }\n\n const page = pdfDoc.addPage();\n page.setSize(width, height);\n\n page.drawImage(image, {\n x: 0,\n y: 0,\n width,\n height,\n });\n\n const pdfBytes = await pdfDoc.save();\n const outputFileName = replaceExtWithPdf(originalName);\n\n const outBin = await this.helpers.prepareBinaryData(\n Buffer.from(pdfBytes),\n outputFileName,\n 'application/pdf'\n );\n\n out.push({\n json: {\n ...item.json,\n filename: outputFileName,\n converted: true,\n },\n binary: {\n [BIN_PROP]: outBin,\n },\n });\n } catch (err) {\n out.push({\n json: {\n ...item.json,\n filename: originalName,\n converted: false,\n error: err.message || String(err),\n },\n });\n }\n}\n\nreturn out;" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 720, 240 ], "id": "9bc5f6e9-a3bf-456b-8e40-7c5d16966cfb", "name": "image to pdf" } ], "pinData": {}, "connections": { "Extract from File1": { "main": [ [ { "node": "XML", "type": "main", "index": 0 }, { "node": "FTP", "type": "main", "index": 0 } ] ] }, "XML": { "main": [ [ { "node": "Prepare Mazet Request", "type": "main", "index": 0 } ] ] }, "Switch": { "main": [ [ { "node": "Extract from File1", "type": "main", "index": 0 } ], [ { "node": "image to pdf1", "type": "main", "index": 0 } ] ] }, "List files": { "main": [ [ { "node": "Download files", "type": "main", "index": 0 } ] ] }, "Download files": { "main": [ [ { "node": "Switch", "type": "main", "index": 0 } ] ] }, "Create XML": { "main": [ [ { "node": "FTP1", "type": "main", "index": 0 } ] ] }, "FTP2": { "main": [ [ { "node": "FTP3", "type": "main", "index": 0 } ] ] }, "image to pdf1": { "main": [ [ { "node": "Create XML1", "type": "main", "index": 0 } ] ] }, "Create XML1": { "main": [ [ { "node": "FTP2", "type": "main", "index": 0 } ] ] }, "Schedule Trigger": { "main": [ [ { "node": "List files", "type": "main", "index": 0 } ] ] }, "Prepare Mazet Request": { "main": [ [ { "node": "HTTP Request", "type": "main", "index": 0 }, { "node": "Merge", "type": "main", "index": 0 } ] ] }, "HTTP Request": { "main": [ [ { "node": "Merge", "type": "main", "index": 1 } ] ] }, "Merge": { "main": [ [ { "node": "Create XML", "type": "main", "index": 0 } ] ] } }, "active": true, "settings": { "executionOrder": "v1", "binaryMode": "separate", "availableInMCP": false, "timeSavedMode": "fixed", "errorWorkflow": "1Ps5lukDf2sgcGL7", "callerPolicy": "workflowsFromSameOwner" }, "versionId": "020ceed8-44e0-47dd-9b69-3d502d0a7f48", "meta": { "templateCredsSetupCompleted": true, "instanceId": "bef8d409866a58c0777dfe7cca1b9c2400fd051c056d361501393ab423006b5f" }, "nodeGroups": [], "id": "Fz6CG-Lw-TCfvnTZStTWo", "tags": [] }