From 17dbfda67e650f157c576c96655b0203f286485c Mon Sep 17 00:00:00 2001 From: adminjeroen Date: Fri, 3 Jul 2026 11:19:42 +0200 Subject: [PATCH] Exporteert de status van Mazet naar het TLN formaat van Transpas --- Mazet status --_ TLN.json | 505 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 505 insertions(+) create mode 100644 Mazet status --_ TLN.json diff --git a/Mazet status --_ TLN.json b/Mazet status --_ TLN.json new file mode 100644 index 0000000..6ff30e5 --- /dev/null +++ b/Mazet status --_ TLN.json @@ -0,0 +1,505 @@ +{ + "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": [] +} \ No newline at end of file