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,
|
|
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 =>
|
|
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
|
|
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
|
|
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
|
|
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() ===
|
|
72
|
-
|
|
73
|
-
|
|
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()];
|
|
@@ -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
|
-
//
|
|
56
|
-
//
|
|
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 (
|
|
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
|
-
|
|
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);
|
package/dist/server/openapi.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-parcel-scraper",
|
|
3
|
-
"version": "0.3.
|
|
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",
|