universal-parcel-scraper 0.94.0-main.990 → 0.95.0-main.992
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 +2 -2
- package/data/catalog.json +11 -2
- package/data/coverage-summary.json +1 -1
- package/data/detection-golden.json +40 -10
- package/dist/browser/scraper.js +11 -3
- package/dist/carriers/dao/adapter.d.ts +16 -0
- package/dist/carriers/dao/adapter.js +70 -0
- package/dist/carriers/dao/carrier.json +66 -8
- package/dist/carriers/dao/numbers.json +17 -7
- package/dist/carriers/dao/parser.d.ts +27 -0
- package/dist/carriers/dao/parser.js +141 -0
- package/dist/carriers/dao/status.d.ts +10 -0
- package/dist/carriers/dao/status.js +46 -0
- package/dist/carriers/dao/statuses.json +168 -1
- package/dist/core/catalog/statusMaps.js +2 -1
- package/dist/data/catalog.json +11 -2
- package/dist/generated/catalog.d.ts +9 -2
- package/dist/generated/catalog.js +12 -3
- package/dist/generated/recognition.js +1 -0
- package/dist/generated/registry.js +3 -1
- package/dist/server/openapi.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ The engine behind [Peek](https://peektracker.com) ([GitHub](https://github.com/p
|
|
|
13
13
|
[](LICENSE)
|
|
14
14
|
|
|
15
15
|
<!-- GENERATED:summary -->
|
|
16
|
-
**3,500+ carriers** through **
|
|
16
|
+
**3,500+ carriers** through **154 dedicated adapters** and **5 universal fallbacks**
|
|
17
17
|
|
|
18
18
|
<sub>208 catalog entries · 66 countries represented</sub>
|
|
19
19
|
<!-- /GENERATED:summary -->
|
|
@@ -155,7 +155,7 @@ createTracker({ providers: ['ParcelsApp', 'Ship24', '17TRACK', 'Postal Ninja', '
|
|
|
155
155
|
<!-- GENERATED:stages -->
|
|
156
156
|
<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">
|
|
157
157
|
|
|
158
|
-
The carrier folders record 4,
|
|
158
|
+
The carrier folders record 4,298 such statuses, each filed under one stage.
|
|
159
159
|
<!-- /GENERATED:stages -->
|
|
160
160
|
|
|
161
161
|
Wording nobody recorded yet goes through a classifier that reads seven European languages.
|
package/data/catalog.json
CHANGED
|
@@ -5249,7 +5249,9 @@
|
|
|
5249
5249
|
"timezone": "Europe/Copenhagen",
|
|
5250
5250
|
"tracking": {
|
|
5251
5251
|
"mode": "automatic",
|
|
5252
|
-
"adapter": "
|
|
5252
|
+
"adapter": "dao",
|
|
5253
|
+
"recognitionRank": 850,
|
|
5254
|
+
"localClocks": true
|
|
5253
5255
|
},
|
|
5254
5256
|
"canaryUrl": "https://dao.as/find-din-pakke/",
|
|
5255
5257
|
"trackingUrlTemplate": "https://dao.as/find-din-pakke/#q={trackingNumber}",
|
|
@@ -5276,7 +5278,14 @@
|
|
|
5276
5278
|
]
|
|
5277
5279
|
}
|
|
5278
5280
|
],
|
|
5279
|
-
"detectionRules": [
|
|
5281
|
+
"detectionRules": [
|
|
5282
|
+
{
|
|
5283
|
+
"pattern": "^000(?:5712696|5715127)\\d{10}$",
|
|
5284
|
+
"confidence": "low",
|
|
5285
|
+
"checksum": "sscc",
|
|
5286
|
+
"preferred": true
|
|
5287
|
+
}
|
|
5288
|
+
]
|
|
5280
5289
|
},
|
|
5281
5290
|
"day-ross": {
|
|
5282
5291
|
"displayName": "Day & Ross",
|
|
@@ -162,14 +162,17 @@
|
|
|
162
162
|
"confidence": "low",
|
|
163
163
|
"candidates": [
|
|
164
164
|
"postnord",
|
|
165
|
+
"dao",
|
|
165
166
|
"swiss-post-cargo",
|
|
166
167
|
"usps",
|
|
167
168
|
"nz-post"
|
|
168
169
|
],
|
|
169
170
|
"preferred": [
|
|
170
|
-
"postnord"
|
|
171
|
+
"postnord",
|
|
172
|
+
"dao"
|
|
171
173
|
],
|
|
172
174
|
"asked": [
|
|
175
|
+
"dao",
|
|
173
176
|
"postnord",
|
|
174
177
|
"swiss-post-cargo",
|
|
175
178
|
"nz-post"
|
|
@@ -181,14 +184,17 @@
|
|
|
181
184
|
"confidence": "low",
|
|
182
185
|
"candidates": [
|
|
183
186
|
"postnord",
|
|
187
|
+
"dao",
|
|
184
188
|
"swiss-post-cargo",
|
|
185
189
|
"usps",
|
|
186
190
|
"nz-post"
|
|
187
191
|
],
|
|
188
192
|
"preferred": [
|
|
189
|
-
"postnord"
|
|
193
|
+
"postnord",
|
|
194
|
+
"dao"
|
|
190
195
|
],
|
|
191
196
|
"asked": [
|
|
197
|
+
"dao",
|
|
192
198
|
"postnord",
|
|
193
199
|
"swiss-post-cargo",
|
|
194
200
|
"nz-post"
|
|
@@ -200,14 +206,17 @@
|
|
|
200
206
|
"confidence": "low",
|
|
201
207
|
"candidates": [
|
|
202
208
|
"postnord",
|
|
209
|
+
"dao",
|
|
203
210
|
"swiss-post-cargo",
|
|
204
211
|
"usps",
|
|
205
212
|
"nz-post"
|
|
206
213
|
],
|
|
207
214
|
"preferred": [
|
|
208
|
-
"postnord"
|
|
215
|
+
"postnord",
|
|
216
|
+
"dao"
|
|
209
217
|
],
|
|
210
218
|
"asked": [
|
|
219
|
+
"dao",
|
|
211
220
|
"postnord",
|
|
212
221
|
"swiss-post-cargo",
|
|
213
222
|
"nz-post"
|
|
@@ -219,14 +228,17 @@
|
|
|
219
228
|
"confidence": "low",
|
|
220
229
|
"candidates": [
|
|
221
230
|
"postnord",
|
|
231
|
+
"dao",
|
|
222
232
|
"swiss-post-cargo",
|
|
223
233
|
"usps",
|
|
224
234
|
"nz-post"
|
|
225
235
|
],
|
|
226
236
|
"preferred": [
|
|
227
|
-
"postnord"
|
|
237
|
+
"postnord",
|
|
238
|
+
"dao"
|
|
228
239
|
],
|
|
229
240
|
"asked": [
|
|
241
|
+
"dao",
|
|
230
242
|
"postnord",
|
|
231
243
|
"swiss-post-cargo",
|
|
232
244
|
"nz-post"
|
|
@@ -238,14 +250,17 @@
|
|
|
238
250
|
"confidence": "low",
|
|
239
251
|
"candidates": [
|
|
240
252
|
"postnord",
|
|
253
|
+
"dao",
|
|
241
254
|
"swiss-post-cargo",
|
|
242
255
|
"usps",
|
|
243
256
|
"nz-post"
|
|
244
257
|
],
|
|
245
258
|
"preferred": [
|
|
246
|
-
"postnord"
|
|
259
|
+
"postnord",
|
|
260
|
+
"dao"
|
|
247
261
|
],
|
|
248
262
|
"asked": [
|
|
263
|
+
"dao",
|
|
249
264
|
"postnord",
|
|
250
265
|
"swiss-post-cargo",
|
|
251
266
|
"nz-post"
|
|
@@ -257,14 +272,17 @@
|
|
|
257
272
|
"confidence": "low",
|
|
258
273
|
"candidates": [
|
|
259
274
|
"postnord",
|
|
275
|
+
"dao",
|
|
260
276
|
"swiss-post-cargo",
|
|
261
277
|
"usps",
|
|
262
278
|
"nz-post"
|
|
263
279
|
],
|
|
264
280
|
"preferred": [
|
|
265
|
-
"postnord"
|
|
281
|
+
"postnord",
|
|
282
|
+
"dao"
|
|
266
283
|
],
|
|
267
284
|
"asked": [
|
|
285
|
+
"dao",
|
|
268
286
|
"postnord",
|
|
269
287
|
"swiss-post-cargo",
|
|
270
288
|
"nz-post"
|
|
@@ -276,14 +294,17 @@
|
|
|
276
294
|
"confidence": "low",
|
|
277
295
|
"candidates": [
|
|
278
296
|
"postnord",
|
|
297
|
+
"dao",
|
|
279
298
|
"swiss-post-cargo",
|
|
280
299
|
"usps",
|
|
281
300
|
"nz-post"
|
|
282
301
|
],
|
|
283
302
|
"preferred": [
|
|
284
|
-
"postnord"
|
|
303
|
+
"postnord",
|
|
304
|
+
"dao"
|
|
285
305
|
],
|
|
286
306
|
"asked": [
|
|
307
|
+
"dao",
|
|
287
308
|
"postnord",
|
|
288
309
|
"swiss-post-cargo",
|
|
289
310
|
"nz-post"
|
|
@@ -295,14 +316,17 @@
|
|
|
295
316
|
"confidence": "low",
|
|
296
317
|
"candidates": [
|
|
297
318
|
"postnord",
|
|
319
|
+
"dao",
|
|
298
320
|
"swiss-post-cargo",
|
|
299
321
|
"usps",
|
|
300
322
|
"nz-post"
|
|
301
323
|
],
|
|
302
324
|
"preferred": [
|
|
303
|
-
"postnord"
|
|
325
|
+
"postnord",
|
|
326
|
+
"dao"
|
|
304
327
|
],
|
|
305
328
|
"asked": [
|
|
329
|
+
"dao",
|
|
306
330
|
"postnord",
|
|
307
331
|
"swiss-post-cargo",
|
|
308
332
|
"nz-post"
|
|
@@ -314,14 +338,17 @@
|
|
|
314
338
|
"confidence": "low",
|
|
315
339
|
"candidates": [
|
|
316
340
|
"postnord",
|
|
341
|
+
"dao",
|
|
317
342
|
"swiss-post-cargo",
|
|
318
343
|
"usps",
|
|
319
344
|
"nz-post"
|
|
320
345
|
],
|
|
321
346
|
"preferred": [
|
|
322
|
-
"postnord"
|
|
347
|
+
"postnord",
|
|
348
|
+
"dao"
|
|
323
349
|
],
|
|
324
350
|
"asked": [
|
|
351
|
+
"dao",
|
|
325
352
|
"postnord",
|
|
326
353
|
"swiss-post-cargo",
|
|
327
354
|
"nz-post"
|
|
@@ -333,14 +360,17 @@
|
|
|
333
360
|
"confidence": "low",
|
|
334
361
|
"candidates": [
|
|
335
362
|
"postnord",
|
|
363
|
+
"dao",
|
|
336
364
|
"swiss-post-cargo",
|
|
337
365
|
"usps",
|
|
338
366
|
"nz-post"
|
|
339
367
|
],
|
|
340
368
|
"preferred": [
|
|
341
|
-
"postnord"
|
|
369
|
+
"postnord",
|
|
370
|
+
"dao"
|
|
342
371
|
],
|
|
343
372
|
"asked": [
|
|
373
|
+
"dao",
|
|
344
374
|
"postnord",
|
|
345
375
|
"swiss-post-cargo",
|
|
346
376
|
"nz-post"
|
package/dist/browser/scraper.js
CHANGED
|
@@ -4274,7 +4274,9 @@ var be = {
|
|
|
4274
4274
|
timezone: "Europe/Copenhagen",
|
|
4275
4275
|
tracking: {
|
|
4276
4276
|
mode: "automatic",
|
|
4277
|
-
adapter: "
|
|
4277
|
+
adapter: "dao",
|
|
4278
|
+
recognitionRank: 850,
|
|
4279
|
+
localClocks: !0
|
|
4278
4280
|
},
|
|
4279
4281
|
canaryUrl: "https://dao.as/find-din-pakke/",
|
|
4280
4282
|
trackingUrlTemplate: "https://dao.as/find-din-pakke/#q={trackingNumber}",
|
|
@@ -4289,7 +4291,12 @@ var be = {
|
|
|
4289
4291
|
pathPattern: "^/(?:en/private/track-your-parcel/?|dao-app/?)?$",
|
|
4290
4292
|
params: ["stregkode"]
|
|
4291
4293
|
}],
|
|
4292
|
-
detectionRules: [
|
|
4294
|
+
detectionRules: [{
|
|
4295
|
+
pattern: "^000(?:5712696|5715127)\\d{10}$",
|
|
4296
|
+
confidence: "low",
|
|
4297
|
+
checksum: "sscc",
|
|
4298
|
+
preferred: !0
|
|
4299
|
+
}]
|
|
4293
4300
|
},
|
|
4294
4301
|
"day-ross": {
|
|
4295
4302
|
displayName: "Day & Ross",
|
|
@@ -7093,7 +7100,7 @@ var be = {
|
|
|
7093
7100
|
cne: ["cne-1"],
|
|
7094
7101
|
"correos-de-mexico": [],
|
|
7095
7102
|
"cyprus-post": [],
|
|
7096
|
-
dao: [],
|
|
7103
|
+
dao: ["dao-1"],
|
|
7097
7104
|
"day-ross": ["day-ross-1"],
|
|
7098
7105
|
"dhl-ecommerce-es": ["dhl-ecommerce-es-1", "dhl-ecommerce-es-2"],
|
|
7099
7106
|
"dhl-ecommerce-nl": [
|
|
@@ -11921,6 +11928,7 @@ var bc = {
|
|
|
11921
11928
|
"colis-prive": 300,
|
|
11922
11929
|
"correos-express": 360,
|
|
11923
11930
|
"ctt-express": 350,
|
|
11931
|
+
dao: 850,
|
|
11924
11932
|
delhivery: 160,
|
|
11925
11933
|
"dhl-ecommerce": 230,
|
|
11926
11934
|
"dhl-ecommerce-es": 190,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type AdapterFactory, type TrackingContext } from '../../core/adapter/index.js';
|
|
2
|
+
import type { CarrierResult } from '../../core/result/index.js';
|
|
3
|
+
import { type StepRecorder } from '../../core/telemetry/index.js';
|
|
4
|
+
export declare const DAO_ENDPOINT = "https://dao.as/wp-json/dao-wp/v1/track-parcel";
|
|
5
|
+
/** The service's address for one number, with English wording. */
|
|
6
|
+
export declare function daoTrackingUrl(raw: string): string;
|
|
7
|
+
export declare class DaoTracker {
|
|
8
|
+
private readonly options;
|
|
9
|
+
constructor(options?: {
|
|
10
|
+
fetcher?: typeof fetch;
|
|
11
|
+
recorder?: StepRecorder;
|
|
12
|
+
userAgent?: string;
|
|
13
|
+
});
|
|
14
|
+
fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
|
|
15
|
+
}
|
|
16
|
+
export declare const adapter: AdapterFactory;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
|
|
2
|
+
import { ChallengeError, IndeterminateError, SchemaError, TransportError, UpstreamHttpError } from '../../core/errors/index.js';
|
|
3
|
+
import { runSteps } from '../../core/runner/index.js';
|
|
4
|
+
import { firstAttemptMs, networkRetryStep } from '../../core/runner/networkRetry.js';
|
|
5
|
+
import { NOOP_RECORDER } from '../../core/telemetry/index.js';
|
|
6
|
+
import { decodeText, fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
|
|
7
|
+
import { DAO_MAX_BYTES, normalizeDaoNumber, parseDao, PROVIDER } from './parser.js';
|
|
8
|
+
// The service dao.as/find-din-pakke asks, without a key, nonce or cookie.
|
|
9
|
+
export const DAO_ENDPOINT = 'https://dao.as/wp-json/dao-wp/v1/track-parcel';
|
|
10
|
+
// A WordPress or edge block page instead of the service's JSON.
|
|
11
|
+
const BLOCKED = /<title>\s*(?:Just a moment|Attention Required|Access denied|403 Forbidden)|cf-chl-|cf-turnstile|captcha/i;
|
|
12
|
+
/** The service's address for one number, with English wording. */
|
|
13
|
+
export function daoTrackingUrl(raw) {
|
|
14
|
+
return `${DAO_ENDPOINT}?${new URLSearchParams({ barcode: normalizeDaoNumber(raw).number, lang: 'EN' })}`;
|
|
15
|
+
}
|
|
16
|
+
export class DaoTracker {
|
|
17
|
+
options;
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.options = options;
|
|
20
|
+
}
|
|
21
|
+
async fetch(raw, context = {}) {
|
|
22
|
+
const url = daoTrackingUrl(raw);
|
|
23
|
+
const read = async (signal, timeoutMs) => {
|
|
24
|
+
let bytes;
|
|
25
|
+
let status;
|
|
26
|
+
try {
|
|
27
|
+
// The page sends its WordPress nonce, which the service does not need
|
|
28
|
+
// without a session cookie.
|
|
29
|
+
({ bytes, response: { status } } = await fetchBounded(url, { signal,
|
|
30
|
+
headers: { Accept: 'application/json', 'User-Agent': userAgentOf(this.options.userAgent) } }, { provider: PROVIDER, timeoutMs: Math.max(1, Math.floor(timeoutMs)), maxBytes: DAO_MAX_BYTES, fetcher: this.options.fetcher,
|
|
31
|
+
allowHttpStatuses: [400] }));
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
// An unknown number is {"found":false}; a missing route is not a missing parcel.
|
|
35
|
+
if (error instanceof UpstreamHttpError && [404, 410].includes(error.status)) {
|
|
36
|
+
throw new TransportError(PROVIDER, 'DAO tracking service is unavailable', { cause: error });
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
let payload;
|
|
41
|
+
try {
|
|
42
|
+
payload = parseJsonBytes(bytes, PROVIDER);
|
|
43
|
+
}
|
|
44
|
+
catch (cause) {
|
|
45
|
+
if (BLOCKED.test(decodeText(bytes.subarray(0, 64_000))))
|
|
46
|
+
throw new ChallengeError(PROVIDER, 'DAO answered with a block page', { cause });
|
|
47
|
+
throw new SchemaError(PROVIDER, 'DAO returned invalid tracking JSON', { cause });
|
|
48
|
+
}
|
|
49
|
+
// The service names a request it cannot read, such as one without a number.
|
|
50
|
+
if (status === 400)
|
|
51
|
+
throw new IndeterminateError(PROVIDER, 'DAO rejected the tracking request');
|
|
52
|
+
return parseDao(payload, raw);
|
|
53
|
+
};
|
|
54
|
+
// A request that fails to reach DAO, or hangs, gets one more try: the
|
|
55
|
+
// first gets half of the budget, so the retry fits in the rest.
|
|
56
|
+
return runSteps({ carrier: 'dao', budgetMs: context.budgetMs ?? 15_000, signal: context.signal,
|
|
57
|
+
recorder: this.options.recorder ?? NOOP_RECORDER }, [
|
|
58
|
+
{ id: 'direct', run: ({ signal, remainingMs }) => read(signal, firstAttemptMs(remainingMs)) },
|
|
59
|
+
networkRetryStep(({ signal, remainingMs }) => read(signal, remainingMs)),
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export const adapter = (environment) => {
|
|
64
|
+
const tracker = new DaoTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
|
|
65
|
+
return {
|
|
66
|
+
id: 'dao', recordsSteps: true, steps: ['direct', 'retry'],
|
|
67
|
+
track: (input, context) => tracker.fetch(input.number, context),
|
|
68
|
+
recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizeDaoNumber(number))),
|
|
69
|
+
};
|
|
70
|
+
};
|
|
@@ -18,18 +18,68 @@
|
|
|
18
18
|
"timezone": "Europe/Copenhagen",
|
|
19
19
|
"tracking": {
|
|
20
20
|
"mode": "automatic",
|
|
21
|
-
"adapter": "
|
|
22
|
-
"steps": [
|
|
21
|
+
"adapter": "dao",
|
|
22
|
+
"steps": [
|
|
23
|
+
"direct",
|
|
24
|
+
"retry"
|
|
25
|
+
],
|
|
26
|
+
"recognition": {
|
|
27
|
+
"rank": 850
|
|
28
|
+
},
|
|
29
|
+
"localClocks": true
|
|
23
30
|
},
|
|
24
|
-
"capabilities": [
|
|
31
|
+
"capabilities": [
|
|
32
|
+
"history",
|
|
33
|
+
"location",
|
|
34
|
+
"provider_code",
|
|
35
|
+
"pickup_point",
|
|
36
|
+
"delivery_partner",
|
|
37
|
+
"delivery_tracking_number"
|
|
38
|
+
],
|
|
25
39
|
"portal": {
|
|
26
40
|
"url": "https://dao.as/find-din-pakke/#q={trackingNumber}",
|
|
27
41
|
"searchUrl": "https://dao.as/find-din-pakke/",
|
|
28
42
|
"canaryUrl": "https://dao.as/find-din-pakke/",
|
|
29
|
-
"shows": [
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
"shows": [
|
|
44
|
+
"status",
|
|
45
|
+
"history",
|
|
46
|
+
"location",
|
|
47
|
+
"sender_name",
|
|
48
|
+
"recipient_name",
|
|
49
|
+
"recipient_address",
|
|
50
|
+
"postcodes",
|
|
51
|
+
"pickup_point",
|
|
52
|
+
"pickup_point_address",
|
|
53
|
+
"office_opening_hours",
|
|
54
|
+
"coordinates",
|
|
55
|
+
"delivery_partner",
|
|
56
|
+
"delivery_tracking_number"
|
|
57
|
+
],
|
|
58
|
+
"scraped": [
|
|
59
|
+
"status",
|
|
60
|
+
"history",
|
|
61
|
+
"location",
|
|
62
|
+
"provider_code",
|
|
63
|
+
"pickup_point",
|
|
64
|
+
"delivery_partner",
|
|
65
|
+
"delivery_tracking_number"
|
|
66
|
+
],
|
|
67
|
+
"discarded": [
|
|
68
|
+
"sender_name",
|
|
69
|
+
"recipient_name",
|
|
70
|
+
"recipient_address",
|
|
71
|
+
"postcodes",
|
|
72
|
+
"pickup_point_address",
|
|
73
|
+
"office_opening_hours",
|
|
74
|
+
"coordinates"
|
|
75
|
+
],
|
|
76
|
+
"unavailable": [
|
|
77
|
+
"eta",
|
|
78
|
+
"weight",
|
|
79
|
+
"dimensions",
|
|
80
|
+
"service_name",
|
|
81
|
+
"offset_timestamps"
|
|
82
|
+
]
|
|
33
83
|
},
|
|
34
84
|
"links": [
|
|
35
85
|
{
|
|
@@ -53,5 +103,13 @@
|
|
|
53
103
|
]
|
|
54
104
|
}
|
|
55
105
|
],
|
|
56
|
-
"detection": [
|
|
106
|
+
"detection": [
|
|
107
|
+
{
|
|
108
|
+
"id": "dao-1",
|
|
109
|
+
"pattern": "^000(?:5712696|5715127)\\d{10}$",
|
|
110
|
+
"confidence": "low",
|
|
111
|
+
"preferred": true,
|
|
112
|
+
"checksum": "sscc"
|
|
113
|
+
}
|
|
114
|
+
]
|
|
57
115
|
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"confidence": "low",
|
|
20
20
|
"candidates": [
|
|
21
21
|
"postnord",
|
|
22
|
+
"dao",
|
|
22
23
|
"swiss-post-cargo",
|
|
23
24
|
"usps",
|
|
24
25
|
"nz-post"
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
"confidence": "low",
|
|
45
46
|
"candidates": [
|
|
46
47
|
"postnord",
|
|
48
|
+
"dao",
|
|
47
49
|
"swiss-post-cargo",
|
|
48
50
|
"usps",
|
|
49
51
|
"nz-post"
|
|
@@ -62,13 +64,14 @@
|
|
|
62
64
|
"access": "indexed_excerpt"
|
|
63
65
|
},
|
|
64
66
|
"context": {
|
|
65
|
-
"assessment": "
|
|
67
|
+
"assessment": "confirmed"
|
|
66
68
|
},
|
|
67
69
|
"expect": {
|
|
68
70
|
"carrier": "unknown",
|
|
69
71
|
"confidence": "low",
|
|
70
72
|
"candidates": [
|
|
71
73
|
"postnord",
|
|
74
|
+
"dao",
|
|
72
75
|
"swiss-post-cargo",
|
|
73
76
|
"usps",
|
|
74
77
|
"nz-post"
|
|
@@ -94,6 +97,7 @@
|
|
|
94
97
|
"confidence": "low",
|
|
95
98
|
"candidates": [
|
|
96
99
|
"postnord",
|
|
100
|
+
"dao",
|
|
97
101
|
"swiss-post-cargo",
|
|
98
102
|
"usps",
|
|
99
103
|
"nz-post"
|
|
@@ -112,13 +116,14 @@
|
|
|
112
116
|
"access": "indexed_excerpt"
|
|
113
117
|
},
|
|
114
118
|
"context": {
|
|
115
|
-
"assessment": "
|
|
119
|
+
"assessment": "confirmed"
|
|
116
120
|
},
|
|
117
121
|
"expect": {
|
|
118
122
|
"carrier": "unknown",
|
|
119
123
|
"confidence": "low",
|
|
120
124
|
"candidates": [
|
|
121
125
|
"postnord",
|
|
126
|
+
"dao",
|
|
122
127
|
"swiss-post-cargo",
|
|
123
128
|
"usps",
|
|
124
129
|
"nz-post"
|
|
@@ -137,13 +142,14 @@
|
|
|
137
142
|
"access": "indexed_excerpt"
|
|
138
143
|
},
|
|
139
144
|
"context": {
|
|
140
|
-
"assessment": "
|
|
145
|
+
"assessment": "confirmed"
|
|
141
146
|
},
|
|
142
147
|
"expect": {
|
|
143
148
|
"carrier": "unknown",
|
|
144
149
|
"confidence": "low",
|
|
145
150
|
"candidates": [
|
|
146
151
|
"postnord",
|
|
152
|
+
"dao",
|
|
147
153
|
"swiss-post-cargo",
|
|
148
154
|
"usps",
|
|
149
155
|
"nz-post"
|
|
@@ -162,13 +168,14 @@
|
|
|
162
168
|
"access": "indexed_excerpt"
|
|
163
169
|
},
|
|
164
170
|
"context": {
|
|
165
|
-
"assessment": "
|
|
171
|
+
"assessment": "confirmed"
|
|
166
172
|
},
|
|
167
173
|
"expect": {
|
|
168
174
|
"carrier": "unknown",
|
|
169
175
|
"confidence": "low",
|
|
170
176
|
"candidates": [
|
|
171
177
|
"postnord",
|
|
178
|
+
"dao",
|
|
172
179
|
"swiss-post-cargo",
|
|
173
180
|
"usps",
|
|
174
181
|
"nz-post"
|
|
@@ -187,13 +194,14 @@
|
|
|
187
194
|
"access": "indexed_excerpt"
|
|
188
195
|
},
|
|
189
196
|
"context": {
|
|
190
|
-
"assessment": "
|
|
197
|
+
"assessment": "confirmed"
|
|
191
198
|
},
|
|
192
199
|
"expect": {
|
|
193
200
|
"carrier": "unknown",
|
|
194
201
|
"confidence": "low",
|
|
195
202
|
"candidates": [
|
|
196
203
|
"postnord",
|
|
204
|
+
"dao",
|
|
197
205
|
"swiss-post-cargo",
|
|
198
206
|
"usps",
|
|
199
207
|
"nz-post"
|
|
@@ -212,13 +220,14 @@
|
|
|
212
220
|
"access": "indexed_excerpt"
|
|
213
221
|
},
|
|
214
222
|
"context": {
|
|
215
|
-
"assessment": "
|
|
223
|
+
"assessment": "confirmed"
|
|
216
224
|
},
|
|
217
225
|
"expect": {
|
|
218
226
|
"carrier": "unknown",
|
|
219
227
|
"confidence": "low",
|
|
220
228
|
"candidates": [
|
|
221
229
|
"postnord",
|
|
230
|
+
"dao",
|
|
222
231
|
"swiss-post-cargo",
|
|
223
232
|
"usps",
|
|
224
233
|
"nz-post"
|
|
@@ -237,13 +246,14 @@
|
|
|
237
246
|
"access": "indexed_excerpt"
|
|
238
247
|
},
|
|
239
248
|
"context": {
|
|
240
|
-
"assessment": "
|
|
249
|
+
"assessment": "confirmed"
|
|
241
250
|
},
|
|
242
251
|
"expect": {
|
|
243
252
|
"carrier": "unknown",
|
|
244
253
|
"confidence": "low",
|
|
245
254
|
"candidates": [
|
|
246
255
|
"postnord",
|
|
256
|
+
"dao",
|
|
247
257
|
"swiss-post-cargo",
|
|
248
258
|
"usps",
|
|
249
259
|
"nz-post"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CarrierResult } from '../../core/result/index.js';
|
|
2
|
+
export declare const PROVIDER = "DAO";
|
|
3
|
+
export declare const DAO_MAX_BYTES = 256000;
|
|
4
|
+
/** A number as DAO's tracker takes it. */
|
|
5
|
+
export interface DaoNumber {
|
|
6
|
+
readonly number: string;
|
|
7
|
+
/**
|
|
8
|
+
* A nine-digit e-label code or a thirteen-digit parcel number, which the
|
|
9
|
+
* tracker may answer with the parcel's SSCC.
|
|
10
|
+
*/
|
|
11
|
+
readonly alias: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* DAO's tracker takes its twenty-digit SSCCs, thirteen-digit parcel numbers
|
|
15
|
+
* starting with 7 and the nine-digit e-label code a sender writes on a parcel
|
|
16
|
+
* without a label. An SSCC must pass its check digit.
|
|
17
|
+
*/
|
|
18
|
+
export declare function normalizeDaoNumber(raw: string): DaoNumber;
|
|
19
|
+
/**
|
|
20
|
+
* The reply of DAO's tracking page service for one number: `{"found":false}`
|
|
21
|
+
* for a number it does not know, else the parcel with its number, its scans
|
|
22
|
+
* newest first, the pickup point and, for a parcel sent abroad, the partner's
|
|
23
|
+
* number and tracking link. The masked sender and recipient, the recipient's
|
|
24
|
+
* postcode and the pickup point's address, opening hours and coordinates are
|
|
25
|
+
* never read.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseDao(payload: unknown, raw: string): CarrierResult;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { carrierIdFromPartner } from '../../core/catalog/hints.js';
|
|
2
|
+
import { isValidSscc, normalizeTrackingNumber } from '../../core/detection/index.js';
|
|
3
|
+
import { IndeterminateError, InvalidInputError, NoHistoryError, NotFoundError, SchemaError } from '../../core/errors/index.js';
|
|
4
|
+
import { languageStageStatus } from '../../core/status/index.js';
|
|
5
|
+
import { calendarDay } from '../../core/time/index.js';
|
|
6
|
+
import { clean, cleanScalar } from '../../core/transport/index.js';
|
|
7
|
+
import { isRecord } from '../../core/types.js';
|
|
8
|
+
import { CODE, daoCode, daoStage } from './status.js';
|
|
9
|
+
export const PROVIDER = 'DAO';
|
|
10
|
+
// A parcel's reply is a few kilobytes.
|
|
11
|
+
export const DAO_MAX_BYTES = 256_000;
|
|
12
|
+
const MAX_ROWS = 300;
|
|
13
|
+
const MAX_EVENTS = 200;
|
|
14
|
+
// The backend's clock, a wall time without an offset: "2026-10-06 23:24:15".
|
|
15
|
+
const CLOCK = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/;
|
|
16
|
+
const LEG = /^[A-Za-z]{1,20}$/;
|
|
17
|
+
const EVENT = /^\d{1,4}$/;
|
|
18
|
+
/**
|
|
19
|
+
* DAO's tracker takes its twenty-digit SSCCs, thirteen-digit parcel numbers
|
|
20
|
+
* starting with 7 and the nine-digit e-label code a sender writes on a parcel
|
|
21
|
+
* without a label. An SSCC must pass its check digit.
|
|
22
|
+
*/
|
|
23
|
+
export function normalizeDaoNumber(raw) {
|
|
24
|
+
const number = normalizeTrackingNumber(raw);
|
|
25
|
+
if (/^00\d{18}$/.test(number) && isValidSscc(number))
|
|
26
|
+
return { number, alias: false };
|
|
27
|
+
if (/^(?:7\d{12}|\d{9})$/.test(number))
|
|
28
|
+
return { number, alias: true };
|
|
29
|
+
throw new InvalidInputError(PROVIDER, 'DAO tracks twenty-digit SSCCs, thirteen-digit numbers starting with 7 and nine-digit e-label codes');
|
|
30
|
+
}
|
|
31
|
+
/** The parcel number the reply names, in the forms DAO issues. */
|
|
32
|
+
function issued(value) {
|
|
33
|
+
const barcode = normalizeTrackingNumber(cleanScalar(value, 40));
|
|
34
|
+
return /^(?:00\d{18}|7\d{12}|\d{9})$/.test(barcode) ? barcode : '';
|
|
35
|
+
}
|
|
36
|
+
/** A clock as a wall time, validated. The reply gives no offset. */
|
|
37
|
+
function wallClock(value) {
|
|
38
|
+
const match = CLOCK.exec(clean(value, 32));
|
|
39
|
+
if (!match)
|
|
40
|
+
return null;
|
|
41
|
+
const [, year, month, day, hour, minute, second] = match;
|
|
42
|
+
if (!calendarDay(Number(year), Number(month), Number(day)) || Number(hour) > 23 || Number(minute) > 59 || Number(second) > 59)
|
|
43
|
+
return null;
|
|
44
|
+
return `${year}-${month}-${day}T${hour}:${minute}:${second}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One row of the parcel's history, from the `raw` record the page's service
|
|
48
|
+
* passes through from DAO's backend: its clock, leg, event number, wording and
|
|
49
|
+
* place. A home delivery's scan has no wording of its own: the delivered text
|
|
50
|
+
* is in its place field.
|
|
51
|
+
*/
|
|
52
|
+
function scanOf(row, index) {
|
|
53
|
+
const raw = isRecord(row) ? row.raw : undefined;
|
|
54
|
+
if (!isRecord(raw))
|
|
55
|
+
throw new SchemaError(PROVIDER, 'DAO returned a scan without its record');
|
|
56
|
+
const local = wallClock(raw.tidspunkt);
|
|
57
|
+
const leg = clean(raw.pakketype, 24);
|
|
58
|
+
const event = cleanScalar(raw.haendelse, 8);
|
|
59
|
+
if (!local || !EVENT.test(event) || (leg && !LEG.test(leg)))
|
|
60
|
+
throw new SchemaError(PROVIDER, 'DAO returned a scan without its clock or event');
|
|
61
|
+
const wording = clean(raw.beskrivelse, 300);
|
|
62
|
+
const place = clean(raw.sted, 120);
|
|
63
|
+
const description = wording || place;
|
|
64
|
+
if (!description)
|
|
65
|
+
throw new SchemaError(PROVIDER, 'DAO returned a scan without wording');
|
|
66
|
+
const code = daoCode(leg, event);
|
|
67
|
+
if (!CODE.test(code))
|
|
68
|
+
throw new SchemaError(PROVIDER, 'DAO returned an invalid event code');
|
|
69
|
+
const stage = daoStage(code);
|
|
70
|
+
return {
|
|
71
|
+
local,
|
|
72
|
+
index,
|
|
73
|
+
event: { local_time: local, description, ...(wording && place ? { location: place } : {}), provider_code: code,
|
|
74
|
+
...(stage ? { stage, stage_source: 'carrier_map' } : {}) },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** The partner and number a parcel sent abroad is tracked under, as the page links them. */
|
|
78
|
+
function abroad(parcel, number) {
|
|
79
|
+
const partnerNumber = normalizeTrackingNumber(clean(parcel.dist_barcode, 64));
|
|
80
|
+
if (!/^[A-Z0-9]{4,40}$/.test(partnerNumber) || partnerNumber === number)
|
|
81
|
+
return {};
|
|
82
|
+
const partner = carrierIdFromPartner('', clean(parcel.external_tracking, 500));
|
|
83
|
+
return { delivery_tracking_number: partnerNumber, ...(partner && partner !== 'dao' ? { delivery_carrier: partner } : {}) };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The reply of DAO's tracking page service for one number: `{"found":false}`
|
|
87
|
+
* for a number it does not know, else the parcel with its number, its scans
|
|
88
|
+
* newest first, the pickup point and, for a parcel sent abroad, the partner's
|
|
89
|
+
* number and tracking link. The masked sender and recipient, the recipient's
|
|
90
|
+
* postcode and the pickup point's address, opening hours and coordinates are
|
|
91
|
+
* never read.
|
|
92
|
+
*/
|
|
93
|
+
export function parseDao(payload, raw) {
|
|
94
|
+
const { number, alias } = normalizeDaoNumber(raw);
|
|
95
|
+
if (!isRecord(payload) || typeof payload.found !== 'boolean')
|
|
96
|
+
throw new SchemaError(PROVIDER, 'DAO returned an unexpected reply');
|
|
97
|
+
if (!payload.found)
|
|
98
|
+
throw new NotFoundError(PROVIDER);
|
|
99
|
+
// The page shows its not-found notice for this too, but no reply showed one.
|
|
100
|
+
if (!isRecord(payload.parcel))
|
|
101
|
+
throw new IndeterminateError(PROVIDER, 'DAO returned no parcel for a number it knows');
|
|
102
|
+
const parcel = payload.parcel;
|
|
103
|
+
const barcode = issued(parcel.barcode);
|
|
104
|
+
// An e-label code or a thirteen-digit number may be answered with the parcel's SSCC.
|
|
105
|
+
if (barcode !== number && !(alias && barcode))
|
|
106
|
+
throw new SchemaError(PROVIDER, 'DAO answered for a different parcel');
|
|
107
|
+
const rows = parcel.events ?? [];
|
|
108
|
+
if (!Array.isArray(rows) || rows.length > MAX_ROWS)
|
|
109
|
+
throw new SchemaError(PROVIDER, 'DAO returned an invalid parcel history');
|
|
110
|
+
const seen = new Set();
|
|
111
|
+
const scans = rows.map(scanOf).filter(({ event }) => {
|
|
112
|
+
// A consignment's parcels can repeat one scan.
|
|
113
|
+
const identity = JSON.stringify(event);
|
|
114
|
+
if (seen.has(identity))
|
|
115
|
+
return false;
|
|
116
|
+
seen.add(identity);
|
|
117
|
+
return true;
|
|
118
|
+
});
|
|
119
|
+
if (!scans.length)
|
|
120
|
+
throw new NoHistoryError(PROVIDER);
|
|
121
|
+
// Newest first; equal clocks keep the reply's order, which is newest first too.
|
|
122
|
+
scans.sort((left, right) => (left.local < right.local ? 1 : left.local > right.local ? -1 : left.index - right.index));
|
|
123
|
+
const events = scans.map(({ event }) => event);
|
|
124
|
+
const newest = events[0];
|
|
125
|
+
const stage = newest.stage;
|
|
126
|
+
// The daoSHOP the parcel goes to or waits at, by its name alone.
|
|
127
|
+
const point = isRecord(parcel.pickup) ? clean(parcel.pickup.name, 160) : '';
|
|
128
|
+
return {
|
|
129
|
+
status: stage ? languageStageStatus(stage) : 'unknown',
|
|
130
|
+
...(stage ? { current_stage: stage, current_stage_source: 'carrier_map' } : {}),
|
|
131
|
+
last_status_text: newest.description,
|
|
132
|
+
last_update: null,
|
|
133
|
+
last_update_local: newest.local_time,
|
|
134
|
+
expected_delivery: null,
|
|
135
|
+
...(barcode !== number ? { canonical_tracking_number: barcode } : {}),
|
|
136
|
+
...(point ? { pickup_point: point } : {}),
|
|
137
|
+
...abroad(parcel, barcode),
|
|
138
|
+
events: events.slice(0, MAX_EVENTS),
|
|
139
|
+
...(events.length > MAX_EVENTS ? { history_truncated: true } : {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Stage } from '../../core/status/index.js';
|
|
2
|
+
import type { CarrierStatusMap } from '../../core/status/statusMap.js';
|
|
3
|
+
/** A leg and an event number: `Home/11`. */
|
|
4
|
+
export declare const CODE: RegExp;
|
|
5
|
+
/** The stage a scan's leg and event number give a parcel, if the map knows them. */
|
|
6
|
+
export declare function daoStage(code: string): Stage | undefined;
|
|
7
|
+
/** The scan's provider code: its leg and event number, or the number alone without a leg. */
|
|
8
|
+
export declare function daoCode(leg: string, event: string): string;
|
|
9
|
+
/** The map reads the whole code: the same event number means different things on each leg. */
|
|
10
|
+
export declare const statusMap: CarrierStatusMap;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DAO numbers its scan events separately for each leg, home delivery (`Home`)
|
|
3
|
+
* and daoSHOP (`Shop`), and the same number means different things on the
|
|
4
|
+
* two: Home 11 delivers a parcel to the door, Shop 11 brings it into a
|
|
5
|
+
* terminal. A scan's provider code is its leg, a slash and its event number,
|
|
6
|
+
* as `Shop/32`. A parcel whose home delivery fails is sent to a daoSHOP, and
|
|
7
|
+
* its later scans are on the Shop leg.
|
|
8
|
+
*/
|
|
9
|
+
const STAGES = new Map([
|
|
10
|
+
// The sender's data, before DAO has the parcel.
|
|
11
|
+
['Home/1', 'registered'],
|
|
12
|
+
['Shop/1', 'registered'],
|
|
13
|
+
// Handed in at a daoSHOP by the sender.
|
|
14
|
+
['Shop/25', 'accepted'],
|
|
15
|
+
['Home/10', 'in_transit'],
|
|
16
|
+
['Home/35', 'in_transit'],
|
|
17
|
+
['Shop/11', 'in_transit'],
|
|
18
|
+
['Shop/12', 'in_transit'],
|
|
19
|
+
['Shop/16', 'in_transit'],
|
|
20
|
+
['Shop/23', 'in_transit'],
|
|
21
|
+
['Shop/26', 'in_transit'],
|
|
22
|
+
// The parcel is on the day's delivery round.
|
|
23
|
+
['Home/31', 'out_for_delivery'],
|
|
24
|
+
['Home/61', 'failed_attempt'],
|
|
25
|
+
// A failed home delivery: the parcel goes on to a daoSHOP.
|
|
26
|
+
['Home/64', 'failed_attempt'],
|
|
27
|
+
['Shop/32', 'ready_for_pickup'],
|
|
28
|
+
['Home/11', 'delivered'],
|
|
29
|
+
// Collected at the daoSHOP.
|
|
30
|
+
['Shop/51', 'delivered'],
|
|
31
|
+
]);
|
|
32
|
+
/** A leg and an event number: `Home/11`. */
|
|
33
|
+
export const CODE = /^(?:[A-Za-z]{1,20}\/)?\d{1,4}$/;
|
|
34
|
+
/** The stage a scan's leg and event number give a parcel, if the map knows them. */
|
|
35
|
+
export function daoStage(code) {
|
|
36
|
+
return STAGES.get(code);
|
|
37
|
+
}
|
|
38
|
+
/** The scan's provider code: its leg and event number, or the number alone without a leg. */
|
|
39
|
+
export function daoCode(leg, event) {
|
|
40
|
+
return leg ? `${leg}/${event}` : event;
|
|
41
|
+
}
|
|
42
|
+
/** The map reads the whole code: the same event number means different things on each leg. */
|
|
43
|
+
export const statusMap = {
|
|
44
|
+
stage: (code) => (code ? STAGES.get(code) : undefined),
|
|
45
|
+
gaps: [],
|
|
46
|
+
};
|
|
@@ -1,4 +1,171 @@
|
|
|
1
1
|
{
|
|
2
2
|
"carrier": "dao",
|
|
3
|
-
"entries": [
|
|
3
|
+
"entries": [
|
|
4
|
+
{
|
|
5
|
+
"code": "Home/1",
|
|
6
|
+
"wording": "Shipment created, awaiting registration",
|
|
7
|
+
"language": "en",
|
|
8
|
+
"stage": "registered",
|
|
9
|
+
"confirmedBy": "live",
|
|
10
|
+
"firstSeen": "2026-10-12",
|
|
11
|
+
"note": "The sender's data, before DAO has the parcel."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"code": "Home/1",
|
|
15
|
+
"wording": "Forsendelse er oprettet, afventer registrering",
|
|
16
|
+
"language": "da",
|
|
17
|
+
"stage": "registered",
|
|
18
|
+
"confirmedBy": "live",
|
|
19
|
+
"firstSeen": "2026-10-12"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"code": "Shop/1",
|
|
23
|
+
"wording": "Shipment created, awaiting registration",
|
|
24
|
+
"language": "en",
|
|
25
|
+
"stage": "registered",
|
|
26
|
+
"confirmedBy": "live",
|
|
27
|
+
"firstSeen": "2026-10-12",
|
|
28
|
+
"note": "The same event on a parcel the sender hands in at a daoSHOP."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"code": "Shop/25",
|
|
32
|
+
"wording": "Handed in at daoSHOP",
|
|
33
|
+
"language": "en",
|
|
34
|
+
"stage": "accepted",
|
|
35
|
+
"confirmedBy": "live",
|
|
36
|
+
"firstSeen": "2026-10-12",
|
|
37
|
+
"note": "The sender handed the parcel in at a daoSHOP."
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"code": "Shop/26",
|
|
41
|
+
"wording": "On the way to terminal",
|
|
42
|
+
"language": "en",
|
|
43
|
+
"stage": "in_transit",
|
|
44
|
+
"confirmedBy": "live",
|
|
45
|
+
"firstSeen": "2026-10-12"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"code": "Shop/11",
|
|
49
|
+
"wording": "Parcel has arrived at terminal",
|
|
50
|
+
"language": "en",
|
|
51
|
+
"stage": "in_transit",
|
|
52
|
+
"confirmedBy": "live",
|
|
53
|
+
"firstSeen": "2026-10-12",
|
|
54
|
+
"note": "On the home leg the same event number is the delivery."
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"code": "Shop/12",
|
|
58
|
+
"wording": "Sorted at: Erritsø terminal",
|
|
59
|
+
"language": "en",
|
|
60
|
+
"stage": "in_transit",
|
|
61
|
+
"confirmedBy": "live",
|
|
62
|
+
"firstSeen": "2026-10-12",
|
|
63
|
+
"note": "The wording names the terminal."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"code": "Shop/16",
|
|
67
|
+
"wording": "Sorted at: Sjælland terminal",
|
|
68
|
+
"language": "en",
|
|
69
|
+
"stage": "in_transit",
|
|
70
|
+
"confirmedBy": "live",
|
|
71
|
+
"firstSeen": "2026-10-12",
|
|
72
|
+
"note": "The wording names the terminal."
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"code": "Shop/23",
|
|
76
|
+
"wording": "On the way to daoSHOP",
|
|
77
|
+
"language": "en",
|
|
78
|
+
"stage": "in_transit",
|
|
79
|
+
"confirmedBy": "live",
|
|
80
|
+
"firstSeen": "2026-10-12"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"code": "Shop/32",
|
|
84
|
+
"wording": "Ready for pickup",
|
|
85
|
+
"language": "en",
|
|
86
|
+
"stage": "ready_for_pickup",
|
|
87
|
+
"confirmedBy": "live",
|
|
88
|
+
"firstSeen": "2026-10-12"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"code": "Shop/51",
|
|
92
|
+
"wording": "Parcel delivered",
|
|
93
|
+
"language": "en",
|
|
94
|
+
"stage": "delivered",
|
|
95
|
+
"confirmedBy": "live",
|
|
96
|
+
"firstSeen": "2026-10-12",
|
|
97
|
+
"note": "Collected at the daoSHOP."
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"code": "Home/10",
|
|
101
|
+
"wording": "Parcel arrived at dao depot",
|
|
102
|
+
"language": "en",
|
|
103
|
+
"stage": "in_transit",
|
|
104
|
+
"confirmedBy": "live",
|
|
105
|
+
"firstSeen": "2026-10-12",
|
|
106
|
+
"note": "The depot comes in the place field."
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"code": "Home/10",
|
|
110
|
+
"wording": "Pakke ankommet til dao depot",
|
|
111
|
+
"language": "da",
|
|
112
|
+
"stage": "in_transit",
|
|
113
|
+
"confirmedBy": "portal",
|
|
114
|
+
"firstSeen": "2026-10-12",
|
|
115
|
+
"note": "Seen in archived replies of the tracking service."
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"code": "Home/35",
|
|
119
|
+
"wording": "Pakke på vej til leveringsterminalen",
|
|
120
|
+
"language": "da",
|
|
121
|
+
"stage": "in_transit",
|
|
122
|
+
"confirmedBy": "portal",
|
|
123
|
+
"firstSeen": "2026-10-12",
|
|
124
|
+
"note": "On the way to the delivery terminal. Seen in archived replies of the tracking service."
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"code": "Home/31",
|
|
128
|
+
"wording": "To be delivered today",
|
|
129
|
+
"language": "en",
|
|
130
|
+
"stage": "out_for_delivery",
|
|
131
|
+
"confirmedBy": "live",
|
|
132
|
+
"firstSeen": "2026-10-12",
|
|
133
|
+
"note": "The parcel is planned for the day's delivery round."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"code": "Home/61",
|
|
137
|
+
"wording": "Not delivered - other reason",
|
|
138
|
+
"language": "en",
|
|
139
|
+
"stage": "failed_attempt",
|
|
140
|
+
"confirmedBy": "live",
|
|
141
|
+
"firstSeen": "2026-10-12"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"code": "Home/64",
|
|
145
|
+
"wording": "Delivery not possible. Redirected to daoSHOP",
|
|
146
|
+
"language": "en",
|
|
147
|
+
"stage": "failed_attempt",
|
|
148
|
+
"confirmedBy": "live",
|
|
149
|
+
"firstSeen": "2026-10-12",
|
|
150
|
+
"note": "Follows a failed delivery at the same second; the parcel goes on to a daoSHOP, on the Shop leg."
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"code": "Home/11",
|
|
154
|
+
"wording": "Parcel delivered",
|
|
155
|
+
"language": "en",
|
|
156
|
+
"stage": "delivered",
|
|
157
|
+
"confirmedBy": "live",
|
|
158
|
+
"firstSeen": "2026-10-12",
|
|
159
|
+
"note": "A home delivery has no wording of its own: the delivered text comes in the place field."
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"code": "Home/11",
|
|
163
|
+
"wording": "Pakke leveret",
|
|
164
|
+
"language": "da",
|
|
165
|
+
"stage": "delivered",
|
|
166
|
+
"confirmedBy": "portal",
|
|
167
|
+
"firstSeen": "2026-10-12",
|
|
168
|
+
"note": "Seen in archived replies of the tracking service."
|
|
169
|
+
}
|
|
170
|
+
]
|
|
4
171
|
}
|
|
@@ -22,6 +22,7 @@ import { statusMap as ceskaPosta } from '../../carriers/ceska-posta/status.js';
|
|
|
22
22
|
import { statusMap as chinaPost } from '../../carriers/china-post/status.js';
|
|
23
23
|
import { statusMap as chronopost } from '../../carriers/chronopost/status.js';
|
|
24
24
|
import { statusMap as correosSpain } from '../../carriers/correos-spain/status.js';
|
|
25
|
+
import { statusMap as dao } from '../../carriers/dao/status.js';
|
|
25
26
|
import { statusMap as dhlExpress } from '../../carriers/dhl-express/status.js';
|
|
26
27
|
import { statusMap as dpd } from '../../carriers/dpd/status.js';
|
|
27
28
|
import { statusMap as dpdDe } from '../../carriers/dpd-de/status.js';
|
|
@@ -76,7 +77,7 @@ import { CARRIER_DEFINITIONS } from './definitions.js';
|
|
|
76
77
|
/** By adapter: a carrier served by another's adapter, as Delivengo by La Poste's, reads that map. */
|
|
77
78
|
export const STATUS_MAPS = {
|
|
78
79
|
'acs-courier': acsCourier, aliexpress, 'allegro-one': allegroOne, 'an-post': anPost, 'aras-kargo': arasKargo, 'box-now': boxNow, cacesa, cargus,
|
|
79
|
-
'ceska-posta': ceskaPosta, 'china-post': chinaPost, chronopost, 'correos-spain': correosSpain, 'dhl-express': dhlExpress, dpd, 'dpd-de': dpdDe,
|
|
80
|
+
'ceska-posta': ceskaPosta, 'china-post': chinaPost, chronopost, 'correos-spain': correosSpain, dao, 'dhl-express': dhlExpress, dpd, 'dpd-de': dpdDe,
|
|
80
81
|
'dpd-fr': dpdFr, 'dpd-ie': dpdIe, 'dpd-pl': dpdPl, econt, elta, emile, 'express-one': expressOne, 'fan-courier': fanCourier, foxpost,
|
|
81
82
|
'geniki-taxydromiki': geniki, 'gls-es': glsEs, 'gls-it': glsIt, 'gofo-fr': gofoFr, 'gofo-it': gofoIt, helthjem, 'hrvatska-posta': hrvatskaPosta,
|
|
82
83
|
'j-and-t': jAndT, 'j-and-t-cargo': jntCargo, 'la-poste': laPoste, 'latvijas-pasts': latvijasPasts, matkahuolto, 'mng-kargo': mngKargo,
|
package/dist/data/catalog.json
CHANGED
|
@@ -5249,7 +5249,9 @@
|
|
|
5249
5249
|
"timezone": "Europe/Copenhagen",
|
|
5250
5250
|
"tracking": {
|
|
5251
5251
|
"mode": "automatic",
|
|
5252
|
-
"adapter": "
|
|
5252
|
+
"adapter": "dao",
|
|
5253
|
+
"recognitionRank": 850,
|
|
5254
|
+
"localClocks": true
|
|
5253
5255
|
},
|
|
5254
5256
|
"canaryUrl": "https://dao.as/find-din-pakke/",
|
|
5255
5257
|
"trackingUrlTemplate": "https://dao.as/find-din-pakke/#q={trackingNumber}",
|
|
@@ -5276,7 +5278,14 @@
|
|
|
5276
5278
|
]
|
|
5277
5279
|
}
|
|
5278
5280
|
],
|
|
5279
|
-
"detectionRules": [
|
|
5281
|
+
"detectionRules": [
|
|
5282
|
+
{
|
|
5283
|
+
"pattern": "^000(?:5712696|5715127)\\d{10}$",
|
|
5284
|
+
"confidence": "low",
|
|
5285
|
+
"checksum": "sscc",
|
|
5286
|
+
"preferred": true
|
|
5287
|
+
}
|
|
5288
|
+
]
|
|
5280
5289
|
},
|
|
5281
5290
|
"day-ross": {
|
|
5282
5291
|
"displayName": "Day & Ross",
|
|
@@ -3588,7 +3588,9 @@ export declare const CARRIER_CATALOG: {
|
|
|
3588
3588
|
readonly timezone: "Europe/Copenhagen";
|
|
3589
3589
|
readonly tracking: {
|
|
3590
3590
|
readonly mode: "automatic";
|
|
3591
|
-
readonly adapter: "
|
|
3591
|
+
readonly adapter: "dao";
|
|
3592
|
+
readonly recognitionRank: 850;
|
|
3593
|
+
readonly localClocks: true;
|
|
3592
3594
|
};
|
|
3593
3595
|
readonly canaryUrl: "https://dao.as/find-din-pakke/";
|
|
3594
3596
|
readonly trackingUrlTemplate: "https://dao.as/find-din-pakke/#q={trackingNumber}";
|
|
@@ -3603,7 +3605,12 @@ export declare const CARRIER_CATALOG: {
|
|
|
3603
3605
|
readonly pathPattern: "^/(?:en/private/track-your-parcel/?|dao-app/?)?$";
|
|
3604
3606
|
readonly params: readonly ["stregkode"];
|
|
3605
3607
|
}];
|
|
3606
|
-
readonly detectionRules: readonly [
|
|
3608
|
+
readonly detectionRules: readonly [{
|
|
3609
|
+
readonly pattern: "^000(?:5712696|5715127)\\d{10}$";
|
|
3610
|
+
readonly confidence: "low";
|
|
3611
|
+
readonly checksum: "sscc";
|
|
3612
|
+
readonly preferred: true;
|
|
3613
|
+
}];
|
|
3607
3614
|
};
|
|
3608
3615
|
readonly "day-ross": {
|
|
3609
3616
|
readonly displayName: "Day & Ross";
|
|
@@ -5250,7 +5250,9 @@ export const CARRIER_CATALOG = {
|
|
|
5250
5250
|
"timezone": "Europe/Copenhagen",
|
|
5251
5251
|
"tracking": {
|
|
5252
5252
|
"mode": "automatic",
|
|
5253
|
-
"adapter": "
|
|
5253
|
+
"adapter": "dao",
|
|
5254
|
+
"recognitionRank": 850,
|
|
5255
|
+
"localClocks": true
|
|
5254
5256
|
},
|
|
5255
5257
|
"canaryUrl": "https://dao.as/find-din-pakke/",
|
|
5256
5258
|
"trackingUrlTemplate": "https://dao.as/find-din-pakke/#q={trackingNumber}",
|
|
@@ -5277,7 +5279,14 @@ export const CARRIER_CATALOG = {
|
|
|
5277
5279
|
]
|
|
5278
5280
|
}
|
|
5279
5281
|
],
|
|
5280
|
-
"detectionRules": [
|
|
5282
|
+
"detectionRules": [
|
|
5283
|
+
{
|
|
5284
|
+
"pattern": "^000(?:5712696|5715127)\\d{10}$",
|
|
5285
|
+
"confidence": "low",
|
|
5286
|
+
"checksum": "sscc",
|
|
5287
|
+
"preferred": true
|
|
5288
|
+
}
|
|
5289
|
+
]
|
|
5281
5290
|
},
|
|
5282
5291
|
"day-ross": {
|
|
5283
5292
|
"displayName": "Day & Ross",
|
|
@@ -8888,7 +8897,7 @@ export const DETECTION_RULE_IDS = {
|
|
|
8888
8897
|
"cne": ["cne-1"],
|
|
8889
8898
|
"correos-de-mexico": [],
|
|
8890
8899
|
"cyprus-post": [],
|
|
8891
|
-
"dao": [],
|
|
8900
|
+
"dao": ["dao-1"],
|
|
8892
8901
|
"day-ross": ["day-ross-1"],
|
|
8893
8902
|
"dhl-ecommerce-es": ["dhl-ecommerce-es-1", "dhl-ecommerce-es-2"],
|
|
8894
8903
|
"dhl-ecommerce-nl": ["dhl-ecommerce-nl-1", "dhl-ecommerce-nl-2", "dhl-ecommerce-nl-3"],
|
|
@@ -33,6 +33,7 @@ import { adapter as correosSpain } from '../carriers/correos-spain/adapter.js';
|
|
|
33
33
|
import { adapter as ctt } from '../carriers/ctt/adapter.js';
|
|
34
34
|
import { adapter as cttExpress } from '../carriers/ctt-express/adapter.js';
|
|
35
35
|
import { adapter as dachser } from '../carriers/dachser/adapter.js';
|
|
36
|
+
import { adapter as dao } from '../carriers/dao/adapter.js';
|
|
36
37
|
import { adapter as delhivery } from '../carriers/delhivery/adapter.js';
|
|
37
38
|
import { adapter as dhl } from '../carriers/dhl/adapter.js';
|
|
38
39
|
import { adapter as dhlEcommerce } from '../carriers/dhl-ecommerce/adapter.js';
|
|
@@ -188,6 +189,7 @@ export const REGISTRY = {
|
|
|
188
189
|
"ctt": ctt,
|
|
189
190
|
"ctt-express": cttExpress,
|
|
190
191
|
"dachser": dachser,
|
|
192
|
+
"dao": dao,
|
|
191
193
|
"delhivery": delhivery,
|
|
192
194
|
"dhl": dhl,
|
|
193
195
|
"dhl-ecommerce": dhlEcommerce,
|
|
@@ -352,7 +354,7 @@ export const REGISTRY = {
|
|
|
352
354
|
"ctt-express": "ctt-express",
|
|
353
355
|
"cyprus-post": "universal",
|
|
354
356
|
"dachser": "dachser",
|
|
355
|
-
"dao": "
|
|
357
|
+
"dao": "dao",
|
|
356
358
|
"day-ross": "universal",
|
|
357
359
|
"delhivery": "delhivery",
|
|
358
360
|
"delivengo": "la-poste",
|
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
|
+
"version": "0.95.0-main.992",
|
|
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",
|