universal-parcel-scraper 0.3.3-main.315 → 0.3.4-main.317

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 1,672 such statuses, each filed under one stage.
152
+ The carrier folders record 1,673 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.
@@ -22,7 +22,8 @@ export function parseDelhivery(payload, number) {
22
22
  throw new SchemaError('Delhivery');
23
23
  if (!payload.data.length && payload.message === 'invalid AWB or very old package')
24
24
  throw new NotFoundError('Delhivery');
25
- const matches = payload.data.filter(isRecord).filter(item => clean(item.awb, 64) === requested);
25
+ const matches = payload.data.filter(isRecord).filter(item => (typeof item.awb === 'string' ? item.awb.trim()
26
+ : typeof item.awb === 'number' && Number.isSafeInteger(item.awb) ? String(item.awb) : '') === requested);
26
27
  if (matches.length !== 1)
27
28
  throw new SchemaError('Delhivery', 'Delhivery did not return one matching shipment');
28
29
  const item = matches[0];
@@ -31,7 +32,14 @@ export function parseDelhivery(payload, number) {
31
32
  const description = clean(item.status.status, 200);
32
33
  if (!description)
33
34
  throw new IndeterminateError('Delhivery', 'Delhivery returned no current status');
34
- const mapped = classifyDelhiveryStatus(description);
35
+ const returning = item.status.statusType === 'RT' || item.currentFlow === 'Returned' || item.currentFlow === 'Reverse';
36
+ // RETURNED names a flow, including parcels still travelling to the sender.
37
+ // The separate shipment status establishes movement or completed delivery.
38
+ const snapshotDescription = returning && description.toUpperCase() === 'RETURNED'
39
+ ? clean(item.hqStatus, 200) : description;
40
+ const current = classifyDelhiveryStatus(snapshotDescription);
41
+ const mapped = returning && current?.stage === 'delivered'
42
+ ? classifyDelhiveryStatus('RTO DELIVERED') : current;
35
43
  const updated = scanTime(item.status.statusDateTime);
36
44
  const currentTimeText = clean(item.status.statusDateTime, 64);
37
45
  const events = [];
@@ -56,21 +64,25 @@ export function parseDelhivery(payload, number) {
56
64
  const time = scanTime(scan.scanDateTime);
57
65
  const timeText = clean(scan.scanDateTime, 64);
58
66
  const location = clean(scan.cityLocation, 160);
59
- const key = `${time?.timestamp ?? timeText}\u0000${label}\u0000${location}`;
67
+ const returnScan = scan.scanType === 'RT' || item.currentFlow === 'Reverse';
68
+ const key = `${time?.timestamp ?? timeText}\u0000${label}\u0000${location}\u0000${returnScan ? 'return' : ''}`;
60
69
  if (seen.has(key))
61
70
  continue;
62
71
  seen.add(key);
63
- const classified = classifyDelhiveryStatus(label);
72
+ const scanStatus = classifyDelhiveryStatus(label);
73
+ const classified = returnScan && scanStatus?.stage === 'delivered'
74
+ ? classifyDelhiveryStatus('RTO DELIVERED') : scanStatus;
64
75
  events.push({ description: label, ...(time ? { time: time.iso } : timeText ? { provider_time_text: timeText } : {}), ...(location ? { location } : {}),
65
- ...(classified ? { stage: classified.stage } : {}) });
76
+ ...(classified ? { stage: classified.stage } : {}), ...(returnScan ? { provider_leg: 'return' } : {}) });
66
77
  }
67
78
  }
68
79
  // Repeated scan labels cannot bind a summary timestamp to a particular
69
80
  // historical location. Keep the dated status snapshot separate.
