Files
n8n-workflows/Flower_direct/Lidl Frankrijk - Pakbon naar NAS, De Wit FTP en Transpas.json
T

700 lines
34 KiB
JSON

{
"name": "Lidl Frankrijk - Pakbon naar NAS, De Wit FTP en Transpas",
"nodes": [
{
"parameters": {
"value": "={{ $json.base64_data }}",
"dataPropertyName": "document_hash"
},
"type": "n8n-nodes-base.crypto",
"typeVersion": 2,
"position": [
-2912,
192
],
"id": "7d05ed50-0189-4805-8888-eb096376006f",
"name": "Bereken SHA-256"
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Bepaalt de fysieke opslagpaden.\n// Het originele bestand blijft binary data en wordt niet in PostgreSQL opgeslagen.\n\nconst inputItem = $input.item;\nconst j = inputItem.json;\n\n// NAS-opslag per jaar/maand.\nconst STORAGE_ROOT = '/files/lidl/pakbonnen';\n\n// FTP blijft één platte map.\nconst FTP_ROOT = '/flower direct/Pakbonnen';\n\nfunction safeFileName(value) {\n const fileName = String(value ?? '')\n .replace(/^.*[\\\\/]/, '') // Verwijder eventueel meegestuurd pad\n .trim();\n\n return fileName || 'document.bin';\n}\n\nfunction getAmsterdamParts(value) {\n let date = new Date(value);\n\n if (Number.isNaN(date.getTime())) {\n date = new Date();\n }\n\n const parts = new Intl.DateTimeFormat('en-GB', {\n timeZone: 'Europe/Amsterdam',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hourCycle: 'h23',\n }).formatToParts(date);\n\n return Object.fromEntries(\n parts.map((part) => [part.type, part.value]),\n );\n}\n\nconst parts = getAmsterdamParts(j.received_at);\n\nconst year = parts.year;\nconst month = parts.month;\n\n// Originele bestandsnaam behouden.\nconst storedFileName = safeFileName(j.file_name);\n\n// NAS: per jaar/maand.\nconst storageDirectory = `${STORAGE_ROOT}/${year}/${month}`;\nconst filePath = `${storageDirectory}/${storedFileName}`;\n\n// FTP: één bestaande platte map.\nconst dewitFtpDirectory = FTP_ROOT;\nconst dewitFtpPath = `${FTP_ROOT}/${storedFileName}`;\n\n// Eventuele base64 niet in de JSON laten staan.\nconst { base64_data, ...metadata } = j;\n\nreturn {\n json: {\n ...metadata,\n\n storage_year: Number(year),\n storage_month: Number(month),\n\n storage_directory: storageDirectory,\n stored_file_name: storedFileName,\n file_path: filePath,\n\n dewit_ftp_directory: dewitFtpDirectory,\n dewit_ftp_path: dewitFtpPath,\n },\n\n // Binary bestand behouden voor NAS- en FTP-nodes.\n binary: inputItem.binary,\n};"
},
"id": "02ea9e6c-9b28-42bc-b8a2-46bf25c38fa0",
"name": "Bepaal NAS- en De Wit FTP-pad",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-2688,
192
]
},
{
"parameters": {
"operation": "write",
"fileName": "={{ $json.file_path }}",
"options": {}
},
"id": "3b3385ef-fb8d-4f73-bbab-5cad0bfb9be0",
"name": "Schrijf pakbon naar NAS",
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1.1,
"position": [
-2016,
192
]
},
{
"parameters": {
"operation": "upload",
"path": "={{ $json.dewit_ftp_path }}",
"options": {}
},
"id": "ce7c3363-e733-4f1e-ad5c-83606029d567",
"name": "Upload originele pakbon naar De Wit FTP",
"type": "n8n-nodes-base.ftp",
"typeVersion": 1,
"position": [
-1792,
192
],
"credentials": {
"ftp": {
"id": "oLAZ4OgmkOopMHAq",
"name": "FTP De Wit Transport"
}
},
"onError": "continueErrorOutput"
},
{
"parameters": {
"operation": "executeQuery",
"query": "WITH input AS (SELECT $1::jsonb AS j),\ndeactivated AS (\n UPDATE public.lidl_fr_documents d SET active=false, updated_at=now()\n FROM input WHERE d.transport_number=j->>'transport_number'\n AND d.document_kind=COALESCE(j->>'document_kind','PAKBON')\n AND d.document_hash IS DISTINCT FROM j->>'document_hash' AND d.active=true RETURNING d.id\n), stored AS (\n INSERT INTO public.lidl_fr_documents AS existing (\n transport_number,document_kind,document_hash,file_name,stored_file_name,file_path,\n storage_year,storage_month,file_type,mime_type,file_size_bytes,received_at,\n source_message_id,source_subject,source_from,storage_status,active,\n dewit_ftp_path,dewit_ftp_status,dewit_ftp_uploaded_at,dewit_ftp_error,transpas_delivery_status\n ) SELECT j->>'transport_number',COALESCE(j->>'document_kind','PAKBON'),j->>'document_hash',\n j->>'file_name',j->>'stored_file_name',j->>'file_path',NULLIF(j->>'storage_year','')::int,\n NULLIF(j->>'storage_month','')::int,j->>'file_type',j->>'mime_type',NULLIF(j->>'file_size_bytes','')::bigint,\n COALESCE(NULLIF(j->>'received_at','')::timestamptz,now()),j->>'source_message_id',j->>'source_subject',j->>'source_from',\n 'stored',true,j->>'dewit_ftp_path','uploaded',now(),NULL,'checking_order'\n FROM input\n ON CONFLICT (transport_number,document_hash) DO UPDATE SET\n file_name=EXCLUDED.file_name,stored_file_name=EXCLUDED.stored_file_name,file_path=EXCLUDED.file_path,\n storage_year=EXCLUDED.storage_year,storage_month=EXCLUDED.storage_month,file_type=EXCLUDED.file_type,\n mime_type=EXCLUDED.mime_type,file_size_bytes=EXCLUDED.file_size_bytes,last_received_at=now(),\n source_message_id=EXCLUDED.source_message_id,source_subject=EXCLUDED.source_subject,source_from=EXCLUDED.source_from,\n storage_status='stored',storage_error=NULL,active=true,dewit_ftp_path=EXCLUDED.dewit_ftp_path,\n dewit_ftp_status='uploaded',dewit_ftp_uploaded_at=now(),dewit_ftp_error=NULL,transpas_delivery_status='checking_order',updated_at=now()\n RETURNING id\n)\nSELECT j->>'transport_number' transport_number,j->>'file_name' file_name,j->>'stored_file_name' stored_file_name,\n j->>'file_path' file_path,j->>'document_hash' document_hash,j->>'file_type' file_type,j->>'mime_type' mime_type,\n j->>'dewit_ftp_path' dewit_ftp_path,stored.id document_id FROM input,stored;",
"options": {
"queryReplacement": "={{ [JSON.stringify($('Bepaal NAS- en De Wit FTP-pad').item.json)] }}"
}
},
"id": "6d1247ea-d5e1-4e9f-894a-96c63e05ccff",
"name": "Sla metadata op - De Wit FTP gelukt",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
-1344,
96
],
"credentials": {
"postgres": {
"id": "m4DQjg3b1iDYGlBp",
"name": "Postgres Flower direct"
}
}
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const original = $('Bepaal NAS- en De Wit FTP-pad').item.json;\nconst error = $json.error ?? $json;\nconst errorText = error?.message ?? error?.description ?? JSON.stringify(error);\nreturn { json: { ...original, dewit_ftp_error: String(errorText).slice(0, 4000) } };"
},
"id": "ba32eab6-e45d-4146-be76-36e620641b08",
"name": "Maak De Wit FTP foutmelding",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1568,
288
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "WITH input AS (SELECT $1::jsonb AS j),\ndeactivated AS (\n UPDATE public.lidl_fr_documents d SET active=false, updated_at=now()\n FROM input WHERE d.transport_number=j->>'transport_number'\n AND d.document_kind=COALESCE(j->>'document_kind','PAKBON')\n AND d.document_hash IS DISTINCT FROM j->>'document_hash' AND d.active=true RETURNING d.id\n), stored AS (\n INSERT INTO public.lidl_fr_documents AS existing (\n transport_number,document_kind,document_hash,file_name,stored_file_name,file_path,\n storage_year,storage_month,file_type,mime_type,file_size_bytes,received_at,\n source_message_id,source_subject,source_from,storage_status,active,\n dewit_ftp_path,dewit_ftp_status,dewit_ftp_uploaded_at,dewit_ftp_error,transpas_delivery_status\n ) SELECT j->>'transport_number',COALESCE(j->>'document_kind','PAKBON'),j->>'document_hash',\n j->>'file_name',j->>'stored_file_name',j->>'file_path',NULLIF(j->>'storage_year','')::int,\n NULLIF(j->>'storage_month','')::int,j->>'file_type',j->>'mime_type',NULLIF(j->>'file_size_bytes','')::bigint,\n COALESCE(NULLIF(j->>'received_at','')::timestamptz,now()),j->>'source_message_id',j->>'source_subject',j->>'source_from',\n 'stored',true,j->>'dewit_ftp_path','error',NULL,j->>'dewit_ftp_error','checking_order'\n FROM input\n ON CONFLICT (transport_number,document_hash) DO UPDATE SET\n file_name=EXCLUDED.file_name,stored_file_name=EXCLUDED.stored_file_name,file_path=EXCLUDED.file_path,\n storage_year=EXCLUDED.storage_year,storage_month=EXCLUDED.storage_month,file_type=EXCLUDED.file_type,\n mime_type=EXCLUDED.mime_type,file_size_bytes=EXCLUDED.file_size_bytes,last_received_at=now(),\n source_message_id=EXCLUDED.source_message_id,source_subject=EXCLUDED.source_subject,source_from=EXCLUDED.source_from,\n storage_status='stored',storage_error=NULL,active=true,dewit_ftp_path=EXCLUDED.dewit_ftp_path,\n dewit_ftp_status='error',dewit_ftp_uploaded_at=existing.dewit_ftp_uploaded_at,dewit_ftp_error=EXCLUDED.dewit_ftp_error,transpas_delivery_status='checking_order',updated_at=now()\n RETURNING id\n)\nSELECT j->>'transport_number' transport_number,j->>'file_name' file_name,j->>'stored_file_name' stored_file_name,\n j->>'file_path' file_path,j->>'document_hash' document_hash,j->>'file_type' file_type,j->>'mime_type' mime_type,\n j->>'dewit_ftp_path' dewit_ftp_path,stored.id document_id FROM input,stored;",
"options": {
"queryReplacement": "={{ [JSON.stringify($json)] }}"
}
},
"id": "81dd85e3-6bdb-4182-b66f-5027b45fe3ef",
"name": "Sla metadata op - De Wit FTP fout",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
-1344,
288
],
"credentials": {
"postgres": {
"id": "m4DQjg3b1iDYGlBp",
"name": "Postgres Flower direct"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "WITH input AS (SELECT $1::jsonb AS j)\nSELECT\n j->>'transport_number' AS transport_number,j->>'file_name' AS file_name,j->>'stored_file_name' AS stored_file_name,\n j->>'file_path' AS file_path,j->>'document_hash' AS document_hash,j->>'file_type' AS file_type,j->>'mime_type' AS mime_type,\n j->>'dewit_ftp_path' AS dewit_ftp_path,(j->>'document_id')::bigint AS document_id,\n o.id AS order_id,o.revision,o.current_status,o.is_cancelled,o.last_transpas_export_at,o.last_transpas_export_action,\n CASE WHEN o.id IS NOT NULL AND o.is_cancelled=false AND o.has_active_export=true\n AND o.last_transpas_export_at IS NOT NULL AND o.last_transpas_export_revision=o.revision\n AND COALESCE(o.last_transpas_export_action,'') <> 'DELETE'\n THEN true ELSE false END AS order_already_in_transpas\nFROM input\nLEFT JOIN public.lidl_fr_orders o ON o.transport_number=j->>'transport_number';",
"options": {
"queryReplacement": "={{ [JSON.stringify($json)] }}"
}
},
"id": "fa6800f2-832d-457b-9a7b-60617bfe3301",
"name": "Controleer of order al in Transpas staat",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
-1120,
192
],
"credentials": {
"postgres": {
"id": "m4DQjg3b1iDYGlBp",
"name": "Postgres Flower direct"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"combinator": "and",
"conditions": [
{
"id": "2de18530-fab8-4487-b233-de44fb6fbfc3",
"leftValue": "={{ $json.order_already_in_transpas }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
]
},
"options": {}
},
"id": "52d05f6a-3571-46ea-875f-dbdcaa567c32",
"name": "Order al geïmporteerd in Transpas?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-896,
192
]
},
{
"parameters": {
"fileSelector": "={{ $json.file_path }}",
"options": {
"fileName": "={{ $json.file_name }}",
"mimeType": "={{ $json.mime_type }}",
"dataPropertyName": "data"
}
},
"id": "202d35de-eeac-479c-820c-40d60ee5b909",
"name": "Lees pakbon van NAS",
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1.1,
"position": [
-672,
96
]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const STATUS_CODE = 'PAKBON';\nconst DOCUMENT_TYPE = 'Paklijst - Pakbon';\nconst TLN_FTP_ROOT = '/prod/ToTP/TLN';\n\n// De NAS-leesnode geeft vooral bestandsmetadata terug.\n// Daarom halen we de oorspronkelijke documentgegevens opnieuw op.\nconst sourceData = $('Controleer of order al in Transpas staat').item.json;\n\nconst j = {\n ...sourceData,\n ...$json,\n};\n\nconst buffer = await this.helpers.getBinaryDataBuffer(0, 'data');\n\nif (!buffer?.length) {\n throw new Error(\n 'Pakbonbestand kon niet vanaf de NAS worden gelezen.',\n );\n}\n\nif (!j.transport_number) {\n throw new Error(\n 'Transportnummer ontbreekt in de gegevens voor het Transpas-documentbericht.',\n );\n}\n\nfunction xmlEsc(value) {\n return String(value ?? '')\n .replace(\n /&(?!amp;|lt;|gt;|quot;|apos;|#\\d+;|#x[0-9A-Fa-f]+;)/g,\n '&amp;',\n )\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n\nfunction safe(value) {\n return String(value ?? '')\n .normalize('NFKD')\n .replace(/[^a-zA-Z0-9._-]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .slice(0, 120) || 'document';\n}\n\nconst now = new Date();\n\nconst parts = new Intl.DateTimeFormat('en-GB', {\n timeZone: 'Europe/Amsterdam',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hourCycle: 'h23',\n}).formatToParts(now);\n\nconst p = Object.fromEntries(\n parts\n .filter(part => part.type !== 'literal')\n .map(part => [part.type, part.value]),\n);\n\nconst at =\n `${p.year}` +\n `${p.month}` +\n `${p.day}` +\n `${p.hour}` +\n `${p.minute}` +\n `${p.second}`;\n\nconst originalFileName =\n j.file_name ??\n j.fileName ??\n j.stored_file_name ??\n 'pakbon.pdf';\n\nconst ext = String(\n j.file_type ??\n j.fileExtension ??\n originalFileName.split('.').pop() ??\n 'pdf',\n)\n .replace(/^\\./, '')\n .toLowerCase();\n\nconst base64 = buffer.toString('base64');\n\nconst xml = [\n '<?xml version=\"1.0\" encoding=\"utf-8\"?>',\n '<xmlStat xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">',\n ` <dateTime type=\"AT\">${at}</dateTime>`,\n ' <codeList>TLN</codeList>',\n ' <consignmentStat xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">',\n ` <id type=\"NOC\" datatype=\"varchar(50)\">${xmlEsc(j.transport_number)}</id>`,\n ` <status>${STATUS_CODE}</status>`,\n ` <dateTime type=\"AT\">${at}</dateTime>`,\n ` <note>${xmlEsc(`Pakbon ontvangen per e-mail: ${originalFileName}`)}</note>`,\n ` <name>${xmlEsc(originalFileName)}</name>`,\n ` <signature type=\"${xmlEsc(DOCUMENT_TYPE)}\" filetype=\"${xmlEsc(ext)}\">${base64}</signature>`,\n ' </consignmentStat>',\n '</xmlStat>',\n].join('\\n');\n\nconst documentHashPart = String(\n j.document_hash ?? '',\n).slice(0, 16);\n\nconst xmlFileName = [\n at,\n safe(j.transport_number),\n documentHashPart || 'document',\n].join('-') + '.xml';\n\nreturn {\n json: {\n ...j,\n file_name: originalFileName,\n file_type: ext,\n xml_file_name: xmlFileName,\n transpas_ftp_path: `${TLN_FTP_ROOT}/${xmlFileName}`,\n xml,\n },\n};"
},
"id": "17829fb0-03dd-47bf-879e-2503b6d2143d",
"name": "Bouw Transpas documentbericht",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-448,
96
]
},
{
"parameters": {
"operation": "upload",
"path": "={{ $json.transpas_ftp_path }}",
"binaryData": false,
"fileContent": "={{ $json.xml }}",
"options": {}
},
"id": "1bd8fa28-f902-4cd5-909d-ed2e900210f0",
"name": "Upload pakbonbericht naar Transpas FTP",
"type": "n8n-nodes-base.ftp",
"typeVersion": 1,
"position": [
-224,
96
],
"credentials": {
"ftp": {
"id": "oLAZ4OgmkOopMHAq",
"name": "FTP De Wit Transport"
}
},
"onError": "continueErrorOutput"
},
{
"parameters": {
"operation": "executeQuery",
"query": "WITH input AS (SELECT $1::jsonb AS j)\nUPDATE public.lidl_fr_documents d SET transpas_sent_at=now(),transpas_xml_file=j->>'xml_file_name',transpas_error=NULL,\n transpas_delivery_status='sent_separately',waiting_for_order_since=NULL,updated_at=now()\nFROM input WHERE d.id=(j->>'document_id')::bigint\nRETURNING d.id,d.transport_number,d.file_path,d.dewit_ftp_path,d.transpas_sent_at,d.transpas_delivery_status;",
"options": {
"queryReplacement": "={{ [JSON.stringify($('Bouw Transpas documentbericht').item.json)] }}"
}
},
"id": "f02632e9-ad41-4267-9432-d72d14016e87",
"name": "Markeer pakbon naar Transpas verzonden",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
0,
0
],
"credentials": {
"postgres": {
"id": "m4DQjg3b1iDYGlBp",
"name": "Postgres Flower direct"
}
}
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const original = $('Bouw Transpas documentbericht').item.json;\nconst error=$json.error??$json; const errorText=error?.message??error?.description??JSON.stringify(error);\nreturn {json:{...original,transpas_error:String(errorText).slice(0,4000)}};"
},
"id": "b718fc1b-4d39-4433-ac0a-c4cac2cbb209",
"name": "Maak Transpas FTP foutmelding",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
0,
192
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "WITH input AS (SELECT $1::jsonb AS j)\nUPDATE public.lidl_fr_documents d SET transpas_error=j->>'transpas_error',transpas_delivery_status='error',updated_at=now()\nFROM input WHERE d.id=(j->>'document_id')::bigint\nRETURNING d.id,d.transport_number,d.file_path,d.transpas_error,d.transpas_delivery_status;",
"options": {
"queryReplacement": "={{ [JSON.stringify($json)] }}"
}
},
"id": "5347e54b-883f-47a6-8ad2-90be927d9b7a",
"name": "Markeer Transpas FTP fout",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
224,
192
],
"credentials": {
"postgres": {
"id": "m4DQjg3b1iDYGlBp",
"name": "Postgres Flower direct"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "WITH input AS (SELECT $1::jsonb AS j)\nUPDATE public.lidl_fr_documents d SET transpas_delivery_status='waiting_for_order',\n waiting_for_order_since=COALESCE(waiting_for_order_since,now()),transpas_error=NULL,updated_at=now()\nFROM input WHERE d.id=(j->>'document_id')::bigint\nRETURNING d.id,d.transport_number,d.file_path,d.dewit_ftp_path,d.transpas_delivery_status,d.waiting_for_order_since;",
"options": {
"queryReplacement": "={{ [JSON.stringify($json)] }}"
}
},
"id": "91567b1e-49cf-42c2-974a-47118054ee79",
"name": "Markeer pakbon wachtend op order",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
-672,
288
],
"credentials": {
"postgres": {
"id": "m4DQjg3b1iDYGlBp",
"name": "Postgres Flower direct"
}
}
},
{
"parameters": {
"mode": "chooseBranch"
},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
-2240,
192
],
"id": "5b62ed61-8057-4ed7-bb46-5653bf6c46d6",
"name": "Merge",
"notesInFlow": true,
"notes": "Wacht totdat alle benodigde NAS-mappen zijn aangemaakt en geeft daarna de originele items met binary data door naar de schrijfstap."
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 2
}
]
}
},
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
-3632,
192
],
"id": "b3e97fbb-5619-45f9-b3b4-c3f246552da9",
"name": "Elke 2 minuten"
},
{
"parameters": {
"operation": "getAll",
"limit": 50,
"output": "raw",
"filtersUI": {
"values": {
"filters": {
"hasAttachments": true,
"foldersToInclude": [
"AAMkAGZjMTE1ODhkLTViMTQtNGU4Yy1iODZhLWMwZTI3YmNmNTYxZgAuAAAAAACup9WnxzOwS7jncSsUGGY1AQB1QUdboUtaT7RtUHCMKBsJAAAAAGH0AAA="
],
"readStatus": "unread"
}
}
},
"options": {
"attachmentsPrefix": "attachment_",
"downloadAttachments": true
}
},
"type": "n8n-nodes-base.microsoftOutlook",
"typeVersion": 2,
"position": [
-3392,
192
],
"id": "d098b53e-53a0-4a54-a9a6-0f6ec372b754",
"name": "Haal ongelezen mails op",
"webhookId": "fbb1ea67-eb95-421e-8513-ea59d391fdde",
"credentials": {
"microsoftOutlookOAuth2Api": {
"id": "6kCOZktUdJo40zI6",
"name": "Lidl"
}
}
},
{
"parameters": {
"operation": "update",
"messageId": {
"__rl": true,
"value": "={{ $('Pakbon voorbereiden1').item.json.source_message_id }}",
"mode": "id"
},
"updateFields": {
"isRead": true
}
},
"type": "n8n-nodes-base.microsoftOutlook",
"typeVersion": 2,
"position": [
-1792,
16
],
"id": "4cfee871-ddfe-4b04-b0c1-7ec8a56440a5",
"name": "Markeer mail als gelezen",
"webhookId": "5259ca62-5685-4dba-a871-a342df91e8ae",
"credentials": {
"microsoftOutlookOAuth2Api": {
"id": "6kCOZktUdJo40zI6",
"name": "Lidl"
}
}
},
{
"parameters": {
"jsCode": "// Run once for all items.\n// Verwerkt alle bruikbare pakbonbijlagen uit alle opgehaalde ongelezen e-mails.\nconst ALLOWED_EXTENSIONS = new Set(['pdf', 'tif', 'tiff', 'jpg', 'jpeg', 'png']);\nconst INLINE_NAME_RE = /(?:^|[_\\s.-])(logo|image\\d*|img\\d*|signature|handtekening|facebook|linkedin|instagram|twitter|icon|banner)(?:[_\\s.-]|$)/i;\n\nfunction getExtension(fileName, binaryInfo) {\n const supplied = String(binaryInfo?.fileExtension ?? '').replace(/^\\./, '').toLowerCase();\n if (supplied) return supplied === 'tiff' ? 'tif' : supplied;\n const match = String(fileName ?? '').match(/\\.([A-Za-z0-9]+)$/);\n const ext = match ? match[1].toLowerCase() : '';\n return ext === 'tiff' ? 'tif' : ext;\n}\n\nfunction stripExtension(fileName) {\n return String(fileName ?? '').replace(/\\.[^.]+$/, '');\n}\n\nfunction normalizeReference(value) {\n return String(value ?? '').trim().replace(/^[\\s:_-]+|[\\s:_-]+$/g, '').toUpperCase();\n}\n\nfunction findReferenceInText(value) {\n const text = String(value ?? '');\n const patterns = [\n /\\b(TR\\d{10})\\b/i,\n /(?:transport(?:nummer|nr)?|referentie(?:nummer|nr)?|reference|ref)[\\s:#_-]*([A-Z0-9][A-Z0-9./_-]{4,39})/i,\n /\\b([A-Z]{1,8}\\d{6,20})\\b/i,\n /\\b(\\d{8,20})\\b/,\n ];\n\n for (const pattern of patterns) {\n const match = text.match(pattern);\n if (match?.[1]) return normalizeReference(match[1]);\n }\n return '';\n}\n\nfunction determineReference(fileName, subject, textPlain) {\n const fromFile = findReferenceInText(stripExtension(fileName));\n if (fromFile) return { reference: fromFile, source: 'filename' };\n\n const fromSubject = findReferenceInText(subject);\n if (fromSubject) return { reference: fromSubject, source: 'subject' };\n\n const fromBody = findReferenceInText(textPlain);\n if (fromBody) return { reference: fromBody, source: 'body' };\n\n return { reference: '', source: '' };\n}\n\nconst inputItems = $input.all();\nconst output = [];\nconst skipped = [];\n\nfor (let itemIndex = 0; itemIndex < inputItems.length; itemIndex++) {\n const item = inputItems[itemIndex];\n const json = item.json ?? {};\n const subject = String(json.subject ?? '');\n const textPlain = String(\n json.textPlain ??\n json.text ??\n json.bodyPreview ??\n json.body?.content ??\n ''\n );\n const metadata = json.metadata ?? {};\n\n // Get Many levert het Graph-bericht-ID doorgaans als \"id\".\n const sourceMessageId = String(\n json.id ??\n json.messageId ??\n json.message_id ??\n metadata['message-id'] ??\n ''\n );\n\n const binaryEntries = Object.entries(item.binary ?? {});\n\n if (!binaryEntries.length) {\n skipped.push(`${subject || `item ${itemIndex + 1}`}: geen gedownloade bijlagen aanwezig`);\n continue;\n }\n\n for (const [binaryPropertyName, binaryInfo] of binaryEntries) {\n const fileName = String(\n binaryInfo?.fileName ??\n binaryInfo?.fileNameFromResponse ??\n `${binaryPropertyName}.bin`\n );\n\n if (fileName.toLowerCase() === 'winmail.dat') continue;\n\n const extension = getExtension(fileName, binaryInfo);\n\n if (!ALLOWED_EXTENSIONS.has(extension)) {\n skipped.push(`${fileName}: bestandstype niet toegestaan`);\n continue;\n }\n\n if (['jpg', 'jpeg', 'png'].includes(extension) && INLINE_NAME_RE.test(fileName)) {\n skipped.push(`${fileName}: vermoedelijk logo`);\n continue;\n }\n\n const refResult = determineReference(fileName, subject, textPlain);\n\n if (['jpg', 'jpeg', 'png'].includes(extension) && refResult.source !== 'filename') {\n skipped.push(`${fileName}: afbeelding zonder transportnummer in bestandsnaam`);\n continue;\n }\n\n if (!refResult.reference) {\n skipped.push(`${fileName}: geen transportnummer gevonden`);\n continue;\n }\n\n const buffer = await this.helpers.getBinaryDataBuffer(itemIndex, binaryPropertyName);\n if (!buffer?.length) {\n skipped.push(`${fileName}: lege binary data`);\n continue;\n }\n\n const prepared = await this.helpers.prepareBinaryData(\n buffer,\n fileName,\n binaryInfo?.mimeType || 'application/octet-stream',\n );\n\n output.push({\n json: {\n transport_number: refResult.reference,\n reference_source: refResult.source,\n document_kind: 'PAKBON',\n file_name: fileName,\n file_type: extension,\n mime_type: binaryInfo?.mimeType ?? '',\n file_size_bytes: buffer.length,\n base64_data: buffer.toString('base64'),\n\n // Nodig om de e-mail na succesvolle verwerking als gelezen te markeren.\n source_message_id: sourceMessageId,\n source_subject: subject,\n source_from: String(\n json.from?.emailAddress?.address ??\n json.from?.emailAddress?.name ??\n json.from ??\n json.fromEmail ??\n metadata.from ??\n ''\n ),\n received_at: String(\n json.receivedDateTime ??\n json.date ??\n metadata['delivery-date'] ??\n new Date().toISOString()\n ),\n },\n binary: { data: prepared },\n pairedItem: { item: itemIndex },\n });\n }\n}\n\nif (!output.length) {\n throw new Error(`Geen bruikbare pakbon gevonden. ${skipped.join(' | ')}`);\n}\n\nreturn output;"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-3136,
192
],
"id": "d75b9398-9195-4fe4-99b4-c487c946e2a9",
"name": "Pakbon voorbereiden1"
},
{
"parameters": {
"command": "={{ [...new Set($input.all().map(item => `/files/lidl/pakbonnen/${item.json.storage_year}/${String(item.json.storage_month).padStart(2, \"0\")}`))].map(path => `mkdir -p \"${path}\"`).join(\" && \") }}"
},
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
-2448,
304
],
"id": "dc4cd62d-6398-48df-9c73-ea1b5e76a2a0",
"name": "Maak alle jaar-maandmappen aan",
"notesInFlow": true,
"executeOnce": true,
"notes": "Maakt in één uitvoering alle unieke jaar/maandmappen aan die in de huidige batch nodig zijn. Hierdoor worden bijvoorbeeld zowel 2026/07 als 2026/08 aangemaakt."
}
],
"pinData": {},
"connections": {
"Bereken SHA-256": {
"main": [
[
{
"node": "Bepaal NAS- en De Wit FTP-pad",
"type": "main",
"index": 0
}
]
]
},
"Bepaal NAS- en De Wit FTP-pad": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0
},
{
"node": "Maak alle jaar-maandmappen aan",
"type": "main",
"index": 0
}
]
]
},
"Schrijf pakbon naar NAS": {
"main": [
[
{
"node": "Upload originele pakbon naar De Wit FTP",
"type": "main",
"index": 0
},
{
"node": "Markeer mail als gelezen",
"type": "main",
"index": 0
}
]
]
},
"Upload originele pakbon naar De Wit FTP": {
"main": [
[
{
"node": "Sla metadata op - De Wit FTP gelukt",
"type": "main",
"index": 0
}
],
[
{
"node": "Maak De Wit FTP foutmelding",
"type": "main",
"index": 0
}
]
]
},
"Sla metadata op - De Wit FTP gelukt": {
"main": [
[
{
"node": "Controleer of order al in Transpas staat",
"type": "main",
"index": 0
}
]
]
},
"Maak De Wit FTP foutmelding": {
"main": [
[
{
"node": "Sla metadata op - De Wit FTP fout",
"type": "main",
"index": 0
}
]
]
},
"Sla metadata op - De Wit FTP fout": {
"main": [
[
{
"node": "Controleer of order al in Transpas staat",
"type": "main",
"index": 0
}
]
]
},
"Controleer of order al in Transpas staat": {
"main": [
[
{
"node": "Order al geïmporteerd in Transpas?",
"type": "main",
"index": 0
}
]
]
},
"Order al geïmporteerd in Transpas?": {
"main": [
[
{
"node": "Lees pakbon van NAS",
"type": "main",
"index": 0
}
],
[
{
"node": "Markeer pakbon wachtend op order",
"type": "main",
"index": 0
}
]
]
},
"Lees pakbon van NAS": {
"main": [
[
{
"node": "Bouw Transpas documentbericht",
"type": "main",
"index": 0
}
]
]
},
"Bouw Transpas documentbericht": {
"main": [
[
{
"node": "Upload pakbonbericht naar Transpas FTP",
"type": "main",
"index": 0
}
]
]
},
"Upload pakbonbericht naar Transpas FTP": {
"main": [
[
{
"node": "Markeer pakbon naar Transpas verzonden",
"type": "main",
"index": 0
}
],
[
{
"node": "Maak Transpas FTP foutmelding",
"type": "main",
"index": 0
}
]
]
},
"Maak Transpas FTP foutmelding": {
"main": [
[
{
"node": "Markeer Transpas FTP fout",
"type": "main",
"index": 0
}
]
]
},
"Merge": {
"main": [
[
{
"node": "Schrijf pakbon naar NAS",
"type": "main",
"index": 0
}
]
]
},
"Elke 2 minuten": {
"main": [
[
{
"node": "Haal ongelezen mails op",
"type": "main",
"index": 0
}
]
]
},
"Haal ongelezen mails op": {
"main": [
[
{
"node": "Pakbon voorbereiden1",
"type": "main",
"index": 0
}
]
]
},
"Pakbon voorbereiden1": {
"main": [
[
{
"node": "Bereken SHA-256",
"type": "main",
"index": 0
}
]
]
},
"Maak alle jaar-maandmappen aan": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"availableInMCP": false
},
"versionId": "18c83c8d-708c-4dce-99d9-4dbbd39eac3d",
"meta": {
"instanceId": "bef8d409866a58c0777dfe7cca1b9c2400fd051c056d361501393ab423006b5f"
},
"nodeGroups": [],
"id": "ghA6rJrnFmw3SdfB",
"tags": []
}