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
- /** The relay or locker name is the first part of its address; the rest is never read. */
61
- function pickupName(point) {
62
- const parts = point.split(' - ');
63
- const name = clean(parts[0], 200);
64
- return parts.length >= 3 && /\p{L}/u.test(name) && /^[\p{L}\p{M}\p{N} .,'’&()/-]+$/u.test(name) ? name : '';
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's name are projected.
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), pickup: collection && point ? pickupName(point) : '' };
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
- /** Scans arrive oldest first, with a facility's wall clock and no offset. */
22
- export declare function parseDpdDeApp(data: XmlNode, scans: XmlNode, number: string): CarrierResult;
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 `<shop id>|<shop name>` under additional code 999. */
151
+ /** The shop a scan names, as `<PUDO id>|<shop name>` under additional code 999. */
152
152
  function shopOf(row) {
153
- const named = children(row, 'TrackingScanAdditionalList').flatMap(list => children(list, 'TrackingScanAdditionalType'))
154
- .filter(entry => scalar(entry, 'AdditionalCode', 8) === '999')
155
- .map(entry => /^[A-Z]{2}\d{1,12}\|(.{1,120})$/.exec(scalar(entry, 'Description', 200))?.[1]?.trim() ?? '');
156
- return named.find(Boolean) ?? '';
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
- /** Scans arrive oldest first, with a facility's wall clock and no offset. */
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
- return {
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
- // The shop holding the parcel, while it waits there.
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.487",
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",