universal-parcel-scraper 0.48.0-main.487 → 0.48.0-main.489
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.
|
@@ -57,11 +57,19 @@ function appointmentWindow(start, end) {
|
|
|
57
57
|
const clocks = ends.map(match => match ? `${match[4]}:${match[5]}` : '');
|
|
58
58
|
return days[0] && days[0] === days[1] && clocks[0] < clocks[1] ? `${days[0]} ${clocks[0]}–${clocks[1]}` : null;
|
|
59
59
|
}
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
/**
|
|
61
|
+
* A relay or locker, given as `name - street - postcode - city - country`: its name, then its
|
|
62
|
+
* street and town on their own lines when the address has that layout.
|
|
63
|
+
*/
|
|
64
|
+
function relayPoint(point) {
|
|
65
|
+
const parts = point.split(' - ').map(part => clean(part, 200));
|
|
66
|
+
const name = parts[0];
|
|
67
|
+
if (parts.length < 3 || !/\p{L}/u.test(name) || !/^[\p{L}\p{M}\p{N} .,'’&()/-]+$/u.test(name))
|
|
68
|
+
return '';
|
|
69
|
+
const street = parts.slice(1, -3).join(' - ');
|
|
70
|
+
const [postcode, city] = parts.slice(-3, -1);
|
|
71
|
+
return /\p{L}/u.test(street) && /^(?=.*\d)[A-Z\d][A-Z\d -]{1,9}$/.test(postcode) && /\p{L}/u.test(city)
|
|
72
|
+
? `${name}\n${street}\n${postcode} ${city}` : name;
|
|
65
73
|
}
|
|
66
74
|
/** The calendar day of a scan's own clock, or '' without one. */
|
|
67
75
|
const clockDay = (event) => {
|
|
@@ -125,6 +133,7 @@ export function parseChronopostTrackingXml(xml, rawNumber) {
|
|
|
125
133
|
let end;
|
|
126
134
|
let collection = '';
|
|
127
135
|
let point = '';
|
|
136
|
+
let relay = '';
|
|
128
137
|
for (const extra of extras) {
|
|
129
138
|
const name = scalar(extra, 'name');
|
|
130
139
|
const value = scalar(extra, 'value');
|
|
@@ -138,6 +147,8 @@ export function parseChronopostTrackingXml(xml, rawNumber) {
|
|
|
138
147
|
end = value;
|
|
139
148
|
if (name === 'Type de retrait')
|
|
140
149
|
collection = value;
|
|
150
|
+
if (name === 'Point de retrait')
|
|
151
|
+
relay = value;
|
|
141
152
|
if (name === 'Numéro partenaire') {
|
|
142
153
|
const match = /^(?:GEO\/)?([A-Z0-9]{4,40})$/.exec(value.toUpperCase());
|
|
143
154
|
if (match) {
|
|
@@ -147,7 +158,7 @@ export function parseChronopostTrackingXml(xml, rawNumber) {
|
|
|
147
158
|
}
|
|
148
159
|
}
|
|
149
160
|
if (name === 'Point de livraison') {
|
|
150
|
-
// Only the terminal country and a pickup point
|
|
161
|
+
// Only the terminal country and a pickup point are projected.
|
|
151
162
|
point = value;
|
|
152
163
|
const country = countryCode(/ - ([A-Z]{2})$/.exec(value)?.[1]);
|
|
153
164
|
if (country)
|
|
@@ -163,7 +174,9 @@ export function parseChronopostTrackingXml(xml, rawNumber) {
|
|
|
163
174
|
const mapped = chronopostStage(code, description);
|
|
164
175
|
return { event, mapped, notification: isChronopostNotification(description), index, redelivery,
|
|
165
176
|
appointment: start === undefined && end === undefined ? undefined : appointmentWindow(start ?? '', end ?? ''),
|
|
166
|
-
dropOff: isPickupDropOff(code, description),
|
|
177
|
+
dropOff: isPickupDropOff(code, description),
|
|
178
|
+
// A delivery point is a relay only when the scan names how it is collected.
|
|
179
|
+
pickup: relay ? relayPoint(relay) : collection && point ? relayPoint(point) : '' };
|
|
167
180
|
});
|
|
168
181
|
// The operation returns oldest first. Reorder only when all scan clocks
|
|
169
182
|
// establish instants, so mixed local clocks cannot create a guessed order.
|
|
@@ -18,8 +18,14 @@ export declare const DPD_DE_APP_RAIL: Readonly<Record<string, ClassifiedStatus>>
|
|
|
18
18
|
* whole; a variable date is recorded as an ellipsis.
|
|
19
19
|
*/
|
|
20
20
|
export declare const DPD_DE_APP_SCANS: Readonly<Record<string, Stage>>;
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Scans arrive oldest first, with a facility's wall clock and no offset. `shop` is the PUDO id of
|
|
23
|
+
* the shop the result names as its pickup point, else empty.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseDpdDeApp(data: XmlNode, scans: XmlNode, number: string): {
|
|
26
|
+
result: CarrierResult;
|
|
27
|
+
shop: string;
|
|
28
|
+
};
|
|
23
29
|
/**
|
|
24
30
|
* The German app's SOAP service. Tracking needs an anonymous device session, which the
|
|
25
31
|
* service takes tens of seconds to open and then accepts for hours: one is kept per client.
|
|
@@ -44,6 +50,8 @@ export declare class DpdDeAppClient {
|
|
|
44
50
|
sessionWaitMs?: number;
|
|
45
51
|
postcode?: string;
|
|
46
52
|
}): Promise<CarrierResult>;
|
|
53
|
+
/** The shop's street and town, or nothing: the parcel is found, and its pickup point keeps its name without them. */
|
|
54
|
+
private shopAddress;
|
|
47
55
|
/**
|
|
48
56
|
* One session for every lookup. The opening runs while a lookup that asked for it is still
|
|
49
57
|
* running, even one that stopped waiting; each lookup waits until its signal or `waitMs` ends.
|
|
@@ -148,12 +148,26 @@ function statusOf(stage) {
|
|
|
148
148
|
return 'pending';
|
|
149
149
|
return stage ? 'in_transit' : 'unknown';
|
|
150
150
|
}
|
|
151
|
-
/** The shop a scan names, as `<
|
|
151
|
+
/** The shop a scan names, as `<PUDO id>|<shop name>` under additional code 999. */
|
|
152
152
|
function shopOf(row) {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
for (const entry of children(row, 'TrackingScanAdditionalList').flatMap(list => children(list, 'TrackingScanAdditionalType'))) {
|
|
154
|
+
const named = scalar(entry, 'AdditionalCode', 8) === '999' && /^([A-Z]{2}\d{1,12})\|(.{1,120})$/.exec(scalar(entry, 'Description', 200));
|
|
155
|
+
if (named && named[2].trim())
|
|
156
|
+
return { id: named[1], name: named[2].trim() };
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
/** The street and town of the shop that `getParcelShopByID` returns, a line each, if it is the requested one. */
|
|
161
|
+
function addressOf(result, id) {
|
|
162
|
+
const shop = one(result, 'ParcelShop');
|
|
163
|
+
if (scalar(shop, 'PUDOID', 40) !== id)
|
|
164
|
+
return '';
|
|
165
|
+
const address = one(shop, 'ShopAddress');
|
|
166
|
+
const street = scalar(address, 'Street', 120);
|
|
167
|
+
const city = scalar(address, 'City', 80);
|
|
168
|
+
if (!street || !city)
|
|
169
|
+
return '';
|
|
170
|
+
return `${[street, scalar(address, 'HouseNo', 20)].filter(Boolean).join(' ')}\n${[scalar(address, 'ZipCode', 16), city].filter(Boolean).join(' ')}`;
|
|
157
171
|
}
|
|
158
172
|
/** The measured length, width and height, in millimetres; the shipper's own figures are not read. */
|
|
159
173
|
function dimensionsOf(order) {
|
|
@@ -162,7 +176,10 @@ function dimensionsOf(order) {
|
|
|
162
176
|
return null;
|
|
163
177
|
return `${sides.map(side => Number(side) / 10).join(' × ')} cm`;
|
|
164
178
|
}
|
|
165
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* Scans arrive oldest first, with a facility's wall clock and no offset. `shop` is the PUDO id of
|
|
181
|
+
* the shop the result names as its pickup point, else empty.
|
|
182
|
+
*/
|
|
166
183
|
export function parseDpdDeApp(data, scans, number) {
|
|
167
184
|
const tracking = one(data, 'TrackingData');
|
|
168
185
|
if (scalar(tracking, 'ParcelNo', 40) !== number)
|
|
@@ -174,7 +191,7 @@ export function parseDpdDeApp(data, scans, number) {
|
|
|
174
191
|
const seen = new Set();
|
|
175
192
|
let placed = '';
|
|
176
193
|
let announced = null;
|
|
177
|
-
let shop
|
|
194
|
+
let shop;
|
|
178
195
|
for (const row of rows) {
|
|
179
196
|
const wording = scalar(row, 'StatusText', 500);
|
|
180
197
|
if (!wording)
|
|
@@ -232,7 +249,9 @@ export function parseDpdDeApp(data, scans, number) {
|
|
|
232
249
|
const kilograms = Number(scalar(order, 'Weight', 16).replace(',', '.'));
|
|
233
250
|
const dimensions = dimensionsOf(order);
|
|
234
251
|
const deliveredAt = status === 'delivered' ? events.find(event => event.stage === 'delivered')?.time : undefined;
|
|
235
|
-
|
|
252
|
+
// The shop holding the parcel, while it waits there.
|
|
253
|
+
const pickup = stage === 'ready_for_pickup' ? shop : undefined;
|
|
254
|
+
const result = {
|
|
236
255
|
status, ...(stage ? { current_stage: stage, current_stage_source: 'carrier_map' } : {}),
|
|
237
256
|
last_status_text: current.description ?? null, last_update: current.time ?? null,
|
|
238
257
|
expected_delivery: affirmative(scalar(tracking, 'Delivered', 8)) || status === 'delivered' || status === 'exception'
|
|
@@ -241,10 +260,10 @@ export function parseDpdDeApp(data, scans, number) {
|
|
|
241
260
|
...(Number.isFinite(kilograms) && kilograms > 0 ? { weight_kg: kilograms } : {}),
|
|
242
261
|
...(dimensions ? { dimensions_text: dimensions } : {}),
|
|
243
262
|
...(deliveredAt ? { delivered_at: deliveredAt } : {}),
|
|
244
|
-
|
|
245
|
-
...(stage === 'ready_for_pickup' && shop ? { pickup_point: shop } : {}),
|
|
263
|
+
...(pickup ? { pickup_point: pickup.name } : {}),
|
|
246
264
|
events: events.slice(0, 100),
|
|
247
265
|
};
|
|
266
|
+
return { result, shop: pickup?.id ?? '' };
|
|
248
267
|
}
|
|
249
268
|
/**
|
|
250
269
|
* The German app's SOAP service. Tracking needs an anonymous device session, which the
|
|
@@ -301,7 +320,13 @@ export class DpdDeAppClient {
|
|
|
301
320
|
}
|
|
302
321
|
const scans = await this.call('getTrackingScanList', { SessionToken: session, ParcelNo: number,
|
|
303
322
|
DeliveryZipCode: verified ? postcode : '' }, options.signal, left());
|
|
304
|
-
const result = parseDpdDeApp(data, scans, number);
|
|
323
|
+
const { result, shop } = parseDpdDeApp(data, scans, number);
|
|
324
|
+
// The scans name the shop without its address, which the shop's own record adds.
|
|
325
|
+
if (shop) {
|
|
326
|
+
const address = await this.shopAddress(session, shop, options.signal, left());
|
|
327
|
+
if (address)
|
|
328
|
+
result.pickup_point = `${result.pickup_point}\n${address}`;
|
|
329
|
+
}
|
|
305
330
|
if (verified !== undefined)
|
|
306
331
|
result.dpd_postcode_verified = verified;
|
|
307
332
|
return result;
|
|
@@ -325,6 +350,17 @@ export class DpdDeAppClient {
|
|
|
325
350
|
throw new TransportError('DPD Germany', 'DPD Germany app request failed');
|
|
326
351
|
}
|
|
327
352
|
}
|
|
353
|
+
/** The shop's street and town, or nothing: the parcel is found, and its pickup point keeps its name without them. */
|
|
354
|
+
async shopAddress(session, id, signal, timeoutMs) {
|
|
355
|
+
try {
|
|
356
|
+
return addressOf(await this.call('getParcelShopByID', { SessionToken: session, ParcelShopID: '0', PudoID: id,
|
|
357
|
+
ParcelShopOnly: 'false' }, signal, timeoutMs), id);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
signal.throwIfAborted();
|
|
361
|
+
return '';
|
|
362
|
+
}
|
|
363
|
+
}
|
|
328
364
|
/**
|
|
329
365
|
* One session for every lookup. The opening runs while a lookup that asked for it is still
|
|
330
366
|
* running, even one that stopped waiting; each lookup waits until its signal or `waitMs` ends.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-parcel-scraper",
|
|
3
|
-
"version": "0.48.0-main.
|
|
3
|
+
"version": "0.48.0-main.489",
|
|
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",
|