universal-parcel-scraper 0.25.2-main.365 → 0.25.3-main.367
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/data/catalog.json +1 -1
- package/data/detection-golden.json +29 -0
- package/dist/browser/scraper.js +1 -1
- package/dist/carriers/dhl/carrier.json +1 -1
- package/dist/carriers/dhl/numbers.json +17 -0
- package/dist/carriers/dhl-express/clock.d.ts +4 -4
- package/dist/carriers/dhl-express/clock.js +15 -9
- package/dist/carriers/dhl-express/numbers.json +22 -0
- package/dist/data/catalog.json +1 -1
- package/dist/generated/catalog.d.ts +1 -1
- package/dist/generated/catalog.js +1 -1
- package/dist/providers/parcelsapp/adapter.js +11 -36
- package/dist/providers/seventeentrack/adapter.d.ts +0 -10
- package/dist/providers/seventeentrack/adapter.js +33 -2
- package/dist/providers/shared/facilityClock.d.ts +25 -0
- package/dist/providers/shared/facilityClock.js +36 -0
- package/dist/server/openapi.json +1 -1
- package/package.json +1 -1
package/data/catalog.json
CHANGED
|
@@ -155,6 +155,14 @@
|
|
|
155
155
|
"dhl"
|
|
156
156
|
]
|
|
157
157
|
},
|
|
158
|
+
{
|
|
159
|
+
"input": "00340435061352161527",
|
|
160
|
+
"carrier": "dhl",
|
|
161
|
+
"confidence": "high",
|
|
162
|
+
"candidates": [
|
|
163
|
+
"dhl"
|
|
164
|
+
]
|
|
165
|
+
},
|
|
158
166
|
{
|
|
159
167
|
"input": "00573132901903503247",
|
|
160
168
|
"carrier": "unknown",
|
|
@@ -2612,6 +2620,27 @@
|
|
|
2612
2620
|
"dpd-fr"
|
|
2613
2621
|
]
|
|
2614
2622
|
},
|
|
2623
|
+
{
|
|
2624
|
+
"input": "2522622410",
|
|
2625
|
+
"carrier": "unknown",
|
|
2626
|
+
"confidence": "low",
|
|
2627
|
+
"candidates": [
|
|
2628
|
+
"dhl-express",
|
|
2629
|
+
"dhl",
|
|
2630
|
+
"mondial-relay",
|
|
2631
|
+
"relais-colis",
|
|
2632
|
+
"tipsa",
|
|
2633
|
+
"estafeta"
|
|
2634
|
+
],
|
|
2635
|
+
"preferred": [
|
|
2636
|
+
"dhl-express"
|
|
2637
|
+
],
|
|
2638
|
+
"asked": [
|
|
2639
|
+
"dhl-express",
|
|
2640
|
+
"relais-colis",
|
|
2641
|
+
"tipsa"
|
|
2642
|
+
]
|
|
2643
|
+
},
|
|
2615
2644
|
{
|
|
2616
2645
|
"input": "25631818",
|
|
2617
2646
|
"carrier": "unknown",
|
package/dist/browser/scraper.js
CHANGED
|
@@ -15,6 +15,23 @@
|
|
|
15
15
|
},
|
|
16
16
|
"note": "Publicly reported shipment number; the attribution was not independently verified."
|
|
17
17
|
},
|
|
18
|
+
{
|
|
19
|
+
"number": "00340435061352161527",
|
|
20
|
+
"role": "shipment",
|
|
21
|
+
"evidence": "public_shipment_report",
|
|
22
|
+
"source": {
|
|
23
|
+
"url": "https://de.trustpilot.com/review/www.dhl.de",
|
|
24
|
+
"date": "2026-10-07"
|
|
25
|
+
},
|
|
26
|
+
"context": {
|
|
27
|
+
"assessment": "confirmed"
|
|
28
|
+
},
|
|
29
|
+
"expect": {
|
|
30
|
+
"carrier": "dhl",
|
|
31
|
+
"confidence": "high"
|
|
32
|
+
},
|
|
33
|
+
"note": "Publicly reported shipment number in the second 0034043 range; DHL's recipient endpoint returned its history."
|
|
34
|
+
},
|
|
18
35
|
{
|
|
19
36
|
"number": "8449089972",
|
|
20
37
|
"role": "shipment",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The zone of a DHL Express facility, from its location: "CITY - COUNTRY",
|
|
3
|
-
* "CITY - REGION - COUNTRY"
|
|
4
|
-
* not settle one: a country with several clocks and no
|
|
5
|
-
* country it writes in a form this does not know.
|
|
2
|
+
* The zone of a DHL Express facility, from its location: "CITY - COUNTRY", and
|
|
3
|
+
* in the USA and Canada "CITY - REGION - COUNTRY" or "CITY, ST - COUNTRY". Null
|
|
4
|
+
* when the location does not settle one: a country with several clocks and no
|
|
5
|
+
* region DHL names, or a country it writes in a form this does not know.
|
|
6
6
|
*
|
|
7
7
|
* A US state or Canadian province with several clocks takes its majority zone.
|
|
8
8
|
* Spain and Portugal take their mainland's, except for a town or island of the
|
|
@@ -16,6 +16,12 @@ const CANADA_PROVINCES = {
|
|
|
16
16
|
'NOVA SCOTIA': 'NS', 'NORTHWEST TERRITORIES': 'NT', NUNAVUT: 'NU', ONTARIO: 'ON', 'PRINCE EDWARD ISLAND': 'PE',
|
|
17
17
|
QUEBEC: 'QC', SASKATCHEWAN: 'SK', YUKON: 'YT',
|
|
18
18
|
};
|
|
19
|
+
// DHL's own names for countries the English ones do not cover: the forms on its
|
|
20
|
+
// scans, and those of its country list for the countries whose zone is known.
|
|
21
|
+
const COUNTRIES = {
|
|
22
|
+
UK: 'GB', "PEOPLE'S REPUBLIC OF CHINA": 'CN', 'CHINA, PEOPLES REPUBLIC': 'CN', 'CZECH REPUBLIC': 'CZ',
|
|
23
|
+
'IRELAND, REPUBLIC OF': 'IE', 'KOREA, REPUBLIC OF (SOUTH K.)': 'KR', TURKEY: 'TR',
|
|
24
|
+
};
|
|
19
25
|
// Island groups an hour behind their mainland, which DHL files under the country.
|
|
20
26
|
const ISLANDS = {
|
|
21
27
|
'Europe/Madrid': { group: 'ES-CN', zone: 'Atlantic/Canary' },
|
|
@@ -26,10 +32,10 @@ function regionCode(region, names) {
|
|
|
26
32
|
return parts.find((part) => /^[A-Z]{2}$/.test(part)) ?? parts.map((part) => names[part]).find(Boolean) ?? '';
|
|
27
33
|
}
|
|
28
34
|
/**
|
|
29
|
-
* The zone of a DHL Express facility, from its location: "CITY - COUNTRY",
|
|
30
|
-
* "CITY - REGION - COUNTRY"
|
|
31
|
-
* not settle one: a country with several clocks and no
|
|
32
|
-
* country it writes in a form this does not know.
|
|
35
|
+
* The zone of a DHL Express facility, from its location: "CITY - COUNTRY", and
|
|
36
|
+
* in the USA and Canada "CITY - REGION - COUNTRY" or "CITY, ST - COUNTRY". Null
|
|
37
|
+
* when the location does not settle one: a country with several clocks and no
|
|
38
|
+
* region DHL names, or a country it writes in a form this does not know.
|
|
33
39
|
*
|
|
34
40
|
* A US state or Canadian province with several clocks takes its majority zone.
|
|
35
41
|
* Spain and Portugal take their mainland's, except for a town or island of the
|
|
@@ -38,15 +44,15 @@ function regionCode(region, names) {
|
|
|
38
44
|
export function facilityZone(location) {
|
|
39
45
|
const parts = location.split(' - ').map((part) => part.trim());
|
|
40
46
|
const country = parts.length > 1 ? parts.at(-1).toUpperCase() : '';
|
|
41
|
-
|
|
47
|
+
// The region is a part of its own, or the code that ends the city's.
|
|
48
|
+
const region = parts.length > 2 ? parts.at(-2) : /, ([A-Z]{2})$/i.exec(parts[0])?.[1] ?? '';
|
|
42
49
|
if (country === 'USA')
|
|
43
50
|
return usStateTimeZone(regionCode(region, US_STATES));
|
|
44
51
|
if (country === 'CANADA')
|
|
45
52
|
return canadaProvinceTimeZone(regionCode(region, CANADA_PROVINCES));
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const zone = countryTimeZone(country.replace(/, THE$/, '').replaceAll(',', ''));
|
|
53
|
+
// DHL writes the article first or last: "THE PEOPLE'S REPUBLIC OF CHINA", "NETHERLANDS, THE".
|
|
54
|
+
const name = country.replace(/^THE /, '').replace(/, THE$/, '');
|
|
55
|
+
const zone = countryTimeZone(COUNTRIES[name] ?? name.replaceAll(',', ''));
|
|
50
56
|
const islands = zone ? ISLANDS[zone] : undefined;
|
|
51
57
|
return islands && regionHasTown(islands.group, parts[0]) ? islands.zone : zone;
|
|
52
58
|
}
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"carrier": "dhl-express",
|
|
3
3
|
"records": [
|
|
4
|
+
{
|
|
5
|
+
"number": "2522622410",
|
|
6
|
+
"role": "shipment",
|
|
7
|
+
"evidence": "open_source_example",
|
|
8
|
+
"source": {
|
|
9
|
+
"url": "https://github.com/iFoxz17/sc-delay-prediction/blob/b9e2f99fcdc157b4fb5ff3b0903e79f75f6b32cb/code/m4estro/LambdaTS/Carrier/sample.json",
|
|
10
|
+
"date": "2025-08-30"
|
|
11
|
+
},
|
|
12
|
+
"expect": {
|
|
13
|
+
"carrier": "unknown",
|
|
14
|
+
"confidence": "low",
|
|
15
|
+
"candidates": [
|
|
16
|
+
"dhl-express",
|
|
17
|
+
"dhl",
|
|
18
|
+
"mondial-relay",
|
|
19
|
+
"relais-colis",
|
|
20
|
+
"tipsa",
|
|
21
|
+
"estafeta"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"note": "A provider's reply for a DHL Express waybill, published as sample data with its scans. DHL has since issued the waybill to another shipment."
|
|
25
|
+
},
|
|
4
26
|
{
|
|
5
27
|
"number": "3408372365",
|
|
6
28
|
"role": "shipment",
|
package/dist/data/catalog.json
CHANGED
|
@@ -301,7 +301,7 @@ export declare const CARRIER_CATALOG: {
|
|
|
301
301
|
readonly pattern: "^JD\\d{18}$";
|
|
302
302
|
readonly confidence: "high";
|
|
303
303
|
}, {
|
|
304
|
-
readonly pattern: "^
|
|
304
|
+
readonly pattern: "^0034043[45]\\d{12}$";
|
|
305
305
|
readonly confidence: "high";
|
|
306
306
|
}, {
|
|
307
307
|
readonly pattern: "^JVGL[0-9]{20}$";
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
import { load } from 'cheerio';
|
|
11
11
|
import { DateTime } from 'luxon';
|
|
12
12
|
import timers from 'node:timers/promises';
|
|
13
|
-
import { facilityZone } from '../../carriers/dhl-express/clock.js';
|
|
14
13
|
import { carrierTimezone } from '../../core/catalog/index.js';
|
|
15
14
|
import { brandTimeZones, carrierIdFromName, carrierNameCountryZone } from '../../core/catalog/hints.js';
|
|
16
15
|
import { CarrierError, carrierErrorKind, ChallengeError, IndeterminateError, InputRequiredError, NoHistoryError, SchemaError, UpstreamHttpError, UpstreamNetworkError } from '../../core/errors/index.js';
|
|
@@ -18,6 +17,7 @@ import { runSteps } from '../../core/runner/index.js';
|
|
|
18
17
|
import { canadaProvinceTimeZone, countryCode, countryTimeZone, mislabeledLocalTime, mislabeledWallTime, regionHasTown, sharedClockZone, timeZoneCountry, usStateTimeZone, } from '../../core/time/index.js';
|
|
19
18
|
import { isRecord } from '../../core/types.js';
|
|
20
19
|
import { capturedBodies, loadCapture } from '../shared/capture.js';
|
|
20
|
+
import { facilityZone, LatestScan, relaysFacilityClock } from '../shared/facilityClock.js';
|
|
21
21
|
import { universalCarrierHints } from '../shared/hints.js';
|
|
22
22
|
import { event, isNotice, localEvent, numberOf, result } from '../shared/result.js';
|
|
23
23
|
import { carrierScan, markReturnLeg } from '../shared/scans.js';
|
|
@@ -117,29 +117,6 @@ function regionScanZone(name, location, inNorthAmerica) {
|
|
|
117
117
|
const region = typeof name === 'string' && LOCAL_CLOCK_CARRIERS.test(name.trim()) ? regionClock(location) : null;
|
|
118
118
|
return region && (region.certain || inNorthAmerica) ? region : null;
|
|
119
119
|
}
|
|
120
|
-
/** A DHL Express date with no zone for its facility: a wall time that no label makes an instant. */
|
|
121
|
-
function isFacilityClock(payload, state) {
|
|
122
|
-
const name = stateCarrierName(payload, state);
|
|
123
|
-
return typeof name === 'string' && carrierIdFromName(name) === 'dhl-express';
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* The newest wall clock of a reply, and whether that scan was left without an
|
|
127
|
-
* instant. A reply whose latest scan has none cannot say where the parcel is,
|
|
128
|
-
* and an older scan must not stand in for it.
|
|
129
|
-
*/
|
|
130
|
-
class LatestScan {
|
|
131
|
-
wall = '';
|
|
132
|
-
unplaced = false;
|
|
133
|
-
see(date, unplaced) {
|
|
134
|
-
const wall = mislabeledWallTime(date) ?? '';
|
|
135
|
-
if (wall > this.wall || (wall === this.wall && unplaced))
|
|
136
|
-
[this.wall, this.unplaced] = [wall, unplaced];
|
|
137
|
-
}
|
|
138
|
-
check() {
|
|
139
|
-
if (this.unplaced)
|
|
140
|
-
throw new IndeterminateError(SOURCE, 'ParcelsApp cannot place the latest DHL Express scan');
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
120
|
function scanZone(payload, state, fallback, number, inNorthAmerica = false) {
|
|
144
121
|
const name = stateCarrierName(payload, state);
|
|
145
122
|
const carrier = typeof name === 'string' ? carrierIdFromName(name) : undefined;
|
|
@@ -153,10 +130,8 @@ function scanZone(payload, state, fallback, number, inNorthAmerica = false) {
|
|
|
153
130
|
if (zone !== 'UTC' && !(typeof name === 'string' && carrierNameCountryZone(name)))
|
|
154
131
|
return zone;
|
|
155
132
|
const location = typeof state.location === 'string' ? state.location.trim() : '';
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
// the facility's offset (checked 2026-10-06). No other step applies to them.
|
|
159
|
-
if (carrier === 'dhl-express')
|
|
133
|
+
// A facility's clock, whatever the label: no other step applies to it.
|
|
134
|
+
if (relaysFacilityClock(name))
|
|
160
135
|
return facilityZone(location);
|
|
161
136
|
const place = location.split(',').at(-1);
|
|
162
137
|
// A state or province the town confirms beats the country its code also
|
|
@@ -242,14 +217,14 @@ function parseHistory(payload, trackingNumber, timezone = null) {
|
|
|
242
217
|
return wall && text ? `${wall}|${text}` : '';
|
|
243
218
|
};
|
|
244
219
|
const localClocks = new Map(entries.filter((entry) => entry.local).map((entry) => [copyKey(entry), entry.zone]).filter(([key]) => key));
|
|
245
|
-
const latest = new LatestScan();
|
|
220
|
+
const latest = new LatestScan(SOURCE);
|
|
246
221
|
for (const entry of entries) {
|
|
247
222
|
const { raw, name, scan } = entry;
|
|
248
223
|
const zone = (localClocks.size && !entry.local ? localClocks.get(copyKey(entry)) : undefined) ?? entry.zone;
|
|
249
|
-
const
|
|
250
|
-
const parsed = localEvent((zone ? mislabeledLocalTime(raw.date, zone)?.iso :
|
|
224
|
+
const unplaced = !zone && relaysFacilityClock(name);
|
|
225
|
+
const parsed = localEvent((zone ? mislabeledLocalTime(raw.date, zone)?.iso : unplaced ? mislabeledWallTime(raw.date) : undefined) ?? raw.date, entry.wording);
|
|
251
226
|
if (parsed)
|
|
252
|
-
latest.see(raw.date,
|
|
227
|
+
latest.see(mislabeledWallTime(raw.date) ?? '', unplaced);
|
|
253
228
|
// Nor has an offset-less date that no zone resolves: a wall time, not an instant.
|
|
254
229
|
if (parsed && !parsed.time) {
|
|
255
230
|
undated++;
|
|
@@ -320,7 +295,7 @@ export function parseParcelsAppHtml(html, trackingNumber, timezone = null) {
|
|
|
320
295
|
const carriers = [];
|
|
321
296
|
const scans = [];
|
|
322
297
|
let undated = 0;
|
|
323
|
-
const latest = new LatestScan();
|
|
298
|
+
const latest = new LatestScan(SOURCE);
|
|
324
299
|
nodes.each((_, node) => {
|
|
325
300
|
const row = $(node);
|
|
326
301
|
if (row.find('input, select, form').length)
|
|
@@ -352,9 +327,9 @@ export function parseParcelsAppHtml(html, trackingNumber, timezone = null) {
|
|
|
352
327
|
const zone = scanZone({ carriers }, state, timezone, numberOf(trackingNumber));
|
|
353
328
|
const scan = carrierScan(name ? carrierIdFromName(name) : undefined, description);
|
|
354
329
|
// The page prints no location, so no DHL Express scan has a facility to read its clock in.
|
|
355
|
-
const
|
|
356
|
-
latest.see(state.date,
|
|
357
|
-
if (
|
|
330
|
+
const unplaced = !zone && relaysFacilityClock(name);
|
|
331
|
+
latest.see(mislabeledWallTime(state.date) ?? '', unplaced);
|
|
332
|
+
if (unplaced) {
|
|
358
333
|
undated++;
|
|
359
334
|
return;
|
|
360
335
|
}
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 17TRACK (t.17track.net), a universal aggregator with richer China Post history.
|
|
3
|
-
*
|
|
4
|
-
* Transport: the browser service loads the public tracking page and captures
|
|
5
|
-
* the page's own `restapi` response; unsigned direct probes answer HTTP 200
|
|
6
|
-
* with a rejection code instead of history (verified 2026-09-10). The parser
|
|
7
|
-
* keeps only history whose shipment number equals the requested one, and turns
|
|
8
|
-
* the provider's own rejection codes into typed errors so routing can tell a
|
|
9
|
-
* verification wall from an outage.
|
|
10
|
-
*/
|
|
11
1
|
import type { AdapterFactory } from '../../core/adapter/index.js';
|
|
12
2
|
import { ChallengeError, NoHistoryError, TransportError } from '../../core/errors/index.js';
|
|
13
3
|
import type { CarrierResult } from '../../core/result/index.js';
|
|
@@ -1,8 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 17TRACK (t.17track.net), a universal aggregator with richer China Post history.
|
|
3
|
+
*
|
|
4
|
+
* Transport: the browser service loads the public tracking page and captures
|
|
5
|
+
* the page's own `restapi` response; unsigned direct probes answer HTTP 200
|
|
6
|
+
* with a rejection code instead of history (verified 2026-09-10). The parser
|
|
7
|
+
* keeps only history whose shipment number equals the requested one, and turns
|
|
8
|
+
* the provider's own rejection codes into typed errors so routing can tell a
|
|
9
|
+
* verification wall from an outage.
|
|
10
|
+
*/
|
|
11
|
+
import { DateTime } from 'luxon';
|
|
1
12
|
import { ChallengeError, NoHistoryError, NotFoundError, SchemaError, TransportError } from '../../core/errors/index.js';
|
|
2
13
|
import { runSteps } from '../../core/runner/index.js';
|
|
3
14
|
import { isRecord } from '../../core/types.js';
|
|
4
15
|
import { universalCarrierHints } from '../shared/hints.js';
|
|
5
16
|
import { capturedBodies, captureFailure, loadCapture } from '../shared/capture.js';
|
|
17
|
+
import { facilityZone, LatestScan, relaysFacilityClock } from '../shared/facilityClock.js';
|
|
6
18
|
import { numberOf, result, text } from '../shared/result.js';
|
|
7
19
|
import { seventeenTrackEvent } from './events.js';
|
|
8
20
|
const SOURCE = '17TRACK';
|
|
@@ -57,6 +69,12 @@ function lookupError(code, providerMessage) {
|
|
|
57
69
|
? new SeventeenTrackVerificationError(code, providerMessage)
|
|
58
70
|
: new SeventeenTrackLookupError('lookup_unavailable', code, providerMessage);
|
|
59
71
|
}
|
|
72
|
+
/** The offset-less clock of a scan whose offset 17TRACK added itself. */
|
|
73
|
+
function facilityWall(raw) {
|
|
74
|
+
const clock = isRecord(raw.time_raw) && raw.time_raw.timezone === null ? raw.time_raw : null;
|
|
75
|
+
return clock && typeof clock.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(clock.date)
|
|
76
|
+
&& typeof clock.time === 'string' && /^\d{2}:\d{2}:\d{2}$/.test(clock.time) ? `${clock.date}T${clock.time}` : null;
|
|
77
|
+
}
|
|
60
78
|
export function parse17TrackResponse(payload, trackingNumber) {
|
|
61
79
|
const number = numberOf(trackingNumber);
|
|
62
80
|
if (!isRecord(payload) || !isRecord(payload.meta) || !Number.isInteger(payload.meta.code)) {
|
|
@@ -91,6 +109,7 @@ export function parse17TrackResponse(payload, trackingNumber) {
|
|
|
91
109
|
const events = [];
|
|
92
110
|
let count = 0;
|
|
93
111
|
let undated = 0;
|
|
112
|
+
const latest = new LatestScan(SOURCE);
|
|
94
113
|
for (const provider of tracking.providers) {
|
|
95
114
|
if (!isRecord(provider) || !Array.isArray(provider.events))
|
|
96
115
|
throw new SchemaError(SOURCE, '17TRACK returned invalid provider history');
|
|
@@ -103,17 +122,29 @@ export function parse17TrackResponse(payload, trackingNumber) {
|
|
|
103
122
|
throw new SchemaError(SOURCE, '17TRACK returned invalid events');
|
|
104
123
|
if (raw.time_utc == null && raw.time_iso == null)
|
|
105
124
|
undated++;
|
|
125
|
+
// A facility's clock, on which 17TRACK puts one offset for the whole parcel.
|
|
126
|
+
const wall = relaysFacilityClock(name) ? facilityWall(raw) : null;
|
|
127
|
+
const zone = wall ? facilityZone(text(raw.location)) : null;
|
|
128
|
+
const placed = wall && zone ? DateTime.fromISO(wall, { zone }) : null;
|
|
106
129
|
let parsed;
|
|
107
130
|
try {
|
|
108
|
-
parsed = seventeenTrackEvent(raw, operator);
|
|
131
|
+
parsed = seventeenTrackEvent(placed?.isValid ? { ...raw, time_utc: placed.toUTC().toISO() } : raw, operator);
|
|
109
132
|
}
|
|
110
133
|
catch (cause) {
|
|
111
134
|
throw new SchemaError(SOURCE, '17TRACK returned an invalid tracking event', { cause });
|
|
112
135
|
}
|
|
113
|
-
if (parsed)
|
|
136
|
+
if (!parsed)
|
|
137
|
+
continue;
|
|
138
|
+
const unplaced = wall !== null && !placed?.isValid;
|
|
139
|
+
latest.see(wall ?? String(raw.time_iso ?? raw.time_utc).slice(0, 19), unplaced);
|
|
140
|
+
// With no zone for the facility it is a wall time, counted like a row without a date.
|
|
141
|
+
if (unplaced)
|
|
142
|
+
undated++;
|
|
143
|
+
else
|
|
114
144
|
events.push(parsed);
|
|
115
145
|
}
|
|
116
146
|
}
|
|
147
|
+
latest.check();
|
|
117
148
|
if (count === 0 && isRecord(shipment.latest_status) && shipment.latest_status.status === 'NotFound') {
|
|
118
149
|
throw new NotFoundError(SOURCE);
|
|
119
150
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { facilityZone } from '../../carriers/dhl-express/clock.js';
|
|
2
|
+
import type { UniversalSource } from './result.js';
|
|
3
|
+
/**
|
|
4
|
+
* DHL Express dates a scan with its facility's clock and no offset, and the
|
|
5
|
+
* providers relay that clock under a label of their own: UTC at ParcelsApp, one
|
|
6
|
+
* offset for the whole parcel at 17TRACK. Ship24 relays it with the facility's
|
|
7
|
+
* offset, and DHL's mobile API gives the same clocks scan for scan (checked
|
|
8
|
+
* 2026-10-06). So such a scan is read in its facility's zone, and with no zone
|
|
9
|
+
* for the facility it is a wall time that no label makes an instant.
|
|
10
|
+
*/
|
|
11
|
+
export declare function relaysFacilityClock(carrierName: unknown): boolean;
|
|
12
|
+
export { facilityZone };
|
|
13
|
+
/**
|
|
14
|
+
* The newest wall clock of a reply, and whether that scan was left without an
|
|
15
|
+
* instant. A reply whose latest scan has none cannot say where the parcel is,
|
|
16
|
+
* and an older scan must not stand in for it.
|
|
17
|
+
*/
|
|
18
|
+
export declare class LatestScan {
|
|
19
|
+
private readonly source;
|
|
20
|
+
private wall;
|
|
21
|
+
private unplaced;
|
|
22
|
+
constructor(source: UniversalSource);
|
|
23
|
+
see(wall: string, unplaced: boolean): void;
|
|
24
|
+
check(): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { facilityZone } from '../../carriers/dhl-express/clock.js';
|
|
2
|
+
import { carrierIdFromName } from '../../core/catalog/hints.js';
|
|
3
|
+
import { IndeterminateError } from '../../core/errors/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* DHL Express dates a scan with its facility's clock and no offset, and the
|
|
6
|
+
* providers relay that clock under a label of their own: UTC at ParcelsApp, one
|
|
7
|
+
* offset for the whole parcel at 17TRACK. Ship24 relays it with the facility's
|
|
8
|
+
* offset, and DHL's mobile API gives the same clocks scan for scan (checked
|
|
9
|
+
* 2026-10-06). So such a scan is read in its facility's zone, and with no zone
|
|
10
|
+
* for the facility it is a wall time that no label makes an instant.
|
|
11
|
+
*/
|
|
12
|
+
export function relaysFacilityClock(carrierName) {
|
|
13
|
+
return typeof carrierName === 'string' && carrierIdFromName(carrierName) === 'dhl-express';
|
|
14
|
+
}
|
|
15
|
+
export { facilityZone };
|
|
16
|
+
/**
|
|
17
|
+
* The newest wall clock of a reply, and whether that scan was left without an
|
|
18
|
+
* instant. A reply whose latest scan has none cannot say where the parcel is,
|
|
19
|
+
* and an older scan must not stand in for it.
|
|
20
|
+
*/
|
|
21
|
+
export class LatestScan {
|
|
22
|
+
source;
|
|
23
|
+
wall = '';
|
|
24
|
+
unplaced = false;
|
|
25
|
+
constructor(source) {
|
|
26
|
+
this.source = source;
|
|
27
|
+
}
|
|
28
|
+
see(wall, unplaced) {
|
|
29
|
+
if (wall > this.wall || (wall === this.wall && unplaced))
|
|
30
|
+
[this.wall, this.unplaced] = [wall, unplaced];
|
|
31
|
+
}
|
|
32
|
+
check() {
|
|
33
|
+
if (this.unplaced)
|
|
34
|
+
throw new IndeterminateError(this.source, `${this.source} cannot place the latest DHL Express scan`);
|
|
35
|
+
}
|
|
36
|
+
}
|
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.25.
|
|
3
|
+
"version": "0.25.3-main.367",
|
|
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",
|