Files
n8n-workflows/palletways/Palletways BI - Import Statussen.json
T

370 lines
33 KiB
JSON

{
"name": "Palletways BI - Import Statussen",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 2
}
]
}
},
"id": "99e52813-bb87-4d86-b436-59ad811a5983",
"name": "Schedule Trigger - elke 2 uur",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
1056,
1792
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "ALTER TABLE palletways_shipments\nADD COLUMN IF NOT EXISTS summary_status text,\nADD COLUMN IF NOT EXISTS summary_status_at timestamp,\nADD COLUMN IF NOT EXISTS last_note_status text,\nADD COLUMN IF NOT EXISTS last_note_text text,\nADD COLUMN IF NOT EXISTS last_note_at timestamp;\n\nALTER TABLE palletways_status_events\nADD COLUMN IF NOT EXISTS interpreted_status text,\nADD COLUMN IF NOT EXISTS status_phase text,\nADD COLUMN IF NOT EXISTS status_priority int;\n\nCREATE INDEX IF NOT EXISTS idx_pw_status_events_tracking\nON palletways_status_events (tracking_id);\n\nCREATE INDEX IF NOT EXISTS idx_pw_status_events_tracking_event\nON palletways_status_events (tracking_id, event_at DESC);\n\nCREATE INDEX IF NOT EXISTS idx_pw_shipments_poll\nON palletways_shipments (status_poll_done, next_status_poll_at);\n\nCREATE INDEX IF NOT EXISTS idx_pw_shipments_current_status\nON palletways_shipments (current_status);",
"options": {}
},
"id": "8a8f7096-4585-4f3e-a460-542cb94e0b03",
"name": "Postgres - Setup columns and indexes",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
1280,
1792
],
"credentials": {
"postgres": {
"id": "a0jnANQ89VLSby0b",
"name": "Palletways DB"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT\n tracking_id,\n COALESCE(con_no, consignment_number) 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 next_status_poll_at IS NULL\n OR next_status_poll_at <= now()\n)\nAND COALESCE(is_deleted, false) = false\nAND COALESCE(is_never_arrived, false) = false\nAND upper(COALESCE(current_status, '')) NOT IN (\n 'DELETED',\n 'DELETE',\n 'JOB DELETED',\n 'CANCELLED',\n 'CANCELED',\n 'NEVER ARRIVED',\n 'NEVERARRIVED',\n 'JOB COMPLETE',\n 'DELIVERED'\n)\nORDER BY COALESCE(last_status_poll_at, '2000-01-01'::timestamp) ASC\nLIMIT 500;",
"options": {}
},
"id": "460ca8cd-328e-4bad-b5e8-960407c9bfbb",
"name": "Postgres - Select shipments to poll",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
1504,
1792
],
"credentials": {
"postgres": {
"id": "a0jnANQ89VLSby0b",
"name": "Palletways DB"
}
}
},
{
"parameters": {
"options": {}
},
"id": "1aed29a8-70bc-4196-9bb8-7edd842a26c4",
"name": "Loop Over Shipments",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
1728,
1792
]
},
{
"parameters": {
"jsCode": "const apiKey = 'SXJaM7PLjZDqfsnXSDP8Y9wDY6crxJySBg705MQEPus%3D';\n\nconst trackingId = String($json.tracking_id);\n\nreturn [{\n json: {\n ...$json,\n api_key: apiKey,\n notes_url: `https://api.palletways.com/getnotes/trackingid/${encodeURIComponent(trackingId)}?apikey=${apiKey}`,\n summary_url: `https://api.palletways.com/gettrackingnotes/${encodeURIComponent(trackingId)}?apikey=${apiKey}`,\n }\n}];"
},
"id": "08ec873b-e824-4ade-9ff4-43f485b479e0",
"name": "Code - Build URLs",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1936,
1648
]
},
{
"parameters": {
"url": "={{ $json.notes_url }}",
"options": {
"response": {
"response": {
"responseFormat": "text"
}
},
"timeout": 120000
}
},
"id": "3ec6f3c9-2236-4dae-a219-617d270d6add",
"name": "HTTP - Get detailed notes",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2160,
1648
]
},
{
"parameters": {
"jsCode": "const shipment = $('Code - Build URLs').item.json;\n\nfunction getXmlFromJson(j) {\n if (typeof j === 'string') return j;\n if (typeof j.data === 'string') return j.data;\n if (typeof j.body === 'string') return j.body;\n if (typeof j.response === 'string') return j.response;\n\n for (const v of Object.values(j)) {\n if (typeof v === 'string' && v.trim().startsWith('<')) return v;\n }\n\n return JSON.stringify(j);\n}\n\nfunction xmlUnescape(s) {\n if (s === null || s === undefined) return null;\n return String(s)\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&amp;/g, '&')\n .replace(/&quot;/g, '\"')\n .replace(/&apos;/g, \"'\");\n}\n\nfunction tag(block, name) {\n const re = new RegExp(`<${name}>([\\\\s\\\\S]*?)<\\\\/${name}>`, 'i');\n const m = String(block || '').match(re);\n return m ? xmlUnescape(m[1]).trim() : null;\n}\n\nfunction dataBlocks(xml) {\n const blocks = [];\n const re = /<Data>([\\s\\S]*?)<\\/Data>/gi;\n let m;\n while ((m = re.exec(String(xml || ''))) !== null) {\n blocks.push(m[1]);\n }\n return blocks;\n}\n\nfunction emptyToNull(v) {\n if (v === undefined || v === null || v === '') return null;\n return v;\n}\n\nfunction toInt(v) {\n if (v === undefined || v === null || v === '') return null;\n const n = Number(v);\n return Number.isFinite(n) ? n : null;\n}\n\nfunction makeTimestamp(date, time) {\n if (!date) return null;\n return `${date} ${time || '00:00:00'}`;\n}\n\nconst xml = getXmlFromJson($json);\n\nconst events = dataBlocks(xml).map(row => {\n const noteDate = emptyToNull(tag(row, 'NoteDate'));\n const noteTime = emptyToNull(tag(row, 'NoteTime'));\n\n return {\n tracking_id: String(tag(row, 'TrackingID') || shipment.tracking_id || ''),\n con_no: emptyToNull(tag(row, 'ConNo') || shipment.con_no),\n delta: toInt(tag(row, 'Delta')),\n sysnote: emptyToNull(tag(row, 'SysNote')),\n barcode: emptyToNull(tag(row, 'Barcode')),\n\n event_at: makeTimestamp(noteDate, noteTime),\n note_date: noteDate,\n note_time: noteTime,\n\n pod_date: emptyToNull(tag(row, 'PodDate')),\n pod_time: emptyToNull(tag(row, 'PodTime')),\n pod_signature: emptyToNull(tag(row, 'PodSignature')),\n\n bookin_date: emptyToNull(tag(row, 'BookInDate')),\n bookin_time: emptyToNull(tag(row, 'BookInTime')),\n bookin_contact: emptyToNull(tag(row, 'BookInContact')),\n bookin_reference: emptyToNull(tag(row, 'BookInReference')),\n\n eta_date: emptyToNull(tag(row, 'ETADate')),\n eta_time: emptyToNull(tag(row, 'ETATime')),\n\n status_code: toInt(tag(row, 'StatusCode')),\n paying_depot: emptyToNull(tag(row, 'PayingDepot')),\n note_group: emptyToNull(tag(row, 'NoteGroup')),\n note_type: emptyToNull(tag(row, 'NoteType')),\n note_code_id: toInt(tag(row, 'NoteCodeID')),\n note_text: emptyToNull(tag(row, 'NoteText')),\n\n raw_json: {\n raw_xml: row\n }\n };\n}).filter(e => e.tracking_id && (e.sysnote || e.note_text || e.note_code_id !== null || e.status_code !== null));\n\nreturn [{\n json: {\n ...shipment,\n events_json: JSON.stringify(events),\n event_count: events.length,\n }\n}];"
},
"id": "ef6367ca-1d6c-43a6-bea4-0795e47693fa",
"name": "Code - Parse notes",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2384,
1648
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "WITH input AS (\n SELECT\n '{{ $json.tracking_id }}'::text AS tracking_id,\n NULLIF('{{ ($json.con_no ?? \"\").toString().replace(/'/g, \"''\") }}', '')::text AS con_no,\n '{{ ($json.notes_url ?? \"\").toString().replace(/'/g, \"''\") }}'::text AS notes_url,\n '{{ ($json.summary_url ?? \"\").toString().replace(/'/g, \"''\") }}'::text AS summary_url,\n '{{ ($json.events_json ?? \"[]\").replace(/'/g, \"''\") }}'::jsonb AS events_json\n),\n\nevents_raw AS (\n SELECT\n x.*\n FROM input i\n CROSS JOIN LATERAL jsonb_to_recordset(i.events_json) AS x(\n tracking_id text,\n con_no text,\n delta bigint,\n sysnote text,\n barcode text,\n event_at timestamp,\n note_date date,\n note_time time,\n pod_date date,\n pod_time time,\n pod_signature text,\n bookin_date date,\n bookin_time time,\n bookin_contact text,\n bookin_reference text,\n eta_date date,\n eta_time time,\n status_code int,\n paying_depot text,\n note_group text,\n note_type text,\n note_code_id int,\n note_text text,\n raw_json jsonb\n )\n),\n\nevents_deduplicated AS (\n SELECT\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 FROM (\n SELECT\n e.*,\n ROW_NUMBER() OVER (\n PARTITION BY\n e.tracking_id,\n COALESCE(e.sysnote, ''),\n COALESCE(e.note_group, ''),\n COALESCE(e.note_type, ''),\n COALESCE(e.note_code_id::text, ''),\n COALESCE(e.status_code::text, ''),\n COALESCE(e.event_at::text, ''),\n COALESCE(e.note_text, '')\n ORDER BY\n e.event_at DESC NULLS LAST,\n e.delta DESC NULLS LAST,\n e.note_text DESC NULLS LAST\n ) AS rn\n FROM events_raw e\n WHERE e.tracking_id IS NOT NULL\n ) ranked\n WHERE rn = 1\n),\n\ninsert_events AS (\n 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 )\n SELECT\n e.tracking_id,\n e.con_no,\n e.delta,\n e.sysnote,\n e.barcode,\n e.event_at,\n e.note_date,\n e.note_time,\n e.pod_date,\n e.pod_time,\n e.pod_signature,\n e.bookin_date,\n e.bookin_time,\n e.bookin_contact,\n e.bookin_reference,\n e.eta_date,\n e.eta_time,\n e.status_code,\n e.paying_depot,\n e.note_group,\n e.note_type,\n e.note_code_id,\n e.note_text,\n e.raw_json\n FROM events_deduplicated e\n ON CONFLICT DO NOTHING\n RETURNING tracking_id\n),\n\nclassify AS (\n UPDATE palletways_status_events e\n SET\n interpreted_status = CASE\n\n WHEN e.note_group = 'JIN'\n AND e.note_type = 'IIN'\n AND e.note_code_id = 52001\n AND (\n e.note_text ILIKE '%successfully deleted%'\n OR e.note_text ILIKE '%consignment%deleted%'\n OR e.note_text ILIKE '%zending%verwijderd%'\n )\n THEN 'DELETED'\n\n WHEN e.note_group = 'JIN'\n AND e.note_type = 'COLRQ'\n AND e.note_code_id = 353999\n THEN 'COLLECTION REQUESTED'\n\n WHEN e.note_group = 'JIN'\n AND e.note_type = 'COLAC'\n AND e.note_code_id = 352999\n THEN 'COLLECTION ACCEPTED'\n\n WHEN e.note_group = 'POC'\n AND e.note_type = 'POC'\n AND e.note_code_id = 219999\n THEN 'COLLECTED'\n\n WHEN 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 THEN 'MANIFESTED'\n\n WHEN e.status_code = 300\n AND e.note_group = 'VL'\n AND e.note_type = 'SCN'\n AND e.note_code_id = 48001\n THEN 'LOADED COLLECTION VEHICLE'\n\n WHEN e.status_code = 500\n AND e.note_group = 'VU'\n AND e.note_type = 'SCN'\n AND e.note_code_id = 49001\n THEN 'UNLOADED AT HUB'\n\n WHEN e.status_code = 500\n AND e.note_group = 'BAY'\n AND e.note_type = 'SCN'\n AND e.note_code_id = 47001\n THEN 'SCANNED IN BAY'\n\n WHEN e.note_group = 'LH'\n AND e.note_type = 'LLD'\n AND e.note_code_id = 101999\n THEN 'LINEHAUL LOAD'\n\n WHEN e.note_group = 'LH'\n AND e.note_type = 'LUL'\n AND e.note_code_id = 102999\n THEN 'LINEHAUL UNLOAD'\n\n WHEN e.note_group = 'LH'\n AND e.note_type = 'LBA'\n AND e.note_code_id = 103999\n THEN 'LINEHAUL BAYCHECK'\n\n WHEN e.status_code = 550\n AND e.note_group = 'JIN'\n AND e.note_type = 'GDEP'\n AND e.note_code_id = 348999\n THEN 'LEFT HUB'\n\n WHEN e.status_code = 700\n AND e.note_group = 'VU'\n AND e.note_type = 'SCN'\n AND e.note_code_id = 49001\n THEN 'AT DELIVERY DEPOT'\n\n WHEN e.status_code = 800\n AND e.note_group = 'VL'\n AND e.note_type = 'SCN'\n AND e.note_code_id = 48001\n THEN 'OUT FOR DELIVERY'\n\n WHEN e.note_group = 'VP'\n AND e.note_type = 'PDT'\n AND e.note_code_id = 218999\n THEN 'ETA PLANNED'\n\n WHEN e.note_group = 'VP'\n AND e.note_type = 'DAR'\n AND e.note_code_id = 307999\n THEN 'DRIVER ARRIVED'\n\n WHEN e.status_code = 900\n AND e.note_group = 'POD'\n AND e.note_type = 'POD'\n AND e.note_code_id = 36\n THEN 'DELIVERED'\n\n WHEN e.note_group = 'POD'\n AND e.note_type = 'IMG'\n AND e.note_code_id = 67999\n THEN 'POD IMAGE'\n\n WHEN e.note_group = 'BIN'\n AND e.note_type = 'BIN'\n AND e.note_code_id = 32\n THEN 'BOOKED IN'\n\n WHEN e.note_group = 'CUSV'\n AND e.note_type = 'CUSN'\n AND e.note_code_id = 297999\n THEN 'CUSTOMER SERVICE NOTE'\n\n WHEN e.note_group = 'JIN'\n AND e.note_type IN ('IIN', 'GIN')\n THEN 'GENERAL INFORMATION'\n\n ELSE 'UNKNOWN'\n END,\n\n status_phase = CASE\n\n WHEN (\n e.note_text ILIKE '%successfully deleted%'\n OR e.note_text ILIKE '%consignment%deleted%'\n OR e.note_text ILIKE '%zending%verwijderd%'\n )\n THEN 'TERMINAL'\n\n WHEN e.note_group = 'JIN'\n AND e.note_type IN ('COLRQ', 'COLAC')\n THEN 'COLLECTION_PLANNING'\n\n WHEN e.note_group = 'POC'\n AND e.note_type = 'POC'\n THEN 'COLLECTED'\n\n WHEN 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 THEN 'MANIFESTED'\n\n WHEN e.status_code IN (300, 500, 525, 530, 550)\n OR e.note_group IN ('LH', 'BAY')\n THEN 'IN_NETWORK'\n\n WHEN e.status_code = 700\n THEN 'DELIVERY_DEPOT'\n\n WHEN e.status_code = 800\n OR e.note_group = 'VP'\n THEN 'DELIVERY_ROUTE'\n\n WHEN e.status_code = 900\n AND e.note_group = 'POD'\n THEN 'DELIVERED'\n\n WHEN e.note_group = 'BIN'\n THEN 'BOOKING'\n\n ELSE 'INFO'\n END,\n\n status_priority = CASE\n\n WHEN (\n e.note_text ILIKE '%successfully deleted%'\n OR e.note_text ILIKE '%consignment%deleted%'\n OR e.note_text ILIKE '%zending%verwijderd%'\n )\n THEN 110\n\n WHEN e.status_code = 900\n AND e.note_group = 'POD'\n THEN 100\n\n WHEN e.note_group = 'VP'\n AND e.note_type = 'DAR'\n THEN 95\n\n WHEN e.status_code = 800\n THEN 90\n\n WHEN e.status_code = 700\n THEN 80\n\n WHEN e.status_code = 550\n THEN 70\n\n WHEN e.note_group = 'LH'\n THEN 65\n\n WHEN e.status_code = 500\n THEN 60\n\n WHEN e.status_code = 300\n THEN 50\n\n WHEN e.note_group = 'JIN'\n AND e.note_type = 'IIN'\n AND (\n e.note_text ILIKE '%Verzonden%'\n OR e.note_text ILIKE '%Shipped%'\n )\n THEN 40\n\n WHEN e.note_group = 'POC'\n THEN 30\n\n WHEN e.note_type = 'COLAC'\n THEN 20\n\n WHEN e.note_type = 'COLRQ'\n THEN 10\n\n ELSE 0\n END\n\n FROM input i\n WHERE e.tracking_id = i.tracking_id\n\n RETURNING e.tracking_id\n),\n\nagg 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(\n COALESCE(\n CASE\n WHEN e.pod_date IS NOT NULL\n 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 )\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 pod_delivered_at,\n\n MAX(e.event_at) FILTER (\n WHERE e.interpreted_status = 'DELETED'\n ) AS deleted_at,\n\n MAX(e.event_at) AS last_event_at\n\n FROM palletways_status_events e\n JOIN input i\n ON i.tracking_id = e.tracking_id\n\n GROUP BY e.tracking_id\n),\n\nlatest_progress AS (\n SELECT DISTINCT ON (e.tracking_id)\n e.tracking_id,\n e.event_at,\n e.interpreted_status,\n e.note_text\n\n FROM palletways_status_events e\n JOIN input i\n ON i.tracking_id = e.tracking_id\n\n WHERE 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\n ORDER BY\n e.tracking_id,\n e.status_priority DESC,\n e.event_at DESC NULLS LAST,\n e.id DESC\n),\n\nlatest_note AS (\n SELECT DISTINCT ON (e.tracking_id)\n e.tracking_id,\n e.event_at,\n e.note_text\n\n FROM palletways_status_events e\n JOIN input i\n ON i.tracking_id = e.tracking_id\n\n ORDER BY\n e.tracking_id,\n e.event_at DESC NULLS LAST,\n e.id DESC\n),\n\nupdate_shipments AS (\nUPDATE palletways_shipments s\nSET\n is_collected =\n COALESCE(s.is_collected, false)\n OR agg.collected_at IS NOT NULL,\n\n collected_at =\n COALESCE(\n s.collected_at,\n agg.collected_at\n ),\n\n is_manifested =\n COALESCE(s.is_manifested, false)\n OR agg.manifested_at IS NOT NULL,\n\n manifested_at =\n COALESCE(\n s.manifested_at,\n agg.manifested_at\n ),\n\n is_deleted =\n COALESCE(s.is_deleted, false)\n OR agg.deleted_at IS NOT NULL,\n\n deleted_at =\n COALESCE(\n s.deleted_at,\n agg.deleted_at\n ),\n\n is_delivered =\n COALESCE(s.is_delivered, false)\n OR agg.pod_delivered_at IS NOT NULL,\n\n delivered_at =\n COALESCE(\n s.delivered_at,\n agg.pod_delivered_at\n ),\n\n last_note_status =\n latest_progress.interpreted_status,\n\n last_note_text =\n latest_note.note_text,\n\n last_note_at =\n latest_note.event_at,\n\n updated_at =\n now()\n\nFROM agg\n\nLEFT JOIN latest_progress\n ON latest_progress.tracking_id = agg.tracking_id\n\nLEFT JOIN latest_note\n ON latest_note.tracking_id = agg.tracking_id\n\nWHERE s.tracking_id = agg.tracking_id\nRETURNING s.tracking_id\n)\n\nSELECT\n i.tracking_id,\n i.con_no,\n i.notes_url,\n i.summary_url,\n COALESCE((SELECT COUNT(*) FROM events_deduplicated), 0)::int AS parsed_event_count,\n COALESCE((SELECT COUNT(*) FROM insert_events), 0)::int AS inserted_event_count,\n COALESCE((SELECT COUNT(*) FROM classify), 0)::int AS classified_event_count,\n COALESCE((SELECT COUNT(*) FROM update_shipments), 0)::int AS shipment_note_update_count\nFROM input i;",
"options": {}
},
"id": "c0c3732f-3250-4bf3-ba2a-30b5ac21aaaf",
"name": "Postgres - Upsert notes and note fields",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
2624,
1648
],
"credentials": {
"postgres": {
"id": "a0jnANQ89VLSby0b",
"name": "Palletways DB"
}
}
},
{
"parameters": {
"url": "={{ $json.summary_url }}",
"options": {
"response": {
"response": {
"responseFormat": "text"
}
},
"timeout": 120000
}
},
"id": "6cff9996-5235-4d8a-af5c-805c5e8bcd84",
"name": "HTTP - Get summary status",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2864,
1648
]
},
{
"parameters": {
"jsCode": "const shipment = $('Postgres - Upsert notes and note fields').item.json;\n\nfunction getXmlFromJson(j) {\n if (typeof j === 'string') return j;\n if (typeof j.data === 'string') return j.data;\n if (typeof j.body === 'string') return j.body;\n if (typeof j.response === 'string') return j.response;\n\n for (const v of Object.values(j)) {\n if (typeof v === 'string' && v.trim().startsWith('<')) return v;\n }\n\n return JSON.stringify(j);\n}\n\nfunction xmlUnescape(s) {\n if (s === null || s === undefined) return null;\n return String(s)\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/&amp;/g, '&')\n .replace(/&quot;/g, '\"')\n .replace(/&apos;/g, \"'\");\n}\n\nfunction tag(block, name) {\n const re = new RegExp(`<${name}>([\\\\s\\\\S]*?)<\\\\/${name}>`, 'i');\n const m = String(block || '').match(re);\n return m ? xmlUnescape(m[1]).trim() : null;\n}\n\nfunction dataBlocks(xml) {\n const blocks = [];\n const re = /<Data>([\\s\\S]*?)<\\/Data>/gi;\n let m;\n while ((m = re.exec(String(xml || ''))) !== null) {\n blocks.push(m[1]);\n }\n return blocks;\n}\n\nconst xml = getXmlFromJson($json);\nconst block = dataBlocks(xml)[0] || '';\n\nconst summary = {\n tracking_id: tag(block, 'TrackingID') || shipment.tracking_id,\n con_no: tag(block, 'ConNo') || shipment.con_no,\n date_entered: tag(block, 'DateEntered'),\n cust_ref: tag(block, 'CustRef'),\n lifts: tag(block, 'Lifts'),\n weight: tag(block, 'Weight'),\n status: tag(block, 'Status'),\n barcodes: tag(block, 'Barcodes'),\n col_depot: tag(block, 'ColDepot'),\n del_depot: tag(block, 'DelDepot'),\n};\n\nreturn [{\n json: {\n ...shipment,\n summary_status: summary.status ? String(summary.status).trim() : null,\n summary_raw: summary,\n }\n}];"
},
"id": "97d97299-d6b9-449a-bb3e-89937836c9e9",
"name": "Code - Parse summary status",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3088,
1648
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "WITH input AS (\n SELECT\n '{{ $json.tracking_id }}'::text AS tracking_id,\n NULLIF('{{ ($json.summary_status ?? \"\").toString().replace(/'/g, \"''\") }}', '')::text AS summary_status,\n '{{ JSON.stringify($json.summary_raw ?? {}).replace(/'/g, \"''\") }}'::jsonb AS summary_raw\n)\nUPDATE palletways_shipments s\nSET\n con_no = COALESCE(s.con_no, input.summary_raw->>'con_no'),\n\n summary_status = COALESCE(input.summary_status, s.summary_status),\n summary_status_at = CASE\n WHEN input.summary_status IS NOT NULL THEN now()\n ELSE s.summary_status_at\n END,\n\n current_status = CASE\n WHEN input.summary_status IS NOT NULL THEN input.summary_status\n ELSE COALESCE(s.current_status, s.last_note_status, 'NO STATUS')\n END,\n\n current_status_at = CASE\n WHEN input.summary_status IS NOT NULL THEN now()\n ELSE COALESCE(s.current_status_at, s.last_note_at)\n END,\n\n is_deleted = CASE\n WHEN upper(COALESCE(input.summary_status, '')) IN (\n 'DELETED',\n 'DELETE',\n 'JOB DELETED',\n 'CANCELLED',\n 'CANCELED'\n )\n THEN true\n ELSE COALESCE(s.is_deleted, false)\n END,\n\n deleted_at = CASE\n WHEN upper(COALESCE(input.summary_status, '')) IN (\n 'DELETED',\n 'DELETE',\n 'JOB DELETED',\n 'CANCELLED',\n 'CANCELED'\n )\n AND s.deleted_at IS NULL\n THEN now()\n ELSE s.deleted_at\n END,\n\n is_never_arrived = CASE\n WHEN upper(COALESCE(input.summary_status, '')) IN (\n 'NEVER ARRIVED',\n 'NEVERARRIVED'\n )\n THEN true\n ELSE COALESCE(s.is_never_arrived, false)\n END,\n\n never_arrived_at = CASE\n WHEN upper(COALESCE(input.summary_status, '')) IN (\n 'NEVER ARRIVED',\n 'NEVERARRIVED'\n )\n AND s.never_arrived_at IS NULL\n THEN now()\n ELSE s.never_arrived_at\n END,\n\n is_delivered = CASE\n WHEN upper(COALESCE(input.summary_status, '')) IN (\n 'JOB COMPLETE',\n 'DELIVERED'\n )\n THEN true\n WHEN s.delivered_at IS NOT NULL THEN true\n ELSE COALESCE(s.is_delivered, false)\n END,\n\n status_poll_done = CASE\n WHEN upper(COALESCE(input.summary_status, '')) IN (\n 'DELETED',\n 'DELETE',\n 'JOB DELETED',\n 'CANCELLED',\n 'CANCELED',\n 'NEVER ARRIVED',\n 'NEVERARRIVED',\n 'JOB COMPLETE',\n 'DELIVERED'\n )\n THEN true\n\n WHEN s.is_collection_role = true\n AND COALESCE(s.is_delivery_role, false) = false\n AND COALESCE(s.is_manifested, false) = true\n THEN true\n\n ELSE false\n END,\n\n next_status_poll_at = CASE\n WHEN upper(COALESCE(input.summary_status, '')) IN (\n 'DELETED',\n 'DELETE',\n 'JOB DELETED',\n 'CANCELLED',\n 'CANCELED',\n 'NEVER ARRIVED',\n 'NEVERARRIVED',\n 'JOB COMPLETE',\n 'DELIVERED'\n )\n THEN NULL\n\n WHEN s.is_collection_role = true\n AND COALESCE(s.is_delivery_role, false) = false\n AND COALESCE(s.is_manifested, false) = true\n THEN NULL\n\n ELSE now() + interval '2 hours'\n END,\n\n last_status_poll_at = now(),\n status_source = CASE\n WHEN input.summary_status IS NOT NULL THEN 'palletways_summary_api'\n ELSE COALESCE(s.status_source, 'palletways_notes_api')\n END,\n updated_at = now()\nFROM input\nWHERE s.tracking_id = input.tracking_id\nRETURNING\n s.tracking_id,\n s.current_status,\n s.summary_status,\n s.status_poll_done,\n s.next_status_poll_at,\n s.is_delivered,\n s.is_deleted,\n s.is_never_arrived;",
"options": {}
},
"id": "0b6a0c25-b87a-429a-98c6-6b341f5ed573",
"name": "Postgres - Update official current status",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
3328,
1648
],
"credentials": {
"postgres": {
"id": "a0jnANQ89VLSby0b",
"name": "Palletways DB"
}
}
},
{
"parameters": {},
"id": "e7012c24-624f-42d1-a1fc-6b7dee63b7b4",
"name": "Done",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
1936,
1968
]
}
],
"pinData": {},
"connections": {
"Schedule Trigger - elke 2 uur": {
"main": [
[
{
"node": "Postgres - Setup columns and indexes",
"type": "main",
"index": 0
}
]
]
},
"Postgres - Setup columns and indexes": {
"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 URLs",
"type": "main",
"index": 0
}
]
]
},
"Code - Build URLs": {
"main": [
[
{
"node": "HTTP - Get detailed notes",
"type": "main",
"index": 0
}
]
]
},
"HTTP - Get detailed notes": {
"main": [
[
{
"node": "Code - Parse notes",
"type": "main",
"index": 0
}
]
]
},
"Code - Parse notes": {
"main": [
[
{
"node": "Postgres - Upsert notes and note fields",
"type": "main",
"index": 0
}
]
]
},
"Postgres - Upsert notes and note fields": {
"main": [
[
{
"node": "HTTP - Get summary status",
"type": "main",
"index": 0
}
]
]
},
"HTTP - Get summary status": {
"main": [
[
{
"node": "Code - Parse summary status",
"type": "main",
"index": 0
}
]
]
},
"Code - Parse summary status": {
"main": [
[
{
"node": "Postgres - Update official current status",
"type": "main",
"index": 0
}
]
]
},
"Postgres - Update official current status": {
"main": [
[
{
"node": "Loop Over Shipments",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"timezone": "Europe/London",
"callerPolicy": "workflowsFromSameOwner",
"availableInMCP": false,
"timeSavedMode": "fixed",
"errorWorkflow": "1Ps5lukDf2sgcGL7"
},
"versionId": "5bf447c5-6168-4ddd-a47d-43f7df55bdb0",
"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"
}
]
}