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, and the
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 expected = isRecord(order.expected_delivery_ts)
226
- ? normalizedDate(order.expected_delivery_ts.end ?? order.expected_delivery_ts.start)
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: ['delivered', 'exception'].includes(current.status) ? null : expected,
234
- timezone: 'Europe/Paris',
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.550",
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",