Files
n8n-workflows/Mazet status --_ TLN.json
T

505 lines
33 KiB
JSON

{
"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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\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 <note>${xmlEsc(additionalText)}</note>`\n : '';\n\n const etaXml = etaAT\n ? `\\n <eta>${xmlEsc(etaAT)}</eta>`\n : '';\n\n const nameXml = documentName\n ? `\\n <name>${xmlEsc(documentName)}</name>`\n : '';\n\n const signatureXml = signatureBase64\n ? `\\n <signature type=\"${xmlEsc(signatureType)}\" filetype=\"${xmlEsc(signatureFileType)}\">${xmlEsc(signatureBase64)}</signature>`\n : '';\n\n const xml = `<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<xmlStat xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n <dateTime type=\"AT\">${xmlEsc(atDateTime)}</dateTime>\n <codeList>TLN</codeList>\n <consignmentStat xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n <id type=\"NOC\" datatype=\"varchar(50)\">${xmlEsc(consignmentNumber)}</id>\n <status>${xmlEsc(mappedStatus)}</status>\n <dateTime type=\"AT\">${xmlEsc(atDateTime)}</dateTime>${noteXml}${etaXml}${nameXml}${signatureXml}\n </consignmentStat>\n</xmlStat>`;\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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\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 <note>${xmlEsc(additionalText)}</note>`\n : '';\n\n const etaXml = etaAT\n ? `\\n <eta>${xmlEsc(etaAT)}</eta>`\n : '';\n\n const nameXml = documentName\n ? `\\n <name>${xmlEsc(documentName)}</name>`\n : '';\n\n const signatureXml = signatureBase64\n ? `\\n <signature type=\"${xmlEsc(signatureType)}\" filetype=\"${xmlEsc(signatureFileType)}\">${signatureBase64}</signature>`\n : '';\n\n const xml = `<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<xmlStat xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n <dateTime type=\"AT\">${xmlEsc(atDateTime)}</dateTime>\n <codeList>TLN</codeList>\n <consignmentStat xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n <id type=\"NOC\" datatype=\"varchar(50)\">${xmlEsc(consignmentNumber)}</id>\n <status>${xmlEsc(sourceStatus)}</status>\n <dateTime type=\"AT\">${xmlEsc(atDateTime)}</dateTime>${noteXml}${etaXml}${nameXml}${signatureXml}\n </consignmentStat>\n</xmlStat>`;\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": []
}