70
- if (!updated || !events.some(event => event.time && Date.parse(event.time) === updated.timestamp
71
- && event.description?.toUpperCase() === description.toUpperCase())) {
72
- events.unshift({ description, ...(updated ? { time: updated.iso } : currentTimeText ? { provider_time_text: currentTimeText } : {}),
73
- ...(mapped ? { stage: mapped.stage } : {}), summary_snapshot: true });
81
+ if (snapshotDescription && (!updated || !events.some(event => event.time && Date.parse(event.time) === updated.timestamp
82
+ && event.description?.toUpperCase() === snapshotDescription.toUpperCase() && event.stage === mapped?.stage
83
+ && (!returning || event.provider_leg === 'return')))) {
84
+ events.unshift({ description: snapshotDescription, ...(updated ? { time: updated.iso } : currentTimeText ? { provider_time_text: currentTimeText } : {}),
85
+ ...(mapped ? { stage: mapped.stage } : {}), ...(returning ? { provider_leg: 'return' } : {}), summary_snapshot: true });
74
86
  }
75
87
  const dated = events.filter(event => event.time).sort((a, b) => Date.parse(b.time) - Date.parse(a.time));
76
88
  const undated = events.filter(event => !event.time);
@@ -2,9 +2,11 @@ const statuses = {
2
2
  DELIVERED: { status: 'delivered', stage: 'delivered' },
3
3
  'OUT FOR DELIVERY': { status: 'out_for_delivery', stage: 'out_for_delivery' },
4
4
  'IN TRANSIT': { status: 'in_transit', stage: 'in_transit' },
5
+ 'IN TRANSIT FOR RETURN': { status: 'in_transit', stage: 'in_transit' },
5
6
  'PICKED UP': { status: 'in_transit', stage: 'accepted' },
6
7
  MANIFESTED: { status: 'pending', stage: 'registered' },
7
8
  'RTO DELIVERED': { status: 'exception', stage: 'returned' },
9
+ DELIVERED_SELLER: { status: 'exception', stage: 'returned' },
8
10
  };
9
11
  export function classifyDelhiveryStatus(value) {
10
12
  return statuses[value.toUpperCase()];
@@ -6,6 +6,12 @@
6
6
  "language": "en",
7
7
  "stage": "delivered",
8
8
  "confirmedBy": "live"
9
+ },
10
+ {
11
+ "wording": "In Transit",
12
+ "language": "en",
13
+ "stage": "in_transit",
14
+ "confirmedBy": "live"
9
15
  }
10
16
  ]
11
17
  }
@@ -1,5 +1,5 @@
1
1
  import { lookupBudget } from '../../core/adapter/index.js';
2
- import { InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
2
+ import { IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
3
3
  import { zonedTime } from '../../core/time/index.js';
4
4
  import { clean, fetchBounded, parseJsonBytes, UpstreamHttpError, userAgentOf } from '../../core/transport/index.js';
5
5
  import { isRecord } from '../../core/types.js';
@@ -52,11 +52,13 @@ export function parsePacketaTrackingResponse(payload, trackingNumber) {
52
52
  if (!isRecord(payload))
53
53
  throw new SchemaError('Packeta', 'Packeta returned an invalid tracking response');
54
54
  const item = payload.item;
55
- // A 200 carrying an error instead of an item is Packeta's second unknown-code
56
- // signal (mirrors the HTTP 404 contract); it is a domain outcome, not a crash.
55
+ // Only the named notFound error establishes absence; other API errors say
56
+ // nothing about the requested parcel.
57
57
  if (!isRecord(item)) {
58
- if (typeof payload.error === 'string')
58
+ if (payload.error === 'notFound')
59
59
  throw new NotFoundError('Packeta');
60
+ if (typeof payload.error === 'string')
61
+ throw new IndeterminateError('Packeta', 'Packeta returned a tracking error');
60
62
  throw new SchemaError('Packeta', 'Packeta returned an invalid tracking response');
61
63
  }
62
64
  const returned = typeof item.barcode === 'string'
@@ -159,8 +161,18 @@ export class PacketaTracker {
159
161
  allowHttpError: true,
160
162
  fetcher: this.fetcher,
161
163
  });
162
- if (response.status === 404)
163
- throw new NotFoundError('Packeta');
164
+ if (response.status === 404) {
165
+ let payload;
166
+ try {
167
+ payload = parseJsonBytes(bytes, 'Packeta tracking');
168
+ }
169
+ catch {
170
+ throw new UpstreamHttpError('Packeta tracking', response.status);
171
+ }
172
+ if (isRecord(payload) && payload.error === 'notFound')
173
+ throw new NotFoundError('Packeta');
174
+ throw new UpstreamHttpError('Packeta tracking', response.status);
175
+ }
164
176
  if (!response.ok)
165
177
  throw new UpstreamHttpError('Packeta tracking', response.status);
166
178
  return parsePacketaTrackingResponse(parseJsonBytes(bytes, 'Packeta tracking'), trackingNumber);
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.3.3"
5
+ "version": "0.3.4"
6
6
  },
7
7
  "paths": {
8
8
  "/health": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-parcel-scraper",
3
- "version": "0.3.3-main.315",
3
+ "version": "0.3.4-main.317",
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",