universal-parcel-scraper 0.49.2-main.550 → 0.49.2-main.552
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.
package/README.md
CHANGED
|
@@ -149,7 +149,7 @@ createTracker({ providers: ['ParcelsApp', 'Ship24', '17TRACK', 'Postal Ninja', '
|
|
|
149
149
|
<!-- GENERATED:stages -->
|
|
150
150
|
<img src="docs/assets/stages.svg" alt="DHL: Die Sendung wurde in das Zustellfahrzeug geladen.; Mondial Relay: En cours de livraison; Correios Brazil: Objeto saiu para entrega ao destinatário; Correos Express: EN REPARTO; Yamato Transport: 配達中; La Poste / Colissimo: DISTOU. All are filed under out_for_delivery." width="760">
|
|
151
151
|
|
|
152
|
-
The carrier folders record 2,
|
|
152
|
+
The carrier folders record 2,766 such statuses, each filed under one stage.
|
|
153
153
|
<!-- /GENERATED:stages -->
|
|
154
154
|
|
|
155
155
|
Wording nobody recorded yet goes through a classifier that reads seven European languages.
|
|
@@ -15,16 +15,20 @@
|
|
|
15
15
|
* does echo the delivery postcode, which must agree with the requested one.
|
|
16
16
|
*
|
|
17
17
|
* Privacy: the loader payload carries the retailer, the recipient's name,
|
|
18
|
-
* e-mail, phone and address,
|
|
19
|
-
* `parse()` copies nothing from those
|
|
20
|
-
* timestamp, our own description and the mapped
|
|
21
|
-
* fields read are the delivery postcode,
|
|
22
|
-
* delivery window
|
|
18
|
+
* e-mail, phone and address, the PaackGo Point pickup code and QR link, and
|
|
19
|
+
* per-event `variables` that repeat them. `parse()` copies nothing from those:
|
|
20
|
+
* each event is rebuilt from its timestamp, our own description and the mapped
|
|
21
|
+
* stage, and the only order-level fields read are the delivery postcode,
|
|
22
|
+
* compared and never copied, the delivery country, which picks the window's
|
|
23
|
+
* clock, the delivery window, and the PaackGo Point's name and address while
|
|
24
|
+
* the parcel waits there.
|
|
23
25
|
*/
|
|
24
26
|
import { load } from 'cheerio';
|
|
27
|
+
import { DateTime } from 'luxon';
|
|
25
28
|
import { lookupBudget } from '../../core/adapter/index.js';
|
|
26
29
|
import { IndeterminateError, InputRequiredError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
|
|
27
|
-
import {
|
|
30
|
+
import { countryCode, countryTimeZone, explicitOffsetTime } from '../../core/time/index.js';
|
|
31
|
+
import { clean, decodeText, fetchBounded, UpstreamHttpError, userAgentOf } from '../../core/transport/index.js';
|
|
28
32
|
import { isRecord } from '../../core/types.js';
|
|
29
33
|
import { classifyPaackEvent } from './status.js';
|
|
30
34
|
const TRACKING_ENDPOINT = 'https://mydeliveries.paack.app/tracking/order';
|
|
@@ -32,6 +36,7 @@ const DEFAULT_TIMEOUT_MS = 15_000;
|
|
|
32
36
|
const MAX_RESPONSE_BYTES = 2_000_000;
|
|
33
37
|
const MAX_EVENTS = 100;
|
|
34
38
|
const MAX_REDIRECTS = 1;
|
|
39
|
+
const DEFAULT_ZONE = 'Europe/Paris';
|
|
35
40
|
const PAACK_HOST = /(?:^|\.)paack\.(?:app|co)$/;
|
|
36
41
|
const NOT_FOUND_PATTERN = /order not found|incorrect order number|commande introuvable|pedido no encontrado/i;
|
|
37
42
|
const NOT_FOUND_PAGE_PATTERN = /order not found|incorrect order number or postal code|commande introuvable|pedido no encontrado/i;
|
|
@@ -68,6 +73,34 @@ function normalizedDate(value) {
|
|
|
68
73
|
}
|
|
69
74
|
return normalizedTimestamp(value)?.iso.slice(0, 10) ?? null;
|
|
70
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* The clock the page shows the delivery window on: the delivery country's, and
|
|
78
|
+
* the Canary Islands' for Spanish postcodes 35 and 38.
|
|
79
|
+
*/
|
|
80
|
+
function deliveryZone(country, postcode) {
|
|
81
|
+
return countryCode(country) === 'ES' && /^3[58]\d{3}$/.test(postcode)
|
|
82
|
+
? 'Atlantic/Canary'
|
|
83
|
+
: countryTimeZone(country) ?? DEFAULT_ZONE;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* `expected_delivery_ts` as the page shows it: `YYYY-MM-DD HH:MM–HH:MM` on the
|
|
87
|
+
* delivery clock when both ends are instants within one day, else the day
|
|
88
|
+
* alone. A window that ended before the newest scan is dropped.
|
|
89
|
+
*/
|
|
90
|
+
function expectedDelivery(window, zone, newest) {
|
|
91
|
+
const start = explicitOffsetTime(window.start);
|
|
92
|
+
const end = explicitOffsetTime(window.end) ?? start;
|
|
93
|
+
if (!end)
|
|
94
|
+
return normalizedDate(window.end ?? window.start);
|
|
95
|
+
if (end.timestamp < newest)
|
|
96
|
+
return null;
|
|
97
|
+
const clock = (time) => DateTime.fromMillis(time.timestamp, { zone }).toFormat('yyyy-MM-dd HH:mm');
|
|
98
|
+
const last = clock(end);
|
|
99
|
+
const first = start ? clock(start) : last;
|
|
100
|
+
return first.slice(0, 10) === last.slice(0, 10) && first < last
|
|
101
|
+
? `${first}–${last.slice(11)}`
|
|
102
|
+
: last.slice(0, 10);
|
|
103
|
+
}
|
|
71
104
|
function routeData(payload) {
|
|
72
105
|
if (!isRecord(payload))
|
|
73
106
|
throw new SchemaError('Paack', 'Paack returned an invalid tracking response');
|
|
@@ -165,6 +198,22 @@ function redirectTarget(location, from, trackingNumber, postcode) {
|
|
|
165
198
|
return null;
|
|
166
199
|
return { url: trackingOrderUrl(`${target.origin}${target.pathname}`, trackingNumber, postcode) };
|
|
167
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* The PaackGo Point the page names: `pudo_name`, then `pudo_address` on the
|
|
203
|
+
* following lines. The address is one string Paack formats itself; the page
|
|
204
|
+
* prints it as it comes and breaks it only where it holds a line break. The
|
|
205
|
+
* pickup code (`pudo_passcode`), QR link and collection deadline beside them
|
|
206
|
+
* are never read.
|
|
207
|
+
*/
|
|
208
|
+
function pickupPoint(order) {
|
|
209
|
+
const name = clean(order.pudo_name, 120);
|
|
210
|
+
if (!name)
|
|
211
|
+
return '';
|
|
212
|
+
const address = typeof order.pudo_address === 'string'
|
|
213
|
+
? order.pudo_address.split('\n').map((line) => clean(line, 200)).filter(Boolean)
|
|
214
|
+
: [];
|
|
215
|
+
return [name, ...address].join('\n');
|
|
216
|
+
}
|
|
168
217
|
/** Letters and digits only, so "SW1A 1AA" and "4445-027" compare by content. */
|
|
169
218
|
function postcodeKey(value) {
|
|
170
219
|
return value.toLocaleUpperCase('en-US').replace(/[^A-Z0-9]/g, '');
|
|
@@ -222,16 +271,20 @@ export function parsePaackTrackingResponse(payload, rawPostcode) {
|
|
|
222
271
|
const activeTime = activeEvent
|
|
223
272
|
? normalizedTimestamp(activeEvent.timestamp ?? activeEvent.time)
|
|
224
273
|
: null;
|
|
225
|
-
const
|
|
226
|
-
|
|
274
|
+
const zone = deliveryZone(address?.country, postcode);
|
|
275
|
+
const newest = Math.max(activeTime?.timestamp ?? -Infinity, parsedEvents[0]?.timestamp ?? -Infinity);
|
|
276
|
+
const expected = isRecord(order.expected_delivery_ts) && !['delivered', 'exception'].includes(current.status)
|
|
277
|
+
? expectedDelivery(order.expected_delivery_ts, zone, newest)
|
|
227
278
|
: null;
|
|
279
|
+
const pickup = current.stage === 'ready_for_pickup' ? pickupPoint(order) : '';
|
|
228
280
|
return {
|
|
229
281
|
status: current.status,
|
|
230
282
|
current_stage: current.stage,
|
|
231
283
|
last_status_text: current.description,
|
|
232
284
|
last_update: activeTime?.iso ?? events[0]?.time ?? null,
|
|
233
|
-
expected_delivery:
|
|
234
|
-
|
|
285
|
+
expected_delivery: expected,
|
|
286
|
+
...(pickup ? { pickup_point: pickup } : {}),
|
|
287
|
+
timezone: zone,
|
|
235
288
|
events,
|
|
236
289
|
};
|
|
237
290
|
}
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
},
|
|
41
41
|
"capabilities": [
|
|
42
42
|
"history",
|
|
43
|
-
"eta"
|
|
43
|
+
"eta",
|
|
44
|
+
"pickup_point"
|
|
44
45
|
],
|
|
45
46
|
"portal": {
|
|
46
47
|
"url": "https://mydeliveries.paack.app/tracking?tracking_number={trackingNumber}",
|
|
@@ -49,6 +50,9 @@
|
|
|
49
50
|
"status",
|
|
50
51
|
"history",
|
|
51
52
|
"eta",
|
|
53
|
+
"eta_window",
|
|
54
|
+
"pickup_point",
|
|
55
|
+
"pickup_code",
|
|
52
56
|
"sender_name",
|
|
53
57
|
"recipient_name",
|
|
54
58
|
"recipient_address"
|
|
@@ -56,9 +60,12 @@
|
|
|
56
60
|
"scraped": [
|
|
57
61
|
"status",
|
|
58
62
|
"history",
|
|
59
|
-
"eta"
|
|
63
|
+
"eta",
|
|
64
|
+
"eta_window",
|
|
65
|
+
"pickup_point"
|
|
60
66
|
],
|
|
61
67
|
"discarded": [
|
|
68
|
+
"pickup_code",
|
|
62
69
|
"sender_name",
|
|
63
70
|
"recipient_name",
|
|
64
71
|
"recipient_address"
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Order matters: return and failure identifiers are tested before the broader
|
|
12
12
|
* delivery ones, so "notDelivered" can never match the "delivered" substring.
|
|
13
|
+
* PaackGo Point identifiers come from the page's own vocabulary and are tested
|
|
14
|
+
* before the generic words they contain: "rejectedByPudo" is a retried
|
|
15
|
+
* delivery, not a rejection, and "collectedByCustomer" is the recipient
|
|
16
|
+
* collecting the parcel, not a transit collection.
|
|
13
17
|
* The provider's own wording is never returned; each mapped entry supplies the
|
|
14
18
|
* English description we display.
|
|
15
19
|
*
|
|
@@ -15,6 +15,7 @@ const OUT_FOR_DELIVERY = {
|
|
|
15
15
|
description: 'Out for delivery',
|
|
16
16
|
};
|
|
17
17
|
const DELIVERED = { status: 'delivered', stage: 'delivered', description: 'Delivered' };
|
|
18
|
+
const DELIVERY_ISSUE = { status: 'exception', stage: 'failed_attempt', description: 'Delivery issue' };
|
|
18
19
|
/** Paack's English timeline labels, keyed like `statusKey`, with the identifier each one renders. */
|
|
19
20
|
const RELAYED = new Map([
|
|
20
21
|
['orderdetailsreceived', REGISTERED], // manifested
|
|
@@ -36,6 +37,18 @@ export function classifyPaackEvent(value) {
|
|
|
36
37
|
])) {
|
|
37
38
|
return { status: 'exception', stage: 'returned', description: 'Shipment returned' };
|
|
38
39
|
}
|
|
40
|
+
// Expired, refused or unpaid at the PaackGo Point: on its way back to the retailer.
|
|
41
|
+
if (includesAny(key, [
|
|
42
|
+
'inpudotoreturnexpired',
|
|
43
|
+
'inpudotoreturnrejected',
|
|
44
|
+
'inpudotoreturncodnotaccepted',
|
|
45
|
+
])) {
|
|
46
|
+
return { status: 'exception', stage: 'returned', description: 'Returning to sender' };
|
|
47
|
+
}
|
|
48
|
+
// The point was closed, full or refused the parcel; Paack tries again.
|
|
49
|
+
if (includesAny(key, ['pudoclosed', 'pudofull', 'rejectedbypudo'])) {
|
|
50
|
+
return { ...DELIVERY_ISSUE };
|
|
51
|
+
}
|
|
39
52
|
if (includesAny(key, [
|
|
40
53
|
'incorrectaddress',
|
|
41
54
|
'notaccepted',
|
|
@@ -59,10 +72,10 @@ export function classifyPaackEvent(value) {
|
|
|
59
72
|
'notdelivered',
|
|
60
73
|
'undelivered',
|
|
61
74
|
]))
|
|
62
|
-
return {
|
|
63
|
-
if (includesAny(key, ['delivered', 'deliverycompleted']))
|
|
75
|
+
return { ...DELIVERY_ISSUE };
|
|
76
|
+
if (includesAny(key, ['delivered', 'deliverycompleted', 'collectedbycustomer']))
|
|
64
77
|
return { ...DELIVERED };
|
|
65
|
-
if (includesAny(key, ['
|
|
78
|
+
if (includesAny(key, ['droppedinpudo'])) {
|
|
66
79
|
return { status: 'out_for_delivery', stage: 'ready_for_pickup', description: 'Ready for pickup' };
|
|
67
80
|
}
|
|
68
81
|
if (includesAny(key, ['outfordelivery', 'indelivery', 'driverassigned', 'inprogress']))
|
|
@@ -25,6 +25,54 @@
|
|
|
25
25
|
"firstSeen": "2026-09-12",
|
|
26
26
|
"note": "Identifier substring inherited from the pre-package adapter; source not recorded."
|
|
27
27
|
},
|
|
28
|
+
{
|
|
29
|
+
"code": "inpudotoreturnexpired",
|
|
30
|
+
"language": "en",
|
|
31
|
+
"stage": "returned",
|
|
32
|
+
"confirmedBy": "bundle",
|
|
33
|
+
"firstSeen": "2026-10-08",
|
|
34
|
+
"note": "Page label inPudoToReturnExpired, \"Expired at PaackGo Point\": not collected in time, returned to the retailer."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"code": "inpudotoreturnrejected",
|
|
38
|
+
"language": "en",
|
|
39
|
+
"stage": "returned",
|
|
40
|
+
"confirmedBy": "bundle",
|
|
41
|
+
"firstSeen": "2026-10-08",
|
|
42
|
+
"note": "Page label inPudoToReturnRejected: refused at the PaackGo Point, returned to the retailer. Tested before the rejected exception rule."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"code": "inpudotoreturncodnotaccepted",
|
|
46
|
+
"language": "en",
|
|
47
|
+
"stage": "returned",
|
|
48
|
+
"confirmedBy": "bundle",
|
|
49
|
+
"firstSeen": "2026-10-08",
|
|
50
|
+
"note": "Page label inPudoToReturnCODNotAccepted: cash on delivery not paid at the PaackGo Point, returned to the retailer."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"code": "pudoclosed",
|
|
54
|
+
"language": "en",
|
|
55
|
+
"stage": "failed_attempt",
|
|
56
|
+
"confirmedBy": "bundle",
|
|
57
|
+
"firstSeen": "2026-10-08",
|
|
58
|
+
"note": "Page label pudoClosed, \"Has been rescheduled\": the PaackGo Point was closed; Paack tries again."
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"code": "pudofull",
|
|
62
|
+
"language": "en",
|
|
63
|
+
"stage": "failed_attempt",
|
|
64
|
+
"confirmedBy": "bundle",
|
|
65
|
+
"firstSeen": "2026-10-08",
|
|
66
|
+
"note": "Page label pudoFull, \"Has been rescheduled\": the PaackGo Point was full; Paack tries again."
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"code": "rejectedbypudo",
|
|
70
|
+
"language": "en",
|
|
71
|
+
"stage": "failed_attempt",
|
|
72
|
+
"confirmedBy": "bundle",
|
|
73
|
+
"firstSeen": "2026-10-08",
|
|
74
|
+
"note": "Page label rejectedByPudo, \"Has been rescheduled\": the PaackGo Point refused the parcel; Paack tries again. Tested before the rejected exception rule."
|
|
75
|
+
},
|
|
28
76
|
{
|
|
29
77
|
"code": "returntosenderscheduled",
|
|
30
78
|
"language": "en",
|
|
@@ -145,6 +193,22 @@
|
|
|
145
193
|
"firstSeen": "2026-09-12",
|
|
146
194
|
"note": "Identifier substring inherited from the pre-package adapter; source not recorded. Reclassified to exception 2026-09-12."
|
|
147
195
|
},
|
|
196
|
+
{
|
|
197
|
+
"code": "inpudotoreturndamaged",
|
|
198
|
+
"language": "en",
|
|
199
|
+
"stage": "exception",
|
|
200
|
+
"confirmedBy": "bundle",
|
|
201
|
+
"firstSeen": "2026-10-08",
|
|
202
|
+
"note": "Page label inPudoToReturnDamaged, \"Logistic issue\": no longer available at the PaackGo Point. Reached through damaged."
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"code": "lostbypudo",
|
|
206
|
+
"language": "en",
|
|
207
|
+
"stage": "exception",
|
|
208
|
+
"confirmedBy": "bundle",
|
|
209
|
+
"firstSeen": "2026-10-08",
|
|
210
|
+
"note": "Page label lostByPudo, \"Logistic issue\". Reached through lost."
|
|
211
|
+
},
|
|
148
212
|
{
|
|
149
213
|
"code": "integrationerror",
|
|
150
214
|
"language": "en",
|
|
@@ -186,20 +250,20 @@
|
|
|
186
250
|
"note": "Identifier substring inherited from the pre-package adapter; source not recorded."
|
|
187
251
|
},
|
|
188
252
|
{
|
|
189
|
-
"code": "
|
|
253
|
+
"code": "collectedbycustomer",
|
|
190
254
|
"language": "en",
|
|
191
|
-
"stage": "
|
|
192
|
-
"confirmedBy": "
|
|
193
|
-
"firstSeen": "2026-
|
|
194
|
-
"note": "
|
|
255
|
+
"stage": "delivered",
|
|
256
|
+
"confirmedBy": "bundle",
|
|
257
|
+
"firstSeen": "2026-10-08",
|
|
258
|
+
"note": "Page label collectedByCustomer, \"Collected from PaackGo Point\". Tested before the collected transit rule."
|
|
195
259
|
},
|
|
196
260
|
{
|
|
197
|
-
"code": "
|
|
261
|
+
"code": "droppedinpudo",
|
|
198
262
|
"language": "en",
|
|
199
263
|
"stage": "ready_for_pickup",
|
|
200
|
-
"confirmedBy": "
|
|
201
|
-
"firstSeen": "2026-
|
|
202
|
-
"note": "
|
|
264
|
+
"confirmedBy": "bundle",
|
|
265
|
+
"firstSeen": "2026-10-08",
|
|
266
|
+
"note": "Page label droppedInPudo, \"At PaackGo Point\": the parcel waits at the point named by pudo_name and pudo_address."
|
|
203
267
|
},
|
|
204
268
|
{
|
|
205
269
|
"code": "outfordelivery",
|
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.552",
|
|
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",
|