diff --git a/palletways/Palletways BI - Import Statussen.json b/palletways/Palletways BI - Import Statussen.json new file mode 100644 index 0000000..6fd90c4 --- /dev/null +++ b/palletways/Palletways BI - Import Statussen.json @@ -0,0 +1,339 @@ +{ + "name": "Palletways BI - Import Statussen", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "hours", + "hoursInterval": 2 + } + ] + } + }, + "id": "5031836c-8cf3-4375-9fbc-4abe47e6219d", + "name": "Schedule Trigger - elke 2 uur", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + -1312, + 688 + ] + }, + { + "parameters": { + "operation": "executeQuery", + "query": "SELECT\n tracking_id,\n COALESCE(con_no, NULL) AS con_no,\n is_collection_role,\n is_delivery_role,\n is_manifest_role,\n is_manifested,\n is_delivered,\n is_deleted,\n is_never_arrived,\n current_status,\n last_status_poll_at\nFROM palletways_shipments\nWHERE tracking_id IS NOT NULL\nAND COALESCE(status_poll_done, false) = false\nAND (\n (\n is_collection_role = true\n AND COALESCE(is_delivery_role, false) = false\n AND COALESCE(is_manifested, false) = false\n )\n OR\n (\n (\n COALESCE(is_delivery_role, false) = true\n OR COALESCE(is_manifest_role, false) = true\n OR COALESCE(is_collection_role, false) = false\n )\n AND COALESCE(is_delivered, false) = false\n AND COALESCE(is_deleted, false) = false\n AND COALESCE(is_never_arrived, false) = false\n AND COALESCE(current_status, '') NOT IN (\n 'JOB COMPLETE',\n 'DELIVERED',\n 'DELETED',\n 'NEVER ARRIVED'\n )\n )\n)\nAND (\n next_status_poll_at IS NULL\n OR next_status_poll_at <= now()\n)\nORDER BY COALESCE(last_status_poll_at, '2000-01-01'::timestamp) ASC\nLIMIT 500;", + "options": {} + }, + "id": "5d5132f1-03b1-4c8c-879a-e318eabc0c65", + "name": "Postgres - Select shipments to poll", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.6, + "position": [ + -1104, + 688 + ], + "credentials": { + "postgres": { + "id": "a0jnANQ89VLSby0b", + "name": "Palletways DB" + } + } + }, + { + "parameters": { + "options": {} + }, + "id": "013f9249-a851-4d7b-a9b9-e7e250c34839", + "name": "Loop Over Shipments", + "type": "n8n-nodes-base.splitInBatches", + "typeVersion": 3, + "position": [ + -880, + 688 + ] + }, + { + "parameters": { + "jsCode": "const apiKey = 'SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus%3D';\nconst baseUrl = 'https://api.palletways.com/getnotes/trackingid';\n\nconst trackingId = String($json.tracking_id);\n\nreturn [{\n json: {\n ...$json,\n api_key: apiKey,\n status_url: `${baseUrl}/${encodeURIComponent(trackingId)}?apikey=${apiKey}`\n }\n}];" + }, + "id": "052f9605-e43b-42c8-89aa-70a044cf7931", + "name": "Code - Build Palletways URL", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -656, + 560 + ] + }, + { + "parameters": { + "url": "={{ $json.status_url }}", + "options": { + "response": { + "response": { + "responseFormat": "text" + } + }, + "timeout": 120000 + } + }, + "id": "a025af41-cdfe-414b-b132-1244ae3bf316", + "name": "HTTP - Get Palletways notes", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -432, + 560 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "1637a055-5c34-4498-bb8b-5b85cad4b90c", + "name": "XML - Parse notes response", + "type": "n8n-nodes-base.xml", + "typeVersion": 1, + "position": [ + -224, + 560 + ] + }, + { + "parameters": { + "jsCode": "const shipment = $('Code - Build Palletways URL').item.json;\n\nconst response = $json.Response ?? $json;\nconst status = response.Status ?? {};\nconst detail = response.Detail ?? {};\n\nlet data = detail.Data ?? [];\n\nif (!Array.isArray(data)) {\n data = data ? [data] : [];\n}\n\nfunction emptyToNull(value) {\n if (value === undefined || value === null || value === '') return null;\n return value;\n}\n\nfunction toInt(value) {\n if (value === undefined || value === null || value === '') return null;\n const n = Number(value);\n return Number.isFinite(n) ? n : null;\n}\n\nfunction makeTimestamp(date, time) {\n if (!date) return null;\n const t = time || '00:00:00';\n return `${date} ${t}`;\n}\n\nif ((status.Code ?? '').toUpperCase() !== 'OK') {\n return [{\n json: {\n tracking_id: shipment.tracking_id,\n con_no: shipment.con_no,\n api_ok: false,\n api_status_code: status.Code ?? null,\n api_status_description: status.Description ?? null,\n error_text: `Palletways API status niet OK: ${status.Code ?? ''} ${status.Description ?? ''}`,\n has_event: false\n }\n }];\n}\n\nif (data.length === 0) {\n return [{\n json: {\n tracking_id: shipment.tracking_id,\n con_no: shipment.con_no,\n api_ok: true,\n has_event: false,\n no_events: true\n }\n }];\n}\n\nreturn data.map(row => {\n const noteDate = emptyToNull(row.NoteDate);\n const noteTime = emptyToNull(row.NoteTime);\n\n return {\n json: {\n tracking_id: String(row.TrackingID ?? shipment.tracking_id ?? ''),\n con_no: emptyToNull(row.ConNo ?? shipment.con_no),\n delta: toInt(row.Delta),\n sysnote: emptyToNull(row.SysNote),\n barcode: emptyToNull(row.Barcode),\n\n event_at: makeTimestamp(noteDate, noteTime),\n note_date: noteDate,\n note_time: noteTime,\n\n pod_date: emptyToNull(row.PodDate),\n pod_time: emptyToNull(row.PodTime),\n pod_signature: emptyToNull(row.PodSignature),\n\n bookin_date: emptyToNull(row.BookInDate),\n bookin_time: emptyToNull(row.BookInTime),\n bookin_contact: emptyToNull(row.BookInContact),\n bookin_reference: emptyToNull(row.BookInReference),\n\n eta_date: emptyToNull(row.ETADate),\n eta_time: emptyToNull(row.ETATime),\n\n status_code: toInt(row.StatusCode),\n paying_depot: emptyToNull(row.PayingDepot),\n note_group: emptyToNull(row.NoteGroup),\n note_type: emptyToNull(row.NoteType),\n note_code_id: toInt(row.NoteCodeID),\n note_text: emptyToNull(row.NoteText),\n\n raw_json: row,\n api_ok: true,\n has_event: true\n }\n };\n});" + }, + "id": "ba94d04c-0e30-40ca-98ff-f74c6e3eecad", + "name": "Code - Normalize status events", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 16, + 560 + ] + }, + { + "parameters": { + "operation": "executeQuery", + "query": "INSERT INTO palletways_status_events (\n tracking_id,\n con_no,\n delta,\n sysnote,\n barcode,\n event_at,\n note_date,\n note_time,\n pod_date,\n pod_time,\n pod_signature,\n bookin_date,\n bookin_time,\n bookin_contact,\n bookin_reference,\n eta_date,\n eta_time,\n status_code,\n paying_depot,\n note_group,\n note_type,\n note_code_id,\n note_text,\n raw_json\n)\nSELECT\n '{{ $json.tracking_id }}',\n NULLIF('{{ ($json.con_no ?? \"\").toString().replace(/'/g, \"''\") }}', ''),\n {{ $json.delta ?? 'NULL' }},\n NULLIF('{{ ($json.sysnote ?? \"\").toString().replace(/'/g, \"''\") }}', ''),\n NULLIF('{{ ($json.barcode ?? \"\").toString().replace(/'/g, \"''\") }}', ''),\n {{ $json.event_at ? \"'\" + $json.event_at + \"'\" : \"NULL\" }},\n {{ $json.note_date ? \"'\" + $json.note_date + \"'\" : \"NULL\" }},\n {{ $json.note_time ? \"'\" + $json.note_time + \"'\" : \"NULL\" }},\n {{ $json.pod_date ? \"'\" + $json.pod_date + \"'\" : \"NULL\" }},\n {{ $json.pod_time ? \"'\" + $json.pod_time + \"'\" : \"NULL\" }},\n NULLIF('{{ ($json.pod_signature ?? \"\").toString().replace(/'/g, \"''\") }}', ''),\n {{ $json.bookin_date ? \"'\" + $json.bookin_date + \"'\" : \"NULL\" }},\n {{ $json.bookin_time ? \"'\" + $json.bookin_time + \"'\" : \"NULL\" }},\n NULLIF('{{ ($json.bookin_contact ?? \"\").toString().replace(/'/g, \"''\") }}', ''),\n NULLIF('{{ ($json.bookin_reference ?? \"\").toString().replace(/'/g, \"''\") }}', ''),\n {{ $json.eta_date ? \"'\" + $json.eta_date + \"'\" : \"NULL\" }},\n {{ $json.eta_time ? \"'\" + $json.eta_time + \"'\" : \"NULL\" }},\n {{ $json.status_code ?? 'NULL' }},\n NULLIF('{{ ($json.paying_depot ?? \"\").toString().replace(/'/g, \"''\") }}', ''),\n NULLIF('{{ ($json.note_group ?? \"\").toString().replace(/'/g, \"''\") }}', ''),\n NULLIF('{{ ($json.note_type ?? \"\").toString().replace(/'/g, \"''\") }}', ''),\n {{ $json.note_code_id ?? 'NULL' }},\n NULLIF('{{ ($json.note_text ?? \"\").toString().replace(/'/g, \"''\") }}', ''),\n '{{ JSON.stringify($json.raw_json ?? {}).replace(/'/g, \"''\") }}'::jsonb\nWHERE {{ $json.has_event ? 'true' : 'false' }}\nON CONFLICT (tracking_id, sysnote) DO NOTHING;", + "options": {} + }, + "id": "1b860a8c-8191-49b7-a2c1-3e5f174c7ed3", + "name": "Postgres - Insert status event", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.6, + "position": [ + 224, + 560 + ], + "credentials": { + "postgres": { + "id": "a0jnANQ89VLSby0b", + "name": "Palletways DB" + } + } + }, + { + "parameters": { + "operation": "executeQuery", + "query": "WITH agg AS (\n SELECT\n e.tracking_id,\n\n MIN(e.event_at) FILTER (\n WHERE e.note_group = 'POC'\n AND e.note_type = 'POC'\n AND e.note_code_id = 219999\n ) AS collected_at,\n\n MIN(e.event_at) FILTER (\n WHERE e.note_group = 'JIN'\n AND e.note_type = 'IIN'\n AND e.note_code_id = 52001\n AND (\n e.note_text ILIKE '%Verzonden%'\n OR e.note_text ILIKE '%Shipped%'\n )\n ) AS manifested_at,\n\n MAX(COALESCE(\n CASE\n WHEN e.pod_date IS NOT NULL AND e.pod_time IS NOT NULL\n THEN e.pod_date::timestamp + e.pod_time\n ELSE NULL\n END,\n e.event_at\n )) FILTER (\n WHERE e.status_code = 900\n AND e.note_group = 'POD'\n AND e.note_type = 'POD'\n AND e.note_code_id = 36\n ) AS delivered_at,\n\n MAX(e.event_at) AS last_event_at\n FROM palletways_status_events e\n WHERE e.tracking_id = '{{ $(\"Code - Build Palletways URL\").item.json.tracking_id }}'\n GROUP BY e.tracking_id\n),\nlatest AS (\n SELECT DISTINCT ON (e.tracking_id)\n e.tracking_id,\n e.event_at,\n e.interpreted_status AS derived_status,\n e.status_phase,\n e.status_priority\n FROM palletways_status_events e\n WHERE e.tracking_id = '{{ $(\"Code - Build Palletways URL\").item.json.tracking_id }}'\n AND e.interpreted_status IS NOT NULL\n AND e.interpreted_status NOT IN (\n 'GENERAL INFORMATION',\n 'CUSTOMER SERVICE NOTE',\n 'POD IMAGE',\n 'UNKNOWN'\n )\n ORDER BY\n e.tracking_id,\n e.status_priority DESC,\n e.event_at DESC NULLS LAST,\n e.id DESC\n)\nUPDATE palletways_shipments s\nSET\n is_collected = COALESCE(s.is_collected, false) OR agg.collected_at IS NOT NULL,\n collected_at = COALESCE(s.collected_at, agg.collected_at),\n\n is_manifested = COALESCE(s.is_manifested, false) OR agg.manifested_at IS NOT NULL,\n manifested_at = COALESCE(s.manifested_at, agg.manifested_at),\n\n is_delivered = COALESCE(s.is_delivered, false) OR agg.delivered_at IS NOT NULL,\n delivered_at = COALESCE(s.delivered_at, agg.delivered_at),\n\n current_status = CASE\n WHEN agg.delivered_at IS NOT NULL THEN 'DELIVERED'\n ELSE COALESCE(latest.derived_status, s.current_status, 'NO EVENTS')\n END,\n\n current_status_at = COALESCE(\n agg.delivered_at,\n latest.event_at,\n agg.last_event_at,\n s.current_status_at\n ),\n\n last_status_poll_at = now(),\n\n next_status_poll_at = CASE\n WHEN s.is_collection_role = true\n AND COALESCE(s.is_delivery_role, false) = false\n AND agg.manifested_at IS NOT NULL\n THEN NULL\n\n WHEN agg.delivered_at IS NOT NULL\n THEN NULL\n\n ELSE now() + interval '2 hours'\n END,\n\n status_poll_done = CASE\n WHEN s.is_collection_role = true\n AND COALESCE(s.is_delivery_role, false) = false\n AND agg.manifested_at IS NOT NULL\n THEN true\n\n WHEN agg.delivered_at IS NOT NULL\n THEN true\n\n ELSE false\n END,\n\n status_source = 'palletways_notes_api',\n updated_at = now()\nFROM agg\nLEFT JOIN latest ON latest.tracking_id = agg.tracking_id\nWHERE s.tracking_id = agg.tracking_id\nAND s.tracking_id = '{{ $(\"Code - Build Palletways URL\").item.json.tracking_id }}';", + "options": {} + }, + "id": "ec1a02cf-8dc0-4529-8b15-d6db60fd39d9", + "name": "Postgres - Recalculate shipment state", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.6, + "position": [ + 576, + 560 + ], + "credentials": { + "postgres": { + "id": "a0jnANQ89VLSby0b", + "name": "Palletways DB" + } + } + }, + { + "parameters": {}, + "id": "a23f3a53-d305-4ea0-a9da-1b8edaabbd24", + "name": "Done", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + -656, + 832 + ] + }, + { + "parameters": { + "operation": "executeQuery", + "query": "UPDATE palletways_status_events\nSET\n interpreted_status = CASE\n WHEN note_group = 'JIN' AND note_type = 'COLRQ' AND note_code_id = 353999 THEN 'COLLECTION REQUESTED'\n WHEN note_group = 'JIN' AND note_type = 'COLAC' AND note_code_id = 352999 THEN 'COLLECTION ACCEPTED'\n WHEN note_group = 'POC' AND note_type = 'POC' AND note_code_id = 219999 THEN 'COLLECTED'\n WHEN note_group = 'JIN' AND note_type = 'IIN' AND note_code_id = 52001 AND (note_text ILIKE '%Verzonden%' OR note_text ILIKE '%Shipped%') THEN 'MANIFESTED'\n WHEN status_code = 300 AND note_group = 'VL' AND note_type = 'SCN' AND note_code_id = 48001 THEN 'LOADED COLLECTION VEHICLE'\n WHEN status_code = 500 AND note_group = 'VU' AND note_type = 'SCN' AND note_code_id = 49001 THEN 'UNLOADED AT HUB'\n WHEN status_code = 500 AND note_group = 'BAY' AND note_type = 'SCN' AND note_code_id = 47001 THEN 'SCANNED IN BAY'\n WHEN note_group = 'LH' AND note_type = 'LLD' AND note_code_id = 101999 THEN 'LINEHAUL LOAD'\n WHEN note_group = 'LH' AND note_type = 'LUL' AND note_code_id = 102999 THEN 'LINEHAUL UNLOAD'\n WHEN note_group = 'LH' AND note_type = 'LBA' AND note_code_id = 103999 THEN 'LINEHAUL BAYCHECK'\n WHEN status_code = 550 AND note_group = 'JIN' AND note_type = 'GDEP' AND note_code_id = 348999 THEN 'LEFT HUB'\n WHEN status_code = 700 AND note_group = 'VU' AND note_type = 'SCN' AND note_code_id = 49001 THEN 'AT DELIVERY DEPOT'\n WHEN status_code = 800 AND note_group = 'VL' AND note_type = 'SCN' AND note_code_id = 48001 THEN 'OUT FOR DELIVERY'\n WHEN note_group = 'VP' AND note_type = 'PDT' AND note_code_id = 218999 THEN 'ETA PLANNED'\n WHEN note_group = 'VP' AND note_type = 'DAR' AND note_code_id = 307999 THEN 'DRIVER ARRIVED'\n WHEN status_code = 900 AND note_group = 'POD' AND note_type = 'POD' AND note_code_id = 36 THEN 'DELIVERED'\n WHEN note_group = 'POD' AND note_type = 'IMG' AND note_code_id = 67999 THEN 'POD IMAGE'\n WHEN note_group = 'BIN' AND note_type = 'BIN' AND note_code_id = 32 THEN 'BOOKED IN'\n WHEN note_group = 'CUSV' AND note_type = 'CUSN' AND note_code_id = 297999 THEN 'CUSTOMER SERVICE NOTE'\n WHEN note_group = 'JIN' AND note_type IN ('IIN', 'GIN') THEN 'GENERAL INFORMATION'\n ELSE 'UNKNOWN'\n END,\n\n status_phase = CASE\n WHEN note_group = 'JIN' AND note_type IN ('COLRQ', 'COLAC') THEN 'COLLECTION_PLANNING'\n WHEN note_group = 'POC' AND note_type = 'POC' THEN 'COLLECTED'\n WHEN note_group = 'JIN' AND note_type = 'IIN' AND note_code_id = 52001 AND (note_text ILIKE '%Verzonden%' OR note_text ILIKE '%Shipped%') THEN 'MANIFESTED'\n WHEN status_code IN (300, 500, 525, 530, 550) OR note_group IN ('LH', 'BAY') THEN 'IN_NETWORK'\n WHEN status_code = 700 THEN 'DELIVERY_DEPOT'\n WHEN status_code = 800 OR note_group = 'VP' THEN 'DELIVERY_ROUTE'\n WHEN status_code = 900 AND note_group = 'POD' THEN 'DELIVERED'\n WHEN note_group = 'BIN' THEN 'BOOKING'\n ELSE 'INFO'\n END,\n\n status_priority = CASE\n WHEN status_code = 900 AND note_group = 'POD' THEN 100\n WHEN note_group = 'VP' AND note_type = 'DAR' THEN 95\n WHEN status_code = 800 THEN 90\n WHEN status_code = 700 THEN 80\n WHEN status_code = 550 THEN 70\n WHEN note_group = 'LH' THEN 65\n WHEN status_code = 500 THEN 60\n WHEN status_code = 300 THEN 50\n WHEN note_group = 'JIN' AND note_type = 'IIN' AND (note_text ILIKE '%Verzonden%' OR note_text ILIKE '%Shipped%') THEN 40\n WHEN note_group = 'POC' THEN 30\n WHEN note_type = 'COLAC' THEN 20\n WHEN note_type = 'COLRQ' THEN 10\n ELSE 0\n END\nWHERE tracking_id = '{{ $(\"Code - Build Palletways URL\").item.json.tracking_id }}';", + "options": {} + }, + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.6, + "position": [ + 432, + 560 + ], + "id": "2a3b5067-4cd5-450d-8f8c-abf2263aba0e", + "name": "Execute a SQL query", + "credentials": { + "postgres": { + "id": "a0jnANQ89VLSby0b", + "name": "Palletways DB" + } + } + } + ], + "pinData": {}, + "connections": { + "Schedule Trigger - elke 2 uur": { + "main": [ + [ + { + "node": "Postgres - Select shipments to poll", + "type": "main", + "index": 0 + } + ] + ] + }, + "Postgres - Select shipments to poll": { + "main": [ + [ + { + "node": "Loop Over Shipments", + "type": "main", + "index": 0 + } + ] + ] + }, + "Loop Over Shipments": { + "main": [ + [ + { + "node": "Done", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Code - Build Palletways URL", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Build Palletways URL": { + "main": [ + [ + { + "node": "HTTP - Get Palletways notes", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP - Get Palletways notes": { + "main": [ + [ + { + "node": "XML - Parse notes response", + "type": "main", + "index": 0 + } + ] + ] + }, + "XML - Parse notes response": { + "main": [ + [ + { + "node": "Code - Normalize status events", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code - Normalize status events": { + "main": [ + [ + { + "node": "Postgres - Insert status event", + "type": "main", + "index": 0 + } + ] + ] + }, + "Postgres - Insert status event": { + "main": [ + [ + { + "node": "Execute a SQL query", + "type": "main", + "index": 0 + } + ] + ] + }, + "Postgres - Recalculate shipment state": { + "main": [ + [ + { + "node": "Loop Over Shipments", + "type": "main", + "index": 0 + } + ] + ] + }, + "Execute a SQL query": { + "main": [ + [ + { + "node": "Postgres - Recalculate shipment state", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "timezone": "Europe/London", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false, + "timeSavedMode": "fixed", + "errorWorkflow": "1Ps5lukDf2sgcGL7", + "saveDataSuccessExecution": "all" + }, + "versionId": "7dc4122f-1471-417b-bbcb-6a07cf68b1e1", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "bef8d409866a58c0777dfe7cca1b9c2400fd051c056d361501393ab423006b5f" + }, + "nodeGroups": [], + "id": "0EPEzArBNT8pvV6h", + "tags": [ + { + "updatedAt": "2025-11-11T04:59:39.416Z", + "createdAt": "2025-11-11T04:59:39.416Z", + "id": "ClOZrAjJKoGLFvaO", + "name": "Palletways" + } + ] +} \ No newline at end of file