universal-parcel-scraper 0.49.2-main.562 → 0.49.2-main.565

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 post office or locker holding the parcel, as its newest scan names it.
157
- const pickup = status?.stage === 'ready_for_pickup' && ['DD-ER4', 'NT-ER4'].includes(String(latest.event.provider_code))
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 {
@@ -52,7 +52,7 @@
52
52
  "discarded": [
53
53
  "recipient",
54
54
  "sender",
55
- "addresses",
55
+ "recipient_address",
56
56
  "contact_details",
57
57
  "delivery_preferences",
58
58
  "proof_of_delivery"
@@ -16,12 +16,32 @@ function countryCode(value) {
16
16
  const code = clean(value, 8).toUpperCase();
17
17
  return /^[A-Z]{2}$/.test(code) ? code : '';
18
18
  }
19
- /** The pickup point's name, in the first language bpost provides. Its address is never read. */
20
- function pickupPointName(point) {
21
- if (!isRecord(point) || !isRecord(point.name))
19
+ const LANGUAGES = ['en', 'fr', 'nl', 'de'];
20
+ /** A field bpost gives per language, in the first of these languages it provides. */
21
+ function localized(field, maxLength, languages = LANGUAGES) {
22
+ return isRecord(field) ? languages.map((language) => clean(field[language], maxLength)).find(Boolean) ?? '' : '';
23
+ }
24
+ // The steps for which bpost's tracker shows "Collected" at the delivery point
25
+ // instead of "Delivered to" the recipient's address.
26
+ const COLLECTED_STEPS = new Set(['PICKED_UP_IN_POST_OFFICE', 'PICKED_UP_IN_POST_POINT', 'PICKED_UP_IN_KARIBOO_POINT',
27
+ 'PICKED_UP_IN_PARCEL_LOCKER', 'PICKED_UP_IN_SHOP', 'PICKED_UP_IN_PICKUP_POINT_INTERNATIONAL']);
28
+ /**
29
+ * The pickup point's name, then its street and number, then "postcode town",
30
+ * in one language. The name alone when the street or town is missing. Its code
31
+ * and opening hours are not read.
32
+ */
33
+ function pickupPoint(point) {
34
+ if (!isRecord(point))
22
35
  return '';
23
- const names = point.name;
24
- return ['en', 'fr', 'nl', 'de'].map((language) => clean(names[language], 160)).find(Boolean) ?? '';
36
+ const name = localized(point.name, 160);
37
+ const language = LANGUAGES.find((code) => localized(point.street, 120, [code]) && localized(point.municipality, 80, [code]));
38
+ if (!name || !language)
39
+ return name;
40
+ // A number or postcode reads the same in every language.
41
+ const any = [language, ...LANGUAGES];
42
+ const street = [localized(point.street, 120, [language]), localized(point.streetNumber, 16, any)].filter(Boolean).join(' ');
43
+ const town = [localized(point.postcode, 16, any), localized(point.municipality, 80, [language])].filter(Boolean).join(' ');
44
+ return [name, street, town].join('\n');
25
45
  }
26
46
  export function parseBpost(payload, number) {
27
47
  const requested = normalizeBpostNumber(number);
@@ -84,7 +104,12 @@ export function parseBpost(payload, number) {
84
104
  const mapped = classifyBpostStatus(latest.description);
85
105
  const current = returnDelivery && mapped?.stage === 'delivered' ? { status: 'exception', stage: 'returned' } : mapped;
86
106
  const destination = isRecord(item.receiver) ? countryCode(item.receiver.countryCode) : '';
87
- const pickupPoint = current?.stage === 'ready_for_pickup' ? pickupPointName(item.deliveryPoint) : '';
107
+ // The delivery point is the pickup point itself, apart from the receiver: bpost's
108
+ // tracker shows it with opening hours and directions from its own coordinates.
109
+ // It stays once the parcel is collected there, never after a door delivery.
110
+ const collected = current?.stage === 'delivered' && isRecord(item.activeStep)
111
+ && typeof item.activeStep.knownProcessStep === 'string' && COLLECTED_STEPS.has(item.activeStep.knownProcessStep);
112
+ const point = current?.stage === 'ready_for_pickup' || collected ? pickupPoint(item.deliveryPoint) : '';
88
113
  // The sender's barcode is kept only when it is an S10 number other than the one searched.
89
114
  const senderBarcode = clean(item.senderBarcode, 32).toUpperCase();
90
115
  const international = senderBarcode !== requested && isValidS10TrackingNumber(senderBarcode) ? senderBarcode : '';
@@ -97,7 +122,7 @@ export function parseBpost(payload, number) {
97
122
  ...(typeof grams === 'number' && Number.isFinite(grams) && grams > 0 ? { weight_kg: grams / 1000 } : {}),
98
123
  ...(dimensionMatch && dimensionMatch.slice(1).every((value) => Number(value) > 0)
99
124
  ? { dimensions_text: `${dimensionMatch.slice(1).join(' × ')} cm` } : {}),
100
- ...(pickupPoint ? { pickup_point: pickupPoint } : {}),
125
+ ...(point ? { pickup_point: point } : {}),
101
126
  ...(destination ? { destination_country: destination } : {}),
102
127
  ...(international ? { international_tracking_number: international } : {}),
103
128
  events: events.slice(0, 100) };
@@ -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 only while it is ready there.
84
- const point = stage === 'ready_for_pickup' && isRecord(payload.ServicePoint) ? clean(payload.ServicePoint.Name, 120) : '';
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-parcel-scraper",
3
- "version": "0.49.2-main.562",
3
+ "version": "0.49.2-main.565",
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",