diff --git a/Flower_direct/Lidl Frankrijk - Opdracht naar Postgres en Transpas.json b/Flower_direct/Lidl Frankrijk - Opdracht naar Postgres en Transpas.json
new file mode 100644
index 0000000..5896cc0
--- /dev/null
+++ b/Flower_direct/Lidl Frankrijk - Opdracht naar Postgres en Transpas.json
@@ -0,0 +1,808 @@
+{
+ "name": "Lidl Frankrijk - Opdracht naar Postgres en Transpas -",
+ "nodes": [
+ {
+ "parameters": {
+ "jsCode": "// Run Once for All Items\n// Ontvangt bestanden die door de FTP-downloadnode in binary.data zijn gezet.\n// Alleen XLSX, XLS en CSV gaan door.\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 file = item.binary?.data;\n\n if (!file) {\n skipped.push(`item ${itemIndex + 1}: binary.data ontbreekt`);\n continue;\n }\n\n const fileName = String(\n item.json?.source_file_name ??\n file.fileName ??\n item.json?.name ??\n `bestand_${itemIndex + 1}`\n );\n\n const extension = String(\n file.fileExtension ??\n fileName.split('.').pop() ??\n ''\n )\n .replace(/^\\./, '')\n .toLowerCase()\n .trim();\n\n if (!['xlsx', 'xls', 'csv'].includes(extension)) {\n skipped.push(`${fileName}: bestandstype niet toegestaan`);\n continue;\n }\n\n output.push({\n json: {\n ...(item.json ?? {}),\n source_file_name: fileName,\n source_extension: extension,\n source_ftp_path:\n item.json?.source_ftp_path ??\n item.json?.full_path ??\n item.json?.path ??\n null,\n\n // Deze velden blijven bestaan voor compatibiliteit met PostgreSQL,\n // maar er is geen e-mailbron meer.\n source_message_id: null,\n source_subject: null,\n source_from: 'FTP',\n source_received_at:\n item.json?.modified_at ??\n item.json?.modifiedTime ??\n item.json?.modified ??\n new Date().toISOString(),\n\n is_csv: extension === 'csv',\n },\n binary: {\n data: file,\n },\n pairedItem: {\n item: itemIndex,\n },\n });\n}\n\nif (output.length === 0) {\n const details = skipped.length\n ? ` ${skipped.join(' | ')}`\n : ' Er zijn geen bestanden ontvangen.';\n\n throw new Error(\n `Geen bruikbaar XLSX-, XLS- of CSV-bestand gevonden.${details}`\n );\n}\n\nreturn output;"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 1136,
+ 416
+ ],
+ "id": "2d78e121-cdb5-48b6-a98b-0aeec510cbb0",
+ "name": "Selecteer Excel-bijlagen",
+ "notesInFlow": true,
+ "onError": "continueErrorOutput",
+ "notes": "Ontvangt gedownloade FTP-bestanden, accepteert XLSX/XLS/CSV en zet het bestand in binary.data."
+ },
+ {
+ "parameters": {
+ "jsCode": "// Run once for all items.\n// De Excel wordt zonder headernamen gelezen. Dat is bewust: Postcode en Landcode komen twee keer voor.\n// Kolom A = index 0, kolom AF = index 31.\nconst inputItems = $input.all();\nconst sourceItems = $('Selecteer Excel-bijlagen').all();\nconst output = [];\nconst errors = [];\n\nfunction sourceFor(item) {\n const paired = Array.isArray(item.pairedItem) ? item.pairedItem[0] : item.pairedItem;\n const index = Number.isInteger(paired?.item) ? paired.item : 0;\n return sourceItems[index]?.json ?? sourceItems[0]?.json ?? {};\n}\n\nfunction valueAt(row, index) {\n return row?.[String(index)] ?? row?.[index] ?? null;\n}\n\nfunction clean(value) {\n if (value === undefined || value === null) return null;\n const s = String(value).replace(/\\u00a0/g, ' ').trim();\n return s === '' ? null : s;\n}\n\nfunction numberValue(value) {\n if (value === undefined || value === null || value === '') return null;\n if (typeof value === 'number') return Number.isFinite(value) ? value : null;\n let s = String(value).trim().replace(/\\s/g, '');\n if (s.includes(',') && s.includes('.')) {\n s = s.lastIndexOf(',') > s.lastIndexOf('.')\n ? s.replace(/\\./g, '').replace(',', '.')\n : s.replace(/,/g, '');\n } else {\n s = s.replace(',', '.');\n }\n const n = Number(s);\n return Number.isFinite(n) ? n : null;\n}\n\nfunction pad2(n) {\n return String(n).padStart(2, '0');\n}\n\nfunction isoDate(value) {\n if (value === undefined || value === null || value === '') return null;\n\n if (typeof value === 'number' && Number.isFinite(value)) {\n const utc = new Date(Date.UTC(1899, 11, 30) + Math.round(value) * 86400000);\n return `${utc.getUTCFullYear()}-${pad2(utc.getUTCMonth() + 1)}-${pad2(utc.getUTCDate())}`;\n }\n\n const s = String(value).trim();\n let m = s.match(/^(\\d{4})[-/.](\\d{1,2})[-/.](\\d{1,2})/);\n if (m) return `${m[1]}-${pad2(m[2])}-${pad2(m[3])}`;\n\n m = s.match(/^(\\d{1,2})[-/.](\\d{1,2})[-/.](\\d{2}|\\d{4})$/);\n if (m) {\n let year = Number(m[3]);\n if (year < 100) year += 2000;\n return `${year}-${pad2(m[2])}-${pad2(m[1])}`;\n }\n\n const parsed = new Date(s);\n if (!Number.isNaN(parsed.getTime())) {\n return `${parsed.getUTCFullYear()}-${pad2(parsed.getUTCMonth() + 1)}-${pad2(parsed.getUTCDate())}`;\n }\n\n throw new Error(`Ongeldige datum: ${s}`);\n}\n\nfunction hhmm(value) {\n if (value === undefined || value === null || value === '') return null;\n\n if (typeof value === 'number' && Number.isFinite(value)) {\n const fraction = ((value % 1) + 1) % 1;\n let minutes = Math.round(fraction * 24 * 60) % (24 * 60);\n return `${pad2(Math.floor(minutes / 60))}:${pad2(minutes % 60)}`;\n }\n\n const s = String(value).trim();\n const match = s.match(/(?:T|^)(\\d{1,2}):(\\d{2})(?::\\d{2})?/);\n if (!match) throw new Error(`Ongeldige tijd: ${s}`);\n return `${pad2(match[1])}:${pad2(match[2])}`;\n}\n\nfunction country(value) {\n const c = (clean(value) ?? '').toUpperCase();\n if (!c) return null;\n return c === 'UK' ? 'GB' : c.slice(0, 2);\n}\n\nfunction temperatures(value) {\n const raw = clean(value);\n if (!raw) return { min: null, max: null };\n const normalized = raw.replace(/,/g, '.').replace(/°/g, '');\n const range = normalized.match(/(-?\\d+(?:\\.\\d+)?)\\s*(?:-|tot|t\\/?m)\\s*(-?\\d+(?:\\.\\d+)?)/i);\n if (range) {\n const a = Number(range[1]);\n const b = Number(range[2]);\n return { min: Math.min(a, b), max: Math.max(a, b) };\n }\n const single = normalized.match(/-?\\d+(?:\\.\\d+)?/);\n if (!single) return { min: null, max: null };\n const n = Number(single[0]);\n return { min: n, max: n };\n}\n\nfunction cancelledFromStatus(value) {\n const status = (clean(value) ?? '').toLowerCase();\n if (!status) return false;\n if (/\\b(niet|non)\\s+(geannuleerd|cancelled|canceled)\\b/.test(status)) return false;\n return /geannul|annule|cancel|vervallen|verwijderd|deleted/.test(status);\n}\n\nfor (let index = 0; index < inputItems.length; index++) {\n const item = inputItems[index];\n const row = item.json?.row && typeof item.json.row === 'object'\n ? item.json.row\n : item.json;\n\n const values = Array.from({ length: 32 }, (_, i) => valueAt(row, i));\n const filled = values.filter(v => clean(v) !== null).length;\n const first = (clean(values[0]) ?? '').toLowerCase();\n const second = (clean(values[1]) ?? '').toLowerCase();\n\n // Kopregel en lege/samengevoegde tussenregels overslaan.\n if (first === 'status' && second.includes('transport')) continue;\n if (filled < 3) continue;\n\n try {\n const source = sourceFor(item);\n const status = clean(values[0]);\n const transportNumber = (clean(values[1]) ?? '').toUpperCase();\n\n if (!transportNumber) throw new Error('Transportnummer ontbreekt');\n if (!/^TR[A-Z0-9_-]{5,}$/i.test(transportNumber)) {\n throw new Error(`Onverwacht transportnummer: ${transportNumber}`);\n }\n\n const temperature = temperatures(values[26]);\n const normalized = {\n status: status ?? 'Onbekend',\n transport_number: transportNumber,\n pickup_date: isoDate(values[2]),\n pickup_time: hhmm(values[3]),\n pickup_time_till: hhmm(values[4]),\n pickup_name: clean(values[5]),\n pickup_address1: clean(values[6]),\n pickup_zipcode: clean(values[7]),\n pickup_city: clean(values[8]),\n pickup_phone: clean(values[9]),\n pickup_country: country(values[10]),\n delivery_date: isoDate(values[11]),\n delivery_date_till: isoDate(values[11]),\n delivery_name: clean(values[12]),\n delivery_address1: clean(values[13]),\n delivery_zipcode: clean(values[14]),\n delivery_city: clean(values[15]),\n transport_code: clean(values[16]),\n delivery_country: country(values[17]),\n delivery_time: hhmm(values[18]),\n delivery_time_till: hhmm(values[19]),\n colli: numberValue(values[20]),\n loose_boxes: numberValue(values[21]),\n transport_units: numberValue(values[22]),\n product_group: clean(values[23]),\n load_carrier_count: numberValue(values[24]),\n load_carrier_type: clean(values[25]),\n temperature_group: clean(values[26]),\n min_temperature: temperature.min,\n max_temperature: temperature.max,\n reference_text: clean(values[27]),\n external_document_number: clean(values[28]),\n external_document_branch: clean(values[29]),\n forwarder: clean(values[30]),\n gross_weight: numberValue(values[31]),\n is_cancelled: cancelledFromStatus(status),\n };\n\n if (!normalized.pickup_date) throw new Error('Laaddatum ontbreekt');\n if (!normalized.delivery_date) throw new Error('Losdatum ontbreekt');\n if (!normalized.pickup_name || !normalized.pickup_address1) throw new Error('Laadadres is onvolledig');\n if (!normalized.delivery_name || !normalized.delivery_address1) throw new Error('Losadres is onvolledig');\n\n const businessPayload = { ...normalized };\n\n output.push({\n json: {\n ...normalized,\n source_file_name: source.source_file_name ?? null,\n source_subject: source.source_subject ?? null,\n source_from: source.source_from ?? null,\n source_message_id: source.source_message_id ?? null,\n source_received_at: source.source_received_at ?? new Date().toISOString(),\n raw_row: row,\n business_payload: businessPayload,\n },\n pairedItem: {\n item: index,\n },\n });\n } catch (error) {\n errors.push(`Bestandsrij ${index + 1}: ${error.message}`);\n }\n}\n\nif (errors.length) {\n throw new Error(`Bestand bevat ongeldige orderregels:\\n${errors.join('\\n')}`);\n}\nif (!output.length) {\n throw new Error('Geen orderregels gevonden na de kopregel.');\n}\n\nreturn output;\n"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 2352,
+ 368
+ ],
+ "id": "6d4504e4-7b7b-4369-827c-2cee9951b0cf",
+ "name": "Normaliseer en valideer orders",
+ "onError": "continueErrorOutput"
+ },
+ {
+ "parameters": {
+ "operation": "executeQuery",
+ "query": "WITH input AS MATERIALIZED (\n SELECT $1::jsonb AS j\n),\nsrc AS MATERIALIZED (\n SELECT\n j,\n j->'business_payload' AS p,\n md5((j->'business_payload')::text) AS payload_hash\n FROM input\n),\nprevious AS MATERIALIZED (\n SELECT o.*\n FROM public.lidl_fr_orders o\n JOIN src s ON s.p->>'transport_number' = o.transport_number\n FOR UPDATE\n),\nupserted AS (\n INSERT INTO public.lidl_fr_orders AS existing (\n transport_number, current_status, is_cancelled, revision, current_payload_hash,\n pickup_date, pickup_time, pickup_time_till, pickup_name, pickup_address1,\n pickup_zipcode, pickup_city, pickup_phone, pickup_country,\n delivery_date, delivery_date_till, delivery_time, delivery_time_till,\n delivery_name, delivery_address1, delivery_zipcode, delivery_city, delivery_country,\n transport_code, colli, loose_boxes, transport_units, product_group,\n load_carrier_count, load_carrier_type, temperature_group,\n min_temperature, max_temperature, reference_text,\n external_document_number, external_document_branch, forwarder, gross_weight,\n source_file_name, source_message_id, source_subject, source_from,\n source_received_at, raw_row\n )\n SELECT\n p->>'transport_number',\n p->>'status',\n COALESCE((p->>'is_cancelled')::boolean, false),\n 1,\n payload_hash,\n NULLIF(p->>'pickup_date', '')::date,\n NULLIF(p->>'pickup_time', '')::time,\n NULLIF(p->>'pickup_time_till', '')::time,\n p->>'pickup_name',\n p->>'pickup_address1',\n p->>'pickup_zipcode',\n p->>'pickup_city',\n p->>'pickup_phone',\n p->>'pickup_country',\n NULLIF(p->>'delivery_date', '')::date,\n NULLIF(p->>'delivery_date_till', '')::date,\n NULLIF(p->>'delivery_time', '')::time,\n NULLIF(p->>'delivery_time_till', '')::time,\n p->>'delivery_name',\n p->>'delivery_address1',\n p->>'delivery_zipcode',\n p->>'delivery_city',\n p->>'delivery_country',\n p->>'transport_code',\n NULLIF(p->>'colli', '')::numeric,\n NULLIF(p->>'loose_boxes', '')::numeric,\n NULLIF(p->>'transport_units', '')::numeric,\n p->>'product_group',\n NULLIF(p->>'load_carrier_count', '')::numeric,\n p->>'load_carrier_type',\n p->>'temperature_group',\n NULLIF(p->>'min_temperature', '')::numeric,\n NULLIF(p->>'max_temperature', '')::numeric,\n p->>'reference_text',\n p->>'external_document_number',\n p->>'external_document_branch',\n p->>'forwarder',\n NULLIF(p->>'gross_weight', '')::numeric,\n j->>'source_file_name',\n j->>'source_message_id',\n j->>'source_subject',\n j->>'source_from',\n NULLIF(j->>'source_received_at', '')::timestamptz,\n COALESCE(j->'raw_row', '{}'::jsonb)\n FROM src\n ON CONFLICT (transport_number) DO UPDATE SET\n current_status = EXCLUDED.current_status,\n is_cancelled = EXCLUDED.is_cancelled,\n revision = CASE\n WHEN existing.current_payload_hash IS DISTINCT FROM EXCLUDED.current_payload_hash\n THEN existing.revision + 1\n ELSE existing.revision\n END,\n current_payload_hash = EXCLUDED.current_payload_hash,\n pickup_date = EXCLUDED.pickup_date,\n pickup_time = EXCLUDED.pickup_time,\n pickup_time_till = EXCLUDED.pickup_time_till,\n pickup_name = EXCLUDED.pickup_name,\n pickup_address1 = EXCLUDED.pickup_address1,\n pickup_zipcode = EXCLUDED.pickup_zipcode,\n pickup_city = EXCLUDED.pickup_city,\n pickup_phone = EXCLUDED.pickup_phone,\n pickup_country = EXCLUDED.pickup_country,\n delivery_date = EXCLUDED.delivery_date,\n delivery_date_till = EXCLUDED.delivery_date_till,\n delivery_time = EXCLUDED.delivery_time,\n delivery_time_till = EXCLUDED.delivery_time_till,\n delivery_name = EXCLUDED.delivery_name,\n delivery_address1 = EXCLUDED.delivery_address1,\n delivery_zipcode = EXCLUDED.delivery_zipcode,\n delivery_city = EXCLUDED.delivery_city,\n delivery_country = EXCLUDED.delivery_country,\n transport_code = EXCLUDED.transport_code,\n colli = EXCLUDED.colli,\n loose_boxes = EXCLUDED.loose_boxes,\n transport_units = EXCLUDED.transport_units,\n product_group = EXCLUDED.product_group,\n load_carrier_count = EXCLUDED.load_carrier_count,\n load_carrier_type = EXCLUDED.load_carrier_type,\n temperature_group = EXCLUDED.temperature_group,\n min_temperature = EXCLUDED.min_temperature,\n max_temperature = EXCLUDED.max_temperature,\n reference_text = EXCLUDED.reference_text,\n external_document_number = EXCLUDED.external_document_number,\n external_document_branch = EXCLUDED.external_document_branch,\n forwarder = EXCLUDED.forwarder,\n gross_weight = EXCLUDED.gross_weight,\n source_file_name = EXCLUDED.source_file_name,\n source_message_id = EXCLUDED.source_message_id,\n source_subject = EXCLUDED.source_subject,\n source_from = EXCLUDED.source_from,\n source_received_at = EXCLUDED.source_received_at,\n raw_row = EXCLUDED.raw_row,\n last_seen_at = now(),\n status_changed_at = CASE\n WHEN existing.current_status IS DISTINCT FROM EXCLUDED.current_status\n OR existing.is_cancelled IS DISTINCT FROM EXCLUDED.is_cancelled\n THEN now()\n ELSE existing.status_changed_at\n END,\n reactivated_at = CASE\n WHEN existing.is_cancelled = true AND EXCLUDED.is_cancelled = false\n THEN now()\n ELSE existing.reactivated_at\n END,\n last_error = CASE\n WHEN existing.current_payload_hash IS DISTINCT FROM EXCLUDED.current_payload_hash\n THEN NULL\n ELSE existing.last_error\n END\n RETURNING *\n),\nclassified AS MATERIALIZED (\n SELECT\n u.*,\n CASE\n WHEN p.id IS NULL THEN 'created'\n WHEN p.is_cancelled = false AND u.is_cancelled = true THEN 'cancelled'\n WHEN p.is_cancelled = true AND u.is_cancelled = false THEN 'reactivated'\n WHEN p.current_payload_hash IS DISTINCT FROM u.current_payload_hash THEN 'updated'\n ELSE 'unchanged'\n END AS event_type,\n (p.id IS NULL OR p.current_payload_hash IS DISTINCT FROM u.current_payload_hash) AS state_changed\n FROM upserted u\n LEFT JOIN previous p ON p.id = u.id\n),\nevent_insert AS (\n INSERT INTO public.lidl_fr_order_events (\n order_id, transport_number, revision, event_type, status,\n is_cancelled, payload_hash, source_file_name, source_message_id, row_data\n )\n SELECT\n c.id, c.transport_number, c.revision, c.event_type, c.current_status,\n c.is_cancelled, c.current_payload_hash, c.source_file_name,\n c.source_message_id, c.raw_row\n FROM classified c\n WHERE c.state_changed\n ON CONFLICT (transport_number, revision) DO NOTHING\n RETURNING id\n),\nlatest_document AS MATERIALIZED (\n SELECT d.*\n FROM public.lidl_fr_documents d\n JOIN classified c ON c.transport_number = d.transport_number\n WHERE d.document_kind = 'PAKBON'\n AND d.active = true\n AND d.storage_status = 'stored'\n AND d.file_path IS NOT NULL\n ORDER BY d.received_at DESC, d.id DESC\n LIMIT 1\n)\nSELECT\n c.id AS order_id,\n c.transport_number,\n c.current_status,\n c.is_cancelled,\n c.revision,\n c.current_payload_hash AS payload_hash,\n c.event_type,\n (c.revision > COALESCE(c.last_transpas_export_revision, 0)) AS should_export,\n CASE\n WHEN c.is_cancelled THEN 'DELETE'\n WHEN c.has_active_export = false THEN 'NEW'\n ELSE 'UPDATE'\n END AS transpas_action,\n\n c.pickup_date,\n to_char(c.pickup_time, 'HH24:MI') AS pickup_time,\n to_char(c.pickup_time_till, 'HH24:MI') AS pickup_time_till,\n c.pickup_name,\n c.pickup_address1,\n c.pickup_zipcode,\n c.pickup_city,\n c.pickup_phone,\n c.pickup_country,\n\n c.delivery_date,\n c.delivery_date_till,\n to_char(c.delivery_time, 'HH24:MI') AS delivery_time,\n to_char(c.delivery_time_till, 'HH24:MI') AS delivery_time_till,\n c.delivery_name,\n c.delivery_address1,\n c.delivery_zipcode,\n c.delivery_city,\n c.delivery_country,\n\n c.transport_code,\n c.colli,\n c.loose_boxes,\n c.transport_units,\n c.product_group,\n c.load_carrier_count,\n c.load_carrier_type,\n c.temperature_group,\n c.min_temperature,\n c.max_temperature,\n c.reference_text,\n c.external_document_number,\n c.external_document_branch,\n c.forwarder,\n c.gross_weight,\n c.source_file_name,\n\n d.id AS pakbon_id,\n d.file_name AS pakbon_file_name,\n d.stored_file_name AS pakbon_stored_file_name,\n d.file_path AS pakbon_file_path,\n d.file_type AS pakbon_file_type,\n d.mime_type AS pakbon_mime_type,\n d.document_hash AS pakbon_document_hash\nFROM classified c\nLEFT JOIN latest_document d ON true;",
+ "options": {
+ "queryReplacement": "={{ [JSON.stringify($json)] }}"
+ }
+ },
+ "type": "n8n-nodes-base.postgres",
+ "typeVersion": 2.6,
+ "position": [
+ 2672,
+ 352
+ ],
+ "id": "8ef1a7e3-819c-46c5-b04c-cc1db01de2d7",
+ "name": "Upsert order status en zoek pakbon",
+ "credentials": {
+ "postgres": {
+ "id": "m4DQjg3b1iDYGlBp",
+ "name": "Postgres Flower direct"
+ }
+ },
+ "onError": "continueErrorOutput"
+ },
+ {
+ "parameters": {
+ "conditions": {
+ "options": {
+ "caseSensitive": true,
+ "leftValue": "",
+ "typeValidation": "strict",
+ "version": 2
+ },
+ "conditions": [
+ {
+ "id": "0f2ae688-8cf4-47ff-ab51-3457409c88fb",
+ "leftValue": "={{ $json.should_export }}",
+ "rightValue": "",
+ "operator": {
+ "type": "boolean",
+ "operation": "true",
+ "singleValue": true
+ }
+ }
+ ],
+ "combinator": "and"
+ },
+ "options": {}
+ },
+ "type": "n8n-nodes-base.filter",
+ "typeVersion": 2.2,
+ "position": [
+ 2960,
+ 320
+ ],
+ "id": "fa659a1a-13b4-4897-b2e6-e637da769216",
+ "name": "Alleen nieuwe of gewijzigde revisies",
+ "disabled": true
+ },
+ {
+ "parameters": {
+ "mode": "runOnceForEachItem",
+ "jsCode": "// Per item uitvoeren.\n// De ordergegevens komen rechtstreeks uit PostgreSQL of via de gekoppelde item-link\n// wanneer het pakbonbestand eerst van de NAS is gelezen.\n\nconst CFG = {\n customerId: '60971',\n departmentId: '',\n shipmentKindId: '',\n ediproviderId: '',\n companyId: '1',\n unitMatchmode: '1',\n documentTypeId: 'PAKBON',\n documentTypeMatchmode: '1',\n ftpFolder: '/test/ToTP/TPE',\n\n deliveryDriverInfo: [\n 'Transport flowers at +4°C to +6°C – Transport plants at +15°C –',\n 'When transporting a mixed load of flowers and plants, set the temperature to +12°C.',\n 'Exchange the pallets at the unloading location using PAKi eVoucher number 048849 04-CAMBRAI',\n ].join(' '),\n};\n\nconst linkedOrder = $('Upsert order status en zoek pakbon').item.json;\nconst j = $json?.order_id ? $json : linkedOrder;\nconst inputItem = $input.item;\n\nfunction xmlEsc(value) {\n return String(value ?? '')\n .replace(\n /&(?!amp;|lt;|gt;|quot;|apos;|#\\d+;|#x[0-9A-Fa-f]+;)/g,\n '&',\n )\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction tag(name, value, attrs = '') {\n if (\n value === undefined ||\n value === null ||\n String(value).trim() === ''\n ) {\n return '';\n }\n\n const attrText = attrs ? ` ${attrs}` : '';\n return `<${name}${attrText}>${xmlEsc(value)}${name}>`;\n}\n\nfunction emptyTag(name, attrs = '') {\n const attrText = attrs ? ` ${attrs}` : '';\n return `<${name}${attrText} />`;\n}\n\nfunction safeFilePart(value) {\n return String(value ?? '')\n .replace(/[^a-zA-Z0-9._-]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .slice(0, 100) || 'order';\n}\n\nfunction block(name, lines, indent = ' ') {\n const present = lines.filter(Boolean);\n\n if (!present.length) {\n return '';\n }\n\n return [\n `${indent}<${name}>`,\n ...present.map(line => `${indent} ${line}`),\n `${indent}${name}>`,\n ].join('\\n');\n}\n\n/**\n * Zet een PostgreSQL-/ISO-datum om naar de lokale kalenderdatum\n * in Nederland.\n *\n * Voorbeeld:\n * 2026-08-02T22:00:00.000Z -> 2026-08-03\n */\nfunction formatDate(value) {\n if (value === undefined || value === null || value === '') {\n return null;\n }\n\n const text = String(value).trim();\n\n // Een datum zonder tijd kan direct worden gebruikt.\n const dateOnly = text.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n\n if (dateOnly) {\n return `${dateOnly[1]}-${dateOnly[2]}-${dateOnly[3]}`;\n }\n\n const date = new Date(text);\n\n if (Number.isNaN(date.getTime())) {\n throw new Error(`Ongeldige datum ontvangen: ${text}`);\n }\n\n const parts = new Intl.DateTimeFormat('en-CA', {\n timeZone: 'Europe/Amsterdam',\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n }).formatToParts(date);\n\n const result = Object.fromEntries(\n parts\n .filter(part => part.type !== 'literal')\n .map(part => [part.type, part.value]),\n );\n\n return `${result.year}-${result.month}-${result.day}`;\n}\n\nconst pickupDate = formatDate(j.pickup_date);\nconst deliveryDate = formatDate(j.delivery_date);\nconst deliveryDateTill = formatDate(\n j.delivery_date_till || j.delivery_date,\n);\n\nlet pakbonBase64 = '';\n\nif (!j.is_cancelled && inputItem.binary?.pakbon) {\n const buffer = await this.helpers.getBinaryDataBuffer(0, 'pakbon');\n pakbonBase64 = buffer.toString('base64');\n}\n\nconst rootOptional = [\n tag('ediprovider_id', CFG.ediproviderId),\n tag('company_id', CFG.companyId),\n].filter(Boolean);\n\nconst bookingOptional = [\n tag('department_id', CFG.departmentId),\n].filter(Boolean);\n\nconst shipmentOptional = [\n tag(\n 'shipmentkind_id',\n CFG.shipmentKindId,\n 'matchmode=\"1\"',\n ),\n].filter(Boolean);\n\nconst pickup = block(\n 'pickupaddress',\n [\n emptyTag('address_id', 'matchmode=\"11\"'),\n tag('date', pickupDate),\n tag('datetill', pickupDate),\n tag('time', j.pickup_time),\n tag('timetill', j.pickup_time_till),\n tag('name', j.pickup_name),\n tag('address1', j.pickup_address1),\n tag('zipcode', j.pickup_zipcode),\n tag('city_id', j.pickup_city, 'matchmode=\"4\"'),\n tag('country_id', j.pickup_country, 'matchmode=\"2\"'),\n tag('phone', j.pickup_phone),\n\n // Transportnummer is de laadreferentie.\n tag('reference', j.transport_number),\n\n // De naam/code van het afleveradres komt als chauffeursinformatie\n // bij het laadadres.\n tag('driverinfo', j.delivery_name),\n ],\n ' ',\n);\n\nconst delivery = block(\n 'deliveryaddress',\n [\n emptyTag('address_id', 'matchmode=\"11\"'),\n tag('date', deliveryDate),\n tag('datetill', deliveryDateTill),\n tag('time', j.delivery_time),\n tag('timetill', j.delivery_time_till),\n tag('name', j.delivery_name),\n tag('address1', j.delivery_address1),\n tag('zipcode', j.delivery_zipcode),\n tag('city_id', j.delivery_city, 'matchmode=\"4\"'),\n tag('country_id', j.delivery_country, 'matchmode=\"2\"'),\n\n // Transportnummer is ook de losreferentie.\n tag('reference', j.transport_number),\n\n // Vaste Lidl-instructie bij het losadres.\n tag('driverinfo', CFG.deliveryDriverInfo),\n ],\n ' ',\n);\n\nconst sender = block(\n 'sender',\n [\n tag('name', j.pickup_name),\n tag('address1', j.pickup_address1),\n tag('zipcode', j.pickup_zipcode),\n tag('city_id', j.pickup_city, 'matchmode=\"4\"'),\n tag('country_id', j.pickup_country, 'matchmode=\"2\"'),\n tag('phone', j.pickup_phone),\n ],\n ' ',\n);\n\nconst receiver = block(\n 'receiver',\n [\n tag('name', j.delivery_name),\n tag('address1', j.delivery_address1),\n tag('zipcode', j.delivery_zipcode),\n tag('city_id', j.delivery_city, 'matchmode=\"4\"'),\n tag('country_id', j.delivery_country, 'matchmode=\"2\"'),\n ],\n ' ',\n);\n\nconst cargo = block(\n 'cargo',\n [\n tag('colli', j.colli),\n tag(\n 'unitamount',\n j.load_carrier_count ?? j.transport_units,\n ),\n tag(\n 'unit_id',\n j.load_carrier_type,\n `matchmode=\"${CFG.unitMatchmode}\"`,\n ),\n tag('product_id', j.product_group, 'matchmode=\"1\"'),\n tag('productdescription', j.product_group),\n tag('weight', j.gross_weight),\n tag('mintemperature', j.min_temperature),\n tag('maxtemperature', j.max_temperature),\n ],\n ' ',\n);\n\nconst includePakbon =\n !j.is_cancelled &&\n Boolean(pakbonBase64) &&\n Boolean(j.pakbon_file_name);\n\nlet documents = '';\n\nif (includePakbon) {\n documents = [\n ' ',\n ' ',\n ` ${tag(\n 'documenttype_id',\n CFG.documentTypeId,\n `matchmode=\"${CFG.documentTypeMatchmode}\"`,\n )}`,\n ` ${tag('filename', j.pakbon_file_name)}`,\n ` ${tag('filedata', pakbonBase64)}`,\n ` ${tag('reference', j.transport_number)}`,\n ` ${tag('concerns', 'Pakbon')}`,\n ' ',\n ' ',\n ].join('\\n');\n}\n\nconst xmlLines = [\n '',\n '',\n ' ',\n\n ...rootOptional.map(value => ` ${value}`),\n\n ` ${tag(\n 'ediproviderstatus_id',\n j.transpas_action,\n 'matchmode=\"1\"',\n )}`,\n\n ` ${tag('edireference', j.transport_number)}`,\n ` ${tag('reference', j.transport_number)}`,\n ` ${tag(\n 'customer_id',\n CFG.customerId,\n 'matchmode=\"1\"',\n )}`,\n\n ...bookingOptional.map(value => ` ${value}`),\n\n ' ',\n ' ',\n\n ` ${tag(\n 'plangroup_id',\n '17',\n 'matchmode=\"0\"',\n )}`,\n\n ` ${tag(\n 'edireference',\n j.transport_number,\n )}`,\n\n ` ${tag(\n 'reference',\n j.transport_number,\n )}`,\n\n ` ${tag(\n 'cancellation',\n j.is_cancelled ? 'true' : 'false',\n )}`,\n\n ...shipmentOptional.map(value => ` ${value}`),\n\n sender,\n receiver,\n pickup,\n delivery,\n cargo,\n documents,\n\n ' ',\n ' ',\n ' ',\n '',\n].filter(Boolean);\n\nconst xml = xmlLines.join('\\n');\n\nconst stamp = $now\n .setZone('Europe/Amsterdam')\n .toFormat('yyyyLLdd_HHmmss_SSS');\n\nconst xmlFileName =\n `${stamp}_` +\n `${safeFilePart(j.transport_number)}_` +\n `R${j.revision}_` +\n `${j.transpas_action}.xml`;\n\nconst ftpFolder = String(CFG.ftpFolder || '').replace(/\\/+$/, '');\n\nreturn {\n json: {\n order_id: j.order_id,\n transport_number: j.transport_number,\n revision: j.revision,\n payload_hash: j.payload_hash,\n transpas_action: j.transpas_action,\n is_cancelled: j.is_cancelled,\n\n pickup_date: pickupDate,\n delivery_date: deliveryDate,\n delivery_date_till: deliveryDateTill,\n\n pakbon_id: j.pakbon_id ?? null,\n pakbon_file_path: j.pakbon_file_path ?? null,\n document_included: includePakbon,\n\n xml_file_name: xmlFileName,\n ftp_path: `${ftpFolder}/${xmlFileName}`,\n xml,\n },\n};"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 3632,
+ 320
+ ],
+ "id": "13565bdb-edfc-4501-b165-741a6c86ddd6",
+ "name": "Bouw Transpas TPE XML",
+ "onError": "continueErrorOutput"
+ },
+ {
+ "parameters": {
+ "operation": "executeQuery",
+ "query": "WITH input AS (\n SELECT $1::jsonb AS j\n),\nlogged AS (\n INSERT INTO public.lidl_fr_transpas_export_log AS existing (\n order_id, transport_number, revision, payload_hash, transpas_action,\n is_cancelled, document_id, document_included,\n xml_file_name, ftp_path, xml_payload, xml_payload_hash, xml_size_bytes,\n export_status\n )\n SELECT\n (j->>'order_id')::bigint,\n j->>'transport_number',\n (j->>'revision')::integer,\n j->>'payload_hash',\n j->>'transpas_action',\n (j->>'is_cancelled')::boolean,\n NULLIF(j->>'pakbon_id', '')::bigint,\n COALESCE((j->>'document_included')::boolean, false),\n j->>'xml_file_name',\n j->>'ftp_path',\n CASE\n WHEN COALESCE((j->>'document_included')::boolean, false) THEN NULL\n ELSE j->>'xml'\n END,\n md5(j->>'xml'),\n octet_length(j->>'xml')::bigint,\n 'pending'\n FROM input\n ON CONFLICT (transport_number, revision) DO UPDATE SET\n payload_hash = EXCLUDED.payload_hash,\n transpas_action = EXCLUDED.transpas_action,\n is_cancelled = EXCLUDED.is_cancelled,\n document_id = EXCLUDED.document_id,\n document_included = EXCLUDED.document_included,\n xml_file_name = EXCLUDED.xml_file_name,\n ftp_path = EXCLUDED.ftp_path,\n xml_payload = EXCLUDED.xml_payload,\n xml_payload_hash = EXCLUDED.xml_payload_hash,\n xml_size_bytes = EXCLUDED.xml_size_bytes,\n export_status = 'pending',\n error_text = NULL,\n attempt_count = existing.attempt_count + 1,\n updated_at = now()\n RETURNING id\n)\nSELECT\n (j->>'order_id')::bigint AS order_id,\n j->>'transport_number' AS transport_number,\n (j->>'revision')::integer AS revision,\n j->>'payload_hash' AS payload_hash,\n j->>'transpas_action' AS transpas_action,\n (j->>'is_cancelled')::boolean AS is_cancelled,\n NULLIF(j->>'pakbon_id', '')::bigint AS pakbon_id,\n COALESCE((j->>'document_included')::boolean, false) AS document_included,\n j->>'xml_file_name' AS xml_file_name,\n j->>'ftp_path' AS ftp_path,\n j->>'xml' AS xml,\n logged.id AS export_log_id\nFROM input, logged;",
+ "options": {
+ "queryReplacement": "={{ [JSON.stringify($json)] }}"
+ }
+ },
+ "type": "n8n-nodes-base.postgres",
+ "typeVersion": 2.6,
+ "position": [
+ 3872,
+ 304
+ ],
+ "id": "0ce5c72a-9b85-477d-a1cf-f8814821411d",
+ "name": "Log exportpoging",
+ "credentials": {
+ "postgres": {
+ "id": "m4DQjg3b1iDYGlBp",
+ "name": "Postgres Flower direct"
+ }
+ },
+ "onError": "continueErrorOutput"
+ },
+ {
+ "parameters": {
+ "operation": "upload",
+ "path": "={{ $json.ftp_path }}",
+ "binaryData": false,
+ "fileContent": "={{ $json.xml }}",
+ "options": {}
+ },
+ "type": "n8n-nodes-base.ftp",
+ "typeVersion": 1,
+ "position": [
+ 4176,
+ 288
+ ],
+ "id": "cddb120b-27ae-4e65-91f5-63e9a78e045d",
+ "name": "Upload order naar Transpas FTP",
+ "credentials": {
+ "ftp": {
+ "id": "oLAZ4OgmkOopMHAq",
+ "name": "FTP De Wit Transport"
+ }
+ },
+ "onError": "continueErrorOutput"
+ },
+ {
+ "parameters": {
+ "operation": "executeQuery",
+ "query": "WITH input AS (\n SELECT $1::jsonb AS j\n),\nlog_update AS (\n UPDATE public.lidl_fr_transpas_export_log l\n SET\n export_status = 'exported',\n error_text = NULL,\n exported_at = now(),\n updated_at = now()\n FROM input\n WHERE l.id = (j->>'export_log_id')::bigint\n RETURNING l.id\n),\norder_update AS (\n UPDATE public.lidl_fr_orders o\n SET\n last_transpas_export_revision = (j->>'revision')::integer,\n last_transpas_export_hash = j->>'payload_hash',\n last_transpas_export_action = j->>'transpas_action',\n last_transpas_export_at = now(),\n last_transpas_export_file = j->>'xml_file_name',\n has_active_export = o.has_active_export OR NOT (j->>'is_cancelled')::boolean,\n last_error = NULL\n FROM input\n WHERE o.id = (j->>'order_id')::bigint\n AND o.revision = (j->>'revision')::integer\n RETURNING o.id\n),\ndocument_update AS (\n UPDATE public.lidl_fr_documents d\n SET\n last_included_in_order_at = now(),\n transpas_delivery_status = 'sent_with_order',\n waiting_for_order_since = NULL,\n transpas_error = NULL,\n updated_at = now()\n FROM input\n WHERE d.id = NULLIF(j->>'pakbon_id', '')::bigint\n AND COALESCE((j->>'document_included')::boolean, false) = true\n RETURNING d.id\n)\nSELECT\n j->>'transport_number' AS transport_number,\n (j->>'revision')::integer AS revision,\n j->>'transpas_action' AS transpas_action,\n j->>'xml_file_name' AS xml_file_name,\n COALESCE((SELECT count(*) FROM log_update), 0)::integer AS export_log_updated,\n COALESCE((SELECT count(*) FROM order_update), 0)::integer AS order_updated,\n COALESCE((SELECT count(*) FROM document_update), 0)::integer AS document_updated\nFROM input;",
+ "options": {
+ "queryReplacement": "={{ [JSON.stringify($('Log exportpoging').item.json)] }}"
+ }
+ },
+ "type": "n8n-nodes-base.postgres",
+ "typeVersion": 2.6,
+ "position": [
+ 4576,
+ 176
+ ],
+ "id": "74d1eabc-f4c6-4bf3-9e54-393da0f7a5d0",
+ "name": "Markeer export geslaagd",
+ "credentials": {
+ "postgres": {
+ "id": "m4DQjg3b1iDYGlBp",
+ "name": "Postgres Flower direct"
+ }
+ },
+ "onError": "continueErrorOutput"
+ },
+ {
+ "parameters": {
+ "mode": "runOnceForEachItem",
+ "jsCode": "const original = $('Log exportpoging').item.json;\nconst error = $json.error ?? $json;\nconst errorText = error?.message ?? error?.description ?? JSON.stringify(error);\nreturn {\n json: {\n ...original,\n error_text: String(errorText).slice(0, 4000),\n },\n};"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 4464,
+ 352
+ ],
+ "id": "99f0884c-a60c-4b8d-b142-72176bf12618",
+ "name": "Maak FTP foutmelding",
+ "onError": "continueErrorOutput"
+ },
+ {
+ "parameters": {
+ "operation": "executeQuery",
+ "query": "WITH input AS (\n SELECT $1::jsonb AS j\n),\nlog_update AS (\n UPDATE public.lidl_fr_transpas_export_log l\n SET\n export_status = 'error',\n error_text = j->>'error_text',\n updated_at = now()\n FROM input\n WHERE l.id = (j->>'export_log_id')::bigint\n RETURNING l.id\n),\norder_update AS (\n UPDATE public.lidl_fr_orders o\n SET last_error = j->>'error_text'\n FROM input\n WHERE o.id = (j->>'order_id')::bigint\n RETURNING o.id\n)\nSELECT\n j->>'transport_number' AS transport_number,\n (j->>'revision')::integer AS revision,\n j->>'error_text' AS error_text,\n COALESCE((SELECT count(*) FROM log_update), 0)::integer AS export_log_updated,\n COALESCE((SELECT count(*) FROM order_update), 0)::integer AS order_updated\nFROM input;",
+ "options": {
+ "queryReplacement": "={{ [JSON.stringify($json)] }}"
+ }
+ },
+ "type": "n8n-nodes-base.postgres",
+ "typeVersion": 2.6,
+ "position": [
+ 4752,
+ 336
+ ],
+ "id": "ed112513-d2d0-4f4c-ad41-adee052c4bff",
+ "name": "Markeer exportfout",
+ "credentials": {
+ "postgres": {
+ "id": "m4DQjg3b1iDYGlBp",
+ "name": "Postgres Flower direct"
+ }
+ },
+ "onError": "continueErrorOutput"
+ },
+ {
+ "parameters": {
+ "conditions": {
+ "options": {
+ "caseSensitive": true,
+ "leftValue": "",
+ "typeValidation": "strict",
+ "version": 2
+ },
+ "combinator": "and",
+ "conditions": [
+ {
+ "id": "47c2d0f6-3a28-484c-ae8f-dbf5b9667a9c",
+ "leftValue": "={{ Boolean($json.pakbon_file_path) && !$json.is_cancelled }}",
+ "rightValue": "",
+ "operator": {
+ "type": "boolean",
+ "operation": "true",
+ "singleValue": true
+ }
+ }
+ ]
+ },
+ "options": {}
+ },
+ "type": "n8n-nodes-base.filter",
+ "typeVersion": 2.2,
+ "position": [
+ 3184,
+ 224
+ ],
+ "id": "4d787260-1d2c-47d6-b2ac-022636fecd5a",
+ "name": "Met opgeslagen pakbon"
+ },
+ {
+ "parameters": {
+ "conditions": {
+ "options": {
+ "caseSensitive": true,
+ "leftValue": "",
+ "typeValidation": "strict",
+ "version": 2
+ },
+ "combinator": "and",
+ "conditions": [
+ {
+ "id": "d0853ec1-0c52-4493-b2ab-c4dd7cabd921",
+ "leftValue": "={{ !Boolean($json.pakbon_file_path) || $json.is_cancelled }}",
+ "rightValue": "",
+ "operator": {
+ "type": "boolean",
+ "operation": "true",
+ "singleValue": true
+ }
+ }
+ ]
+ },
+ "options": {}
+ },
+ "type": "n8n-nodes-base.filter",
+ "typeVersion": 2.2,
+ "position": [
+ 3312,
+ 400
+ ],
+ "id": "ebc1c06d-376d-4268-ba8b-a4014de05a94",
+ "name": "Zonder pakbonbestand"
+ },
+ {
+ "parameters": {
+ "fileSelector": "={{ $json.pakbon_file_path }}",
+ "options": {
+ "fileName": "={{ $json.pakbon_file_name }}",
+ "mimeType": "={{ $json.pakbon_mime_type }}",
+ "dataPropertyName": "pakbon"
+ }
+ },
+ "type": "n8n-nodes-base.readWriteFile",
+ "typeVersion": 1.1,
+ "position": [
+ 3408,
+ 224
+ ],
+ "id": "b38a126c-6526-4122-909f-e783d9f582af",
+ "name": "Lees pakbon van NAS",
+ "onError": "continueErrorOutput"
+ },
+ {
+ "parameters": {
+ "conditions": {
+ "options": {
+ "caseSensitive": true,
+ "leftValue": "",
+ "typeValidation": "strict",
+ "version": 2
+ },
+ "conditions": [
+ {
+ "id": "42b88165-8154-4ae5-ba77-f204a464daa4",
+ "leftValue": "={{ $json.source_extension }}",
+ "rightValue": "csv",
+ "operator": {
+ "type": "string",
+ "operation": "equals"
+ }
+ }
+ ],
+ "combinator": "and"
+ },
+ "options": {}
+ },
+ "type": "n8n-nodes-base.filter",
+ "typeVersion": 2.2,
+ "position": [
+ 1408,
+ 400
+ ],
+ "id": "5d1963ea-8bd8-44e8-974b-3e4d96f190f7",
+ "name": "Bestand is CSV"
+ },
+ {
+ "parameters": {
+ "jsCode": "// Detecteert het CSV-scheidingsteken en zet Windows-1252/ANSI om naar UTF-8.\nconst inputItems = $input.all();\nconst output = [];\n\n// Windows-1252 wijkt op deze posities af van ISO-8859-1.\nconst WINDOWS_1252 = {\n 0x80: '€',\n 0x82: '‚',\n 0x83: 'ƒ',\n 0x84: '„',\n 0x85: '…',\n 0x86: '†',\n 0x87: '‡',\n 0x88: 'ˆ',\n 0x89: '‰',\n 0x8A: 'Š',\n 0x8B: '‹',\n 0x8C: 'Œ',\n 0x8E: 'Ž',\n 0x91: '‘',\n 0x92: '’',\n 0x93: '“',\n 0x94: '”',\n 0x95: '•',\n 0x96: '–',\n 0x97: '—',\n 0x98: '˜',\n 0x99: '™',\n 0x9A: 'š',\n 0x9B: '›',\n 0x9C: 'œ',\n 0x9E: 'ž',\n 0x9F: 'Ÿ',\n};\n\nfunction decodeWindows1252(buffer) {\n let result = '';\n\n for (const byte of buffer) {\n if (byte < 0x80 || byte >= 0xA0) {\n result += String.fromCharCode(byte);\n } else {\n result += WINDOWS_1252[byte] ?? String.fromCharCode(byte);\n }\n }\n\n return result;\n}\n\nfunction decodeCsv(buffer) {\n // Een ongeldige UTF-8-byte wordt door Node als � weergegeven.\n const utf8 = buffer.toString('utf8');\n\n if (!utf8.includes('\\uFFFD')) {\n return {\n text: utf8,\n encoding: 'utf-8',\n };\n }\n\n return {\n text: decodeWindows1252(buffer),\n encoding: 'windows-1252',\n };\n}\n\nfunction countOutsideQuotes(line, delimiter) {\n let count = 0;\n let quoted = false;\n\n for (let i = 0; i < line.length; i++) {\n const char = line[i];\n\n if (char === '\"') {\n if (quoted && line[i + 1] === '\"') {\n i++;\n } else {\n quoted = !quoted;\n }\n\n continue;\n }\n\n if (!quoted && char === delimiter) {\n count++;\n }\n }\n\n return count;\n}\n\nfor (let index = 0; index < inputItems.length; index++) {\n const item = inputItems[index];\n const sourceBinary = item.binary?.data;\n\n if (!sourceBinary) {\n throw new Error(`Binary-veld 'data' ontbreekt bij item ${index + 1}.`);\n }\n\n const sourceBuffer =\n await this.helpers.getBinaryDataBuffer(index, 'data');\n\n const decoded = decodeCsv(sourceBuffer);\n const text = decoded.text.replace(/^\\uFEFF/, '');\n\n const firstLine =\n text\n .split(/\\r?\\n/)\n .find(line => line.trim() !== '') ??\n '';\n\n const candidates = [';', ',', '\\t'];\n\n const delimiter = candidates\n .map(value => ({\n value,\n count: countOutsideQuotes(firstLine, value),\n }))\n .sort((a, b) => b.count - a.count)[0];\n\n if (!delimiter || delimiter.count === 0) {\n throw new Error(\n `CSV-scheidingsteken kon niet worden bepaald voor ${\n item.json?.source_file_name ?? sourceBinary.fileName ?? 'het bestand'\n }.`\n );\n }\n\n // De volgende CSV-node ontvangt voortaan altijd echte UTF-8-bytes.\n const utf8Binary = await this.helpers.prepareBinaryData(\n Buffer.from(text, 'utf8'),\n sourceBinary.fileName ?? item.json?.source_file_name ?? 'transportopgaaf.csv',\n 'text/csv',\n );\n\n output.push({\n json: {\n ...(item.json ?? {}),\n csv_delimiter: delimiter.value,\n source_encoding: decoded.encoding,\n normalized_encoding: 'utf-8',\n },\n binary: {\n data: utf8Binary,\n },\n pairedItem: {\n item: index,\n },\n });\n}\n\nreturn output;"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 1632,
+ 400
+ ],
+ "id": "f8b5ca41-0887-4ded-a493-49a29a783dba",
+ "name": "Detecteer CSV scheidingsteken",
+ "notesInFlow": true,
+ "onError": "continueErrorOutput",
+ "notes": "Detecteert ; , of tab. Herkent daarnaast ongeldige UTF-8 en zet Windows-1252/ANSI eerst om naar UTF-8. Hierdoor blijven tekens zoals é, è, ç en apostroffen correct in PostgreSQL en XML."
+ },
+ {
+ "parameters": {
+ "options": {
+ "delimiter": "={{ $json.csv_delimiter }}",
+ "encoding": "utf8",
+ "headerRow": false,
+ "includeEmptyCells": true,
+ "rawData": false,
+ "readAsString": false
+ }
+ },
+ "type": "n8n-nodes-base.extractFromFile",
+ "typeVersion": 1.1,
+ "position": [
+ 1904,
+ 384
+ ],
+ "id": "ca5eeac7-94ec-4299-b785-152f774d6dda",
+ "name": "Lees CSV op kolompositie",
+ "notesInFlow": true,
+ "onError": "continueErrorOutput",
+ "notes": "Leest CSV zonder kopregel met het automatisch gedetecteerde scheidingsteken."
+ },
+ {
+ "parameters": {
+ "conditions": {
+ "options": {
+ "caseSensitive": true,
+ "leftValue": "",
+ "typeValidation": "strict",
+ "version": 2
+ },
+ "conditions": [
+ {
+ "id": "b4fb3c27-a621-4551-8ea0-cb8cf70817ca",
+ "leftValue": "={{ $json.should_export }}",
+ "rightValue": "",
+ "operator": {
+ "type": "boolean",
+ "operation": "false",
+ "singleValue": true
+ }
+ }
+ ],
+ "combinator": "and"
+ },
+ "options": {}
+ },
+ "type": "n8n-nodes-base.filter",
+ "typeVersion": 2.2,
+ "position": [
+ 2960,
+ 512
+ ],
+ "id": "84799366-a4ce-4edd-a5b9-67e3233eae48",
+ "name": "Alleen ongewijzigde revisies",
+ "notesInFlow": true,
+ "notes": "Ook een mail zonder nieuwe Transpas-export is correct verwerkt en krijgt daarom de groene categorie."
+ },
+ {
+ "parameters": {
+ "rule": {
+ "interval": [
+ {
+ "field": "minutes"
+ }
+ ]
+ }
+ },
+ "type": "n8n-nodes-base.scheduleTrigger",
+ "typeVersion": 1.2,
+ "position": [
+ 16,
+ 416
+ ],
+ "id": "685995b6-e0ed-4949-bbd8-d06eddd2f996",
+ "name": "Controleer FTP elke 5 minuten"
+ },
+ {
+ "parameters": {
+ "assignments": {
+ "assignments": [
+ {
+ "id": "66b2d1fd-f300-48be-8521-310ffd0472c2",
+ "name": "ftp_source_folder",
+ "value": "/Flower direct/opdrachten",
+ "type": "string"
+ }
+ ]
+ },
+ "options": {}
+ },
+ "type": "n8n-nodes-base.set",
+ "typeVersion": 3.4,
+ "position": [
+ 240,
+ 416
+ ],
+ "id": "0267471f-1841-49c5-8fdc-c62f28df5de8",
+ "name": "FTP bronmap instellen",
+ "notesInFlow": true,
+ "notes": "Vul hier de FTP-map in waarin Lidl de XLSX-, XLS- of CSV-bestanden plaatst."
+ },
+ {
+ "parameters": {
+ "operation": "list",
+ "path": "={{ $json.ftp_source_folder }}",
+ "options": {}
+ },
+ "type": "n8n-nodes-base.ftp",
+ "typeVersion": 1,
+ "position": [
+ 464,
+ 416
+ ],
+ "id": "178fe9f0-1c61-4d19-b94b-18c784338601",
+ "name": "Toon bestanden in FTP-map",
+ "credentials": {
+ "ftp": {
+ "id": "oLAZ4OgmkOopMHAq",
+ "name": "FTP De Wit Transport"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "jsCode": "// Run Once for All Items\n// Houdt alleen echte XLSX-, XLS- en CSV-bestanden over.\n// Verwerkt bewust maximaal één bronbestand per workflow-run.\n// Daardoor kan dat bronbestand na alle zendingregels precies één keer worden verplaatst.\n\nconst items = $input.all();\nconst candidates = [];\n\nfunction joinPath(folder, name) {\n return `${String(folder ?? '').replace(/\\/+$/, '')}/${String(name ?? '').replace(/^\\/+/, '')}`;\n}\n\nfunction timestamp(value) {\n const date = new Date(value ?? 0);\n return Number.isNaN(date.getTime()) ? 0 : date.getTime();\n}\n\nfor (let index = 0; index < items.length; index++) {\n const item = items[index];\n const j = item.json ?? {};\n\n const type = String(j.type ?? j.entryType ?? '').toLowerCase();\n const isDirectory =\n j.isDirectory === true ||\n j.directory === true ||\n ['directory', 'dir', 'd'].includes(type);\n\n if (isDirectory) continue;\n\n const fileName = String(\n j.name ??\n j.fileName ??\n String(j.path ?? '').split('/').pop() ??\n ''\n ).trim();\n\n if (!fileName) continue;\n\n const extension = String(fileName.split('.').pop() ?? '')\n .toLowerCase()\n .trim();\n\n if (!['xlsx', 'xls', 'csv'].includes(extension)) continue;\n\n const folder = String(\n j.ftp_source_folder ??\n $('FTP bronmap instellen').first().json.ftp_source_folder\n );\n\n candidates.push({\n itemIndex: index,\n sortTime: timestamp(\n j.modifiedTime ??\n j.modified_at ??\n j.modified ??\n j.date ??\n j.timestamp\n ),\n fileName,\n json: {\n ...j,\n source_file_name: fileName,\n source_extension: extension,\n source_ftp_path: joinPath(folder, fileName),\n },\n });\n}\n\n// Oudste bestand eerst. Bij ontbrekende datums wordt op bestandsnaam gesorteerd.\ncandidates.sort((a, b) =>\n (a.sortTime - b.sortTime) ||\n a.fileName.localeCompare(b.fileName)\n);\n\nconst selected = candidates[0];\n\nif (!selected) {\n return [];\n}\n\nreturn [{\n json: selected.json,\n pairedItem: { item: selected.itemIndex },\n}];"
+ },
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 688,
+ 416
+ ],
+ "id": "342f1aa0-48cb-4635-8744-467be0439c1d",
+ "name": "Selecteer FTP-bronbestanden",
+ "notesInFlow": true,
+ "notes": "Selecteert maximaal één CSV/XLS/XLSX per uitvoering. Zo kan één bronbestand met meerdere zendingregels na afloop maar één keer worden verplaatst."
+ },
+ {
+ "parameters": {
+ "path": "={{ $json.source_ftp_path }}",
+ "options": {}
+ },
+ "type": "n8n-nodes-base.ftp",
+ "typeVersion": 1,
+ "position": [
+ 912,
+ 416
+ ],
+ "id": "007781e2-68d5-4e3e-b991-86fc501d2f25",
+ "name": "Download bestand van FTP",
+ "credentials": {
+ "ftp": {
+ "id": "oLAZ4OgmkOopMHAq",
+ "name": "FTP De Wit Transport"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "content": "## FTP-bron\n\n1. De workflow leest `.xlsx`, `.xls` en `.csv`.\n2. Per workflow-run wordt bewust **één bronbestand** verwerkt.\n3. Het bestand mag meerdere zendingregels bevatten.\n4. Na succesvolle verwerking wordt het bronbestand **één keer** verplaatst naar `verwerkt`.\n5. Een volgend bestand wordt bij de volgende geplande uitvoering opgepakt.",
+ "height": 260,
+ "width": 500
+ },
+ "type": "n8n-nodes-base.stickyNote",
+ "typeVersion": 1,
+ "position": [
+ 0,
+ 0
+ ],
+ "id": "99f22c78-9c6f-4853-bb9a-8578f5e32c52",
+ "name": "FTP bron uitleg"
+ },
+ {
+ "parameters": {
+ "operation": "rename",
+ "oldPath": "={{ $('Selecteer Excel-bijlagen').item.json.source_ftp_path }}",
+ "newPath": "={{ $('FTP bronmap instellen').first().json.ftp_source_folder + '/verwerkt/' + $('Selecteer Excel-bijlagen').item.json.source_file_name }}",
+ "options": {}
+ },
+ "type": "n8n-nodes-base.ftp",
+ "typeVersion": 1.1,
+ "position": [
+ 4784,
+ 80
+ ],
+ "id": "3984f2f8-7ca4-4e5a-abe2-025818327ef9",
+ "name": "Verplaats bronbestand naar verwerkt",
+ "executeOnce": true,
+ "notesInFlow": true,
+ "credentials": {
+ "ftp": {
+ "id": "oLAZ4OgmkOopMHAq",
+ "name": "FTP De Wit Transport"
+ }
+ },
+ "notes": "Voert de rename maar één keer uit. De bronselectie verwerkt maximaal één bestand per workflow-run."
+ }
+ ],
+ "pinData": {},
+ "connections": {
+ "Selecteer Excel-bijlagen": {
+ "main": [
+ [
+ {
+ "node": "Bestand is CSV",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Bestand is CSV": {
+ "main": [
+ [
+ {
+ "node": "Detecteer CSV scheidingsteken",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Detecteer CSV scheidingsteken": {
+ "main": [
+ [
+ {
+ "node": "Lees CSV op kolompositie",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Lees CSV op kolompositie": {
+ "main": [
+ [
+ {
+ "node": "Normaliseer en valideer orders",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Normaliseer en valideer orders": {
+ "main": [
+ [
+ {
+ "node": "Upsert order status en zoek pakbon",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Upsert order status en zoek pakbon": {
+ "main": [
+ [
+ {
+ "node": "Alleen nieuwe of gewijzigde revisies",
+ "type": "main",
+ "index": 0
+ },
+ {
+ "node": "Alleen ongewijzigde revisies",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Alleen nieuwe of gewijzigde revisies": {
+ "main": [
+ [
+ {
+ "node": "Met opgeslagen pakbon",
+ "type": "main",
+ "index": 0
+ },
+ {
+ "node": "Zonder pakbonbestand",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Met opgeslagen pakbon": {
+ "main": [
+ [
+ {
+ "node": "Lees pakbon van NAS",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Lees pakbon van NAS": {
+ "main": [
+ [
+ {
+ "node": "Bouw Transpas TPE XML",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Zonder pakbonbestand": {
+ "main": [
+ [
+ {
+ "node": "Bouw Transpas TPE XML",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Bouw Transpas TPE XML": {
+ "main": [
+ [
+ {
+ "node": "Log exportpoging",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Log exportpoging": {
+ "main": [
+ [
+ {
+ "node": "Upload order naar Transpas FTP",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Upload order naar Transpas FTP": {
+ "main": [
+ [
+ {
+ "node": "Markeer export geslaagd",
+ "type": "main",
+ "index": 0
+ }
+ ],
+ [
+ {
+ "node": "Maak FTP foutmelding",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Maak FTP foutmelding": {
+ "main": [
+ [
+ {
+ "node": "Markeer exportfout",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Controleer FTP elke 5 minuten": {
+ "main": [
+ [
+ {
+ "node": "FTP bronmap instellen",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "FTP bronmap instellen": {
+ "main": [
+ [
+ {
+ "node": "Toon bestanden in FTP-map",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Toon bestanden in FTP-map": {
+ "main": [
+ [
+ {
+ "node": "Selecteer FTP-bronbestanden",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Selecteer FTP-bronbestanden": {
+ "main": [
+ [
+ {
+ "node": "Download bestand van FTP",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Download bestand van FTP": {
+ "main": [
+ [
+ {
+ "node": "Selecteer Excel-bijlagen",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Markeer export geslaagd": {
+ "main": [
+ [
+ {
+ "node": "Verplaats bronbestand naar verwerkt",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "active": false,
+ "settings": {
+ "executionOrder": "v1",
+ "binaryMode": "separate",
+ "availableInMCP": false
+ },
+ "versionId": "4f45ad38-9fb7-4099-be89-17417dcdf73a",
+ "meta": {
+ "instanceId": "bef8d409866a58c0777dfe7cca1b9c2400fd051c056d361501393ab423006b5f"
+ },
+ "nodeGroups": [],
+ "id": "LmKzYJsq8CIGXvYJ",
+ "tags": []
+}
\ No newline at end of file
diff --git a/Flower_direct/Lidl Frankrijk - Pakbon naar NAS, De Wit FTP en Transpas.json b/Flower_direct/Lidl Frankrijk - Pakbon naar NAS, De Wit FTP en Transpas.json
new file mode 100644
index 0000000..f8bc4bc
--- /dev/null
+++ b/Flower_direct/Lidl Frankrijk - Pakbon naar NAS, De Wit FTP en Transpas.json
@@ -0,0 +1,700 @@
+{
+ "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 '&',\n )\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\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 '',\n '',\n ` ${at}`,\n ' TLN',\n ' ',\n ` ${xmlEsc(j.transport_number)}`,\n ` ${STATUS_CODE}`,\n ` ${at}`,\n ` ${xmlEsc(`Pakbon ontvangen per e-mail: ${originalFileName}`)}`,\n ` ${xmlEsc(originalFileName)}`,\n ` ${base64}`,\n ' ',\n '',\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": []
+}
\ No newline at end of file