universal-parcel-scraper 0.49.2-main.563 → 0.49.2-main.567
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -45,6 +45,24 @@ function abroadZone(location, abroad) {
|
|
|
45
45
|
return canadaProvinceTimeZone(region);
|
|
46
46
|
return countryTimeZone(abroad);
|
|
47
47
|
}
|
|
48
|
+
/** Scans that leave the parcel at an Australian post office or parcel locker for collection. */
|
|
49
|
+
const COLLECTION_SCANS = new Set(['DD-ER4', 'NT-ER4']);
|
|
50
|
+
/** Deliveries left in a safe place, at the door. */
|
|
51
|
+
const SAFE_PLACE = new Set(['DD-ER15', 'DD-ER38']);
|
|
52
|
+
/**
|
|
53
|
+
* The post office or locker its awaiting-collection scan names: the newest scan while the
|
|
54
|
+
* parcel waits there, or the scan just before its delivery once it is collected there. A
|
|
55
|
+
* delivery left in a safe place was not collected.
|
|
56
|
+
*/
|
|
57
|
+
function collectionPoint(stage, events) {
|
|
58
|
+
const index = events.findIndex((event) => event.stage !== 'delivered');
|
|
59
|
+
const placed = stage === 'ready_for_pickup' ? index === 0
|
|
60
|
+
: stage === 'delivered' && index > 0 && !events.slice(0, index).some((event) => SAFE_PLACE.has(String(event.provider_code)));
|
|
61
|
+
const scan = placed ? events[index] : undefined;
|
|
62
|
+
if (!scan || !COLLECTION_SCANS.has(String(scan.provider_code)))
|
|
63
|
+
return undefined;
|
|
64
|
+
return /^Awaiting collection at (.+)$/i.exec(scan.description ?? '')?.[1]?.trim() || undefined;
|
|
65
|
+
}
|
|
48
66
|
/** The one lookup entry and article must independently match the submitted reference. */
|
|
49
67
|
export function parse(payload, trackingNumber) {
|
|
50
68
|
const number = normalizeAustraliaPostNumber(trackingNumber);
|
|
@@ -153,9 +171,8 @@ export function parse(payload, trackingNumber) {
|
|
|
153
171
|
const latest = events[0];
|
|
154
172
|
const status = australiaPostStatus(summary) ?? latest.classified;
|
|
155
173
|
const deliveredAt = status?.status === 'delivered' ? events.find(({ event }) => event.stage === 'delivered')?.event.time : null;
|
|
156
|
-
// The
|
|
157
|
-
const pickup = status?.stage
|
|
158
|
-
? /^Awaiting collection at (.+)$/i.exec(latest.event.description ?? '')?.[1]?.trim() : undefined;
|
|
174
|
+
// The reply names the point but gives no address for it.
|
|
175
|
+
const pickup = collectionPoint(status?.stage, events.map(({ event }) => event));
|
|
159
176
|
// Summary modification/milestone timestamps are not scan times. In the
|
|
160
177
|
// observed reply they differed from the delivery event by about ten hours.
|
|
161
178
|
return {
|
|
@@ -2,7 +2,7 @@ import { DateTime } from 'luxon';
|
|
|
2
2
|
import { isValidS10TrackingNumber, normalizeTrackingNumber } from '../../core/detection/index.js';
|
|
3
3
|
import { IndeterminateError, InvalidInputError, SchemaError } from '../../core/errors/index.js';
|
|
4
4
|
import { calendarDay, explicitOffsetTime } from '../../core/time/index.js';
|
|
5
|
-
import { clean } from '../../core/transport/index.js';
|
|
5
|
+
import { clean, cleanScalar } from '../../core/transport/index.js';
|
|
6
6
|
import { isRecord } from '../../core/types.js';
|
|
7
7
|
import { bringStatus, bringWording } from './status.js';
|
|
8
8
|
/**
|
|
@@ -64,6 +64,18 @@ function estimatedDay(domain) {
|
|
|
64
64
|
const day = /^(\d{4})-(\d{2})-(\d{2})(?:$|T)/.exec(eta.dateOfEstimatedDeliveryIso);
|
|
65
65
|
return day ? calendarDay(Number(day[1]), Number(day[2]), Number(day[3])) : null;
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether the parcel was collected at the pickup point the portal names: the
|
|
69
|
+
* delivery, and the movement before it that made the parcel ready for pickup,
|
|
70
|
+
* were scanned at that point's unit. Notices prove no movement.
|
|
71
|
+
*/
|
|
72
|
+
function collectedAt(rows, pickupUnit) {
|
|
73
|
+
const unit = cleanScalar(pickupUnit, 32);
|
|
74
|
+
const moves = rows.filter(row => bringStatus(String(row.status), text(row.lmCauseCode, 8)));
|
|
75
|
+
const index = moves.findIndex(row => row.status !== 'DELIVERED');
|
|
76
|
+
return Boolean(unit) && index > 0 && moves[index].status === 'READY_FOR_PICKUP'
|
|
77
|
+
&& moves.slice(0, index + 1).every(row => cleanScalar(row.unitId, 32) === unit);
|
|
78
|
+
}
|
|
67
79
|
export function parseBring(payload, rawNumber) {
|
|
68
80
|
const number = normalizeBringNumber(rawNumber);
|
|
69
81
|
if (!isRecord(payload))
|
|
@@ -126,8 +138,9 @@ export function parseBring(payload, rawNumber) {
|
|
|
126
138
|
const sender = text(consignment.senderName, 200) || text(parcel.senderName, 200);
|
|
127
139
|
const pickupInfo = isRecord(parcel.domain.deliveryType) && isRecord(parcel.domain.deliveryType.pickupPointInfo)
|
|
128
140
|
? parcel.domain.deliveryType.pickupPointInfo : {};
|
|
129
|
-
const
|
|
130
|
-
|
|
141
|
+
const atPickupPoint = current?.stage === 'ready_for_pickup'
|
|
142
|
+
|| (current?.stage === 'delivered' && collectedAt(parcel.eventSet.filter(isRecord), parcel.expectedPickupUnitId));
|
|
143
|
+
const pickup = atPickupPoint ? text(parcel.expectedPickupUnitName, 200) || text(pickupInfo.expectedPickupUnitName, 200) : '';
|
|
131
144
|
const country = isRecord(consignment.recipientAddress) ? consignment.recipientAddress.countryCode : undefined;
|
|
132
145
|
const active = current && ['pending', 'in_transit', 'out_for_delivery'].includes(current.status) && current.stage !== 'ready_for_pickup';
|
|
133
146
|
return { status: current?.status ?? 'unknown', ...(current ? { current_stage: current.stage } : {}),
|
|
@@ -38,6 +38,19 @@ function scanClock(date, time) {
|
|
|
38
38
|
const iso = day && calendarDay(Number(day[3]), Number(day[2]), Number(day[1]));
|
|
39
39
|
return iso && clock && Number(clock[1]) < 24 && Number(clock[2]) < 60 ? { local_time: `${iso}T${clock[1]}:${clock[2]}:00` } : {};
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The ServicePoint's name, then its street and "postcode town", as the page's
|
|
43
|
+
* ServicePoint card shows them. The name alone when the street or town is missing.
|
|
44
|
+
*/
|
|
45
|
+
function servicePoint(raw) {
|
|
46
|
+
const point = isRecord(raw) ? raw : {};
|
|
47
|
+
const name = clean(point.Name, 120);
|
|
48
|
+
const street = clean(point.Address, 120);
|
|
49
|
+
const town = clean(point.Location, 80);
|
|
50
|
+
if (!name || !street || !town)
|
|
51
|
+
return name;
|
|
52
|
+
return [name, street, [clean(point.ZipCode, 16), town].filter(Boolean).join(' ')].join('\n');
|
|
53
|
+
}
|
|
41
54
|
function code(value) {
|
|
42
55
|
if (value === undefined || value === null || value === '')
|
|
43
56
|
return undefined;
|
|
@@ -80,8 +93,10 @@ export function parseDhlEcommerceEs(payload, rawNumber) {
|
|
|
80
93
|
const current = final >= 0 && !reopened ? events[final] : latest;
|
|
81
94
|
const stage = current.stage ?? dhlEcommerceEsSummaryStage(payload.Status);
|
|
82
95
|
const weight = weightKg(payload.Weight);
|
|
83
|
-
// The ServicePoint is where the parcel waits
|
|
84
|
-
|
|
96
|
+
// The ServicePoint is where the parcel waits while it is ready there, and where it was
|
|
97
|
+
// collected when the shipment says it was delivered there and the recipient picked it up.
|
|
98
|
+
const collected = stage === 'delivered' && current.provider_code === 'RS' && payload.DeliveredInServicePoint === true;
|
|
99
|
+
const point = stage === 'ready_for_pickup' || collected ? servicePoint(payload.ServicePoint) : '';
|
|
85
100
|
// CTT Express delivers DHL's consumer parcels in Spain under its own label code.
|
|
86
101
|
const partner = typeof payload.ShippingCode === 'string' ? payload.ShippingCode.trim() : '';
|
|
87
102
|
return {
|
|
@@ -11,6 +11,36 @@ export function normalizePostnordNumber(raw) {
|
|
|
11
11
|
}
|
|
12
12
|
return number;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* The scan that made the parcel available at its service point, while it waits
|
|
16
|
+
* there or once it is collected there: the delivery, scanned at that same point,
|
|
17
|
+
* came right after it. A parcel taken back out for delivery has a movement between.
|
|
18
|
+
*/
|
|
19
|
+
function atPickupPoint(stage, events) {
|
|
20
|
+
if (stage === 'ready_for_pickup')
|
|
21
|
+
return events.find((event) => event.stage === 'ready_for_pickup');
|
|
22
|
+
if (stage !== 'delivered')
|
|
23
|
+
return undefined;
|
|
24
|
+
const index = events.findIndex((event) => event.stage !== 'delivered');
|
|
25
|
+
const ready = events[index];
|
|
26
|
+
return index > 0 && ready?.stage === 'ready_for_pickup' && ready.location
|
|
27
|
+
&& events.slice(0, index).every((event) => event.location === ready.location) ? ready : undefined;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The service point's name, then its street and "postcode town", from the point
|
|
31
|
+
* record the tracking widget shows under a parcel ready for pickup. Without that
|
|
32
|
+
* record the scan's name stands alone. The receiver's address is a separate record.
|
|
33
|
+
*/
|
|
34
|
+
function pickupPoint(raw, scanned) {
|
|
35
|
+
const point = isRecord(raw) ? raw : {};
|
|
36
|
+
const address = isRecord(point.address) ? point.address : {};
|
|
37
|
+
const name = clean(point.name, 160);
|
|
38
|
+
if (!name)
|
|
39
|
+
return scanned;
|
|
40
|
+
const street = clean(address.street, 120);
|
|
41
|
+
const town = clean(address.city, 80);
|
|
42
|
+
return street && town ? [name, street, [clean(address.postalCode, 16), town].filter(Boolean).join(' ')].join('\n') : name;
|
|
43
|
+
}
|
|
14
44
|
export function parsePostnord(payload, number) {
|
|
15
45
|
const requested = normalizePostnordNumber(number);
|
|
16
46
|
if (!isRecord(payload) || payload.shipmentId !== requested || payload.actualReturnedId !== requested
|
|
@@ -69,8 +99,8 @@ export function parsePostnord(payload, number) {
|
|
|
69
99
|
// The portal names the sender, usually the shop; of the receiver's address only the country is read.
|
|
70
100
|
const sender = isRecord(payload.sender) ? clean(payload.sender.name, 200) : '';
|
|
71
101
|
const country = isRecord(payload.receiver) && isRecord(payload.receiver.address) ? payload.receiver.address.countryCode : undefined;
|
|
72
|
-
|
|
73
|
-
const pickup =
|
|
102
|
+
const pickupScan = atPickupPoint(mapped?.stage, events);
|
|
103
|
+
const pickup = pickupScan ? pickupPoint(payload.servicePoint, pickupScan.location ?? '') : '';
|
|
74
104
|
return { status: mapped?.status ?? 'unknown', ...(mapped ? { current_stage: mapped.stage } : {}),
|
|
75
105
|
last_status_text: header || events[0].description, last_update: events[0].time,
|
|
76
106
|
expected_delivery: null, ...(delivery?.time ? { delivered_at: delivery.time } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-parcel-scraper",
|
|
3
|
-
"version": "0.49.2-main.
|
|
3
|
+
"version": "0.49.2-main.567",
|
|
4
4
|
"description": "Self-hosted parcel tracking: carrier detection, dedicated scrapers and optional universal providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|