universal-parcel-scraper 0.49.2-main.550 → 0.49.2-main.551
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.
|
@@ -18,12 +18,14 @@
|
|
|
18
18
|
* e-mail, phone and address, and per-event `variables` that repeat them.
|
|
19
19
|
* `parse()` copies nothing from those objects: each event is rebuilt from its
|
|
20
20
|
* timestamp, our own description and the mapped stage, and the only order-level
|
|
21
|
-
* fields read are the delivery postcode, compared and never copied,
|
|
22
|
-
* delivery window.
|
|
21
|
+
* fields read are the delivery postcode, compared and never copied, the
|
|
22
|
+
* delivery country, which picks the window's clock, and the delivery window.
|
|
23
23
|
*/
|
|
24
24
|
import { load } from 'cheerio';
|
|
25
|
+
import { DateTime } from 'luxon';
|
|
25
26
|
import { lookupBudget } from '../../core/adapter/index.js';
|
|
26
27
|
import { IndeterminateError, InputRequiredError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
|
|
28
|
+
import { countryCode, countryTimeZone, explicitOffsetTime } from '../../core/time/index.js';
|
|
27
29
|
import { decodeText, fetchBounded, UpstreamHttpError, userAgentOf } from '../../core/transport/index.js';
|
|
28
30
|
import { isRecord } from '../../core/types.js';
|
|
29
31
|
import { classifyPaackEvent } from './status.js';
|
|
@@ -32,6 +34,7 @@ const DEFAULT_TIMEOUT_MS = 15_000;
|
|
|
32
34
|
const MAX_RESPONSE_BYTES = 2_000_000;
|
|
33
35
|
const MAX_EVENTS = 100;
|
|
34
36
|
const MAX_REDIRECTS = 1;
|
|
37
|
+
const DEFAULT_ZONE = 'Europe/Paris';
|
|
35
38
|
const PAACK_HOST = /(?:^|\.)paack\.(?:app|co)$/;
|
|
36
39
|
const NOT_FOUND_PATTERN = /order not found|incorrect order number|commande introuvable|pedido no encontrado/i;
|
|
37
40
|
const NOT_FOUND_PAGE_PATTERN = /order not found|incorrect order number or postal code|commande introuvable|pedido no encontrado/i;
|
|
@@ -68,6 +71,34 @@ function normalizedDate(value) {
|
|
|
68
71
|
}
|
|
69
72
|
return normalizedTimestamp(value)?.iso.slice(0, 10) ?? null;
|
|
70
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* The clock the page shows the delivery window on: the delivery country's, and
|
|
76
|
+
* the Canary Islands' for Spanish postcodes 35 and 38.
|
|
77
|
+
*/
|
|
78
|
+
function deliveryZone(country, postcode) {
|
|
79
|
+
return countryCode(country) === 'ES' && /^3[58]\d{3}$/.test(postcode)
|
|
80
|
+
? 'Atlantic/Canary'
|
|
81
|
+
: countryTimeZone(country) ?? DEFAULT_ZONE;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* `expected_delivery_ts` as the page shows it: `YYYY-MM-DD HH:MM–HH:MM` on the
|
|
85
|
+
* delivery clock when both ends are instants within one day, else the day
|
|
86
|
+
* alone. A window that ended before the newest scan is dropped.
|
|
87
|
+
*/
|
|
88
|
+
function expectedDelivery(window, zone, newest) {
|
|
89
|
+
const start = explicitOffsetTime(window.start);
|
|
90
|
+
const end = explicitOffsetTime(window.end) ?? start;
|
|
91
|
+
if (!end)
|
|
92
|
+
return normalizedDate(window.end ?? window.start);
|
|
93
|
+
if (end.timestamp < newest)
|
|
94
|
+
return null;
|
|
95
|
+
const clock = (time) => DateTime.fromMillis(time.timestamp, { zone }).toFormat('yyyy-MM-dd HH:mm');
|
|
96
|
+
const last = clock(end);
|
|
97
|
+
const first = start ? clock(start) : last;
|
|
98
|
+
return first.slice(0, 10) === last.slice(0, 10) && first < last
|
|
99
|
+
? `${first}–${last.slice(11)}`
|
|
100
|
+
: last.slice(0, 10);
|
|
101
|
+
}
|
|
71
102
|
function routeData(payload) {
|
|
72
103
|
if (!isRecord(payload))
|
|
73
104
|
throw new SchemaError('Paack', 'Paack returned an invalid tracking response');
|
|
@@ -222,16 +253,18 @@ export function parsePaackTrackingResponse(payload, rawPostcode) {
|
|
|
222
253
|
const activeTime = activeEvent
|
|
223
254
|
? normalizedTimestamp(activeEvent.timestamp ?? activeEvent.time)
|
|
224
255
|
: null;
|
|
225
|
-
const
|
|
226
|
-
|
|
256
|
+
const zone = deliveryZone(address?.country, postcode);
|
|
257
|
+
const newest = Math.max(activeTime?.timestamp ?? -Infinity, parsedEvents[0]?.timestamp ?? -Infinity);
|
|
258
|
+
const expected = isRecord(order.expected_delivery_ts) && !['delivered', 'exception'].includes(current.status)
|
|
259
|
+
? expectedDelivery(order.expected_delivery_ts, zone, newest)
|
|
227
260
|
: null;
|
|
228
261
|
return {
|
|
229
262
|
status: current.status,
|
|
230
263
|
current_stage: current.stage,
|
|
231
264
|
last_status_text: current.description,
|
|
232
265
|
last_update: activeTime?.iso ?? events[0]?.time ?? null,
|
|
233
|
-
expected_delivery:
|
|
234
|
-
timezone:
|
|
266
|
+
expected_delivery: expected,
|
|
267
|
+
timezone: zone,
|
|
235
268
|
events,
|
|
236
269
|
};
|
|
237
270
|
}
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"status",
|
|
50
50
|
"history",
|
|
51
51
|
"eta",
|
|
52
|
+
"eta_window",
|
|
52
53
|
"sender_name",
|
|
53
54
|
"recipient_name",
|
|
54
55
|
"recipient_address"
|
|
@@ -56,7 +57,8 @@
|
|
|
56
57
|
"scraped": [
|
|
57
58
|
"status",
|
|
58
59
|
"history",
|
|
59
|
-
"eta"
|
|
60
|
+
"eta",
|
|
61
|
+
"eta_window"
|
|
60
62
|
],
|
|
61
63
|
"discarded": [
|
|
62
64
|
"sender_name",
|
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.551",
|
|
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",
|