universal-parcel-scraper 0.89.0-main.975 → 0.90.0-main.979
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 +37 -5
- package/data/coverage-summary.json +1 -1
- package/data/detection-golden.json +24 -0
- package/dist/browser/scraper.js +29 -6
- package/dist/carriers/allegro-one/adapter.d.ts +15 -0
- package/dist/carriers/allegro-one/adapter.js +63 -0
- package/dist/carriers/allegro-one/carrier.json +56 -8
- package/dist/carriers/allegro-one/numbers.json +7 -7
- package/dist/carriers/allegro-one/parser.d.ts +15 -0
- package/dist/carriers/allegro-one/parser.js +91 -0
- package/dist/carriers/allegro-one/status.d.ts +12 -0
- package/dist/carriers/allegro-one/status.js +49 -0
- package/dist/carriers/allegro-one/statuses.json +26 -1
- package/dist/carriers/ceska-posta/adapter.d.ts +15 -0
- package/dist/carriers/ceska-posta/adapter.js +65 -0
- package/dist/carriers/ceska-posta/carrier.json +54 -7
- package/dist/carriers/ceska-posta/numbers.json +40 -0
- package/dist/carriers/ceska-posta/parser.d.ts +15 -0
- package/dist/carriers/ceska-posta/parser.js +137 -0
- package/dist/carriers/ceska-posta/status.d.ts +13 -0
- package/dist/carriers/ceska-posta/status.js +76 -0
- package/dist/carriers/ceska-posta/statuses.json +34 -1
- package/dist/carriers/express-one/adapter.d.ts +14 -0
- package/dist/carriers/express-one/adapter.js +49 -0
- package/dist/carriers/express-one/carrier.json +40 -7
- package/dist/carriers/express-one/parser.d.ts +12 -0
- package/dist/carriers/express-one/parser.js +178 -0
- package/dist/carriers/express-one/status.d.ts +13 -0
- package/dist/carriers/express-one/status.js +48 -0
- package/dist/carriers/express-one/statuses.json +123 -1
- package/dist/core/catalog/statusMaps.js +10 -6
- package/dist/data/catalog.json +37 -5
- package/dist/generated/catalog.d.ts +25 -5
- package/dist/generated/catalog.js +38 -6
- package/dist/generated/recognition.js +3 -0
- package/dist/generated/registry.js +9 -3
- 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 **148 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,072 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
|
@@ -4805,10 +4805,32 @@
|
|
|
4805
4805
|
"timezone": "Europe/Warsaw",
|
|
4806
4806
|
"tracking": {
|
|
4807
4807
|
"mode": "automatic",
|
|
4808
|
-
"adapter": "
|
|
4808
|
+
"adapter": "allegro-one",
|
|
4809
|
+
"recognitionRank": 770
|
|
4809
4810
|
},
|
|
4810
|
-
"canaryUrl": "https://allegro.pl/",
|
|
4811
|
-
"
|
|
4811
|
+
"canaryUrl": "https://allegro.pl/allegrodelivery/sledzenie-paczki",
|
|
4812
|
+
"trackingUrlTemplate": "https://allegro.pl/allegrodelivery/sledzenie-paczki?numer={trackingNumber}",
|
|
4813
|
+
"trackingSearchUrl": "https://allegro.pl/allegrodelivery/sledzenie-paczki",
|
|
4814
|
+
"linkRules": [
|
|
4815
|
+
{
|
|
4816
|
+
"domains": [
|
|
4817
|
+
"allegro.pl"
|
|
4818
|
+
],
|
|
4819
|
+
"pathPattern": "^/allegrodelivery/sledzenie-paczki/?$",
|
|
4820
|
+
"params": [
|
|
4821
|
+
"numer"
|
|
4822
|
+
]
|
|
4823
|
+
},
|
|
4824
|
+
{
|
|
4825
|
+
"domains": [
|
|
4826
|
+
"allegro.pl"
|
|
4827
|
+
],
|
|
4828
|
+
"pathPattern": "^/kampania/one/kurier/sledzenie-paczki/?$",
|
|
4829
|
+
"params": [
|
|
4830
|
+
"numer"
|
|
4831
|
+
]
|
|
4832
|
+
}
|
|
4833
|
+
],
|
|
4812
4834
|
"detectionRules": [
|
|
4813
4835
|
{
|
|
4814
4836
|
"pattern": "^A[0D][0-9A-Z]{8}$",
|
|
@@ -5069,7 +5091,9 @@
|
|
|
5069
5091
|
"timezone": "Europe/Prague",
|
|
5070
5092
|
"tracking": {
|
|
5071
5093
|
"mode": "automatic",
|
|
5072
|
-
"adapter": "
|
|
5094
|
+
"adapter": "ceska-posta",
|
|
5095
|
+
"recognitionRank": 760,
|
|
5096
|
+
"localClocks": true
|
|
5073
5097
|
},
|
|
5074
5098
|
"canaryUrl": "https://www.postaonline.cz/trackandtrace",
|
|
5075
5099
|
"trackingUrlTemplate": "https://www.postaonline.cz/trackandtrace/-/zasilka/{trackingNumber}",
|
|
@@ -5086,6 +5110,12 @@
|
|
|
5086
5110
|
{
|
|
5087
5111
|
"pattern": "^[BD][A-Z]\\d{10}[A-Z]$",
|
|
5088
5112
|
"confidence": "high"
|
|
5113
|
+
},
|
|
5114
|
+
{
|
|
5115
|
+
"pattern": "^[A-Z]{2}\\d{9}CZ$",
|
|
5116
|
+
"confidence": "low",
|
|
5117
|
+
"checksum": "s10",
|
|
5118
|
+
"preferred": true
|
|
5089
5119
|
}
|
|
5090
5120
|
]
|
|
5091
5121
|
},
|
|
@@ -6155,7 +6185,9 @@
|
|
|
6155
6185
|
"timezone": "Europe/Budapest",
|
|
6156
6186
|
"tracking": {
|
|
6157
6187
|
"mode": "automatic",
|
|
6158
|
-
"adapter": "
|
|
6188
|
+
"adapter": "express-one",
|
|
6189
|
+
"recognitionRank": 780,
|
|
6190
|
+
"localClocks": true
|
|
6159
6191
|
},
|
|
6160
6192
|
"canaryUrl": "https://tracking.expressone.hu/",
|
|
6161
6193
|
"trackingUrlTemplate": "https://tracking.expressone.hu/?plc_number={trackingNumber}",
|
|
@@ -26851,6 +26851,18 @@
|
|
|
26851
26851
|
"intelcom"
|
|
26852
26852
|
]
|
|
26853
26853
|
},
|
|
26854
|
+
{
|
|
26855
|
+
"input": "CS011354097CZ",
|
|
26856
|
+
"carrier": "intl-post",
|
|
26857
|
+
"confidence": "high",
|
|
26858
|
+
"candidates": [
|
|
26859
|
+
"intl-post"
|
|
26860
|
+
],
|
|
26861
|
+
"asked": [
|
|
26862
|
+
"ceska-posta",
|
|
26863
|
+
"chronopost"
|
|
26864
|
+
]
|
|
26865
|
+
},
|
|
26854
26866
|
{
|
|
26855
26867
|
"input": "CT001362607SK",
|
|
26856
26868
|
"carrier": "intl-post",
|
|
@@ -34167,6 +34179,18 @@
|
|
|
34167
34179
|
"correos-chile"
|
|
34168
34180
|
]
|
|
34169
34181
|
},
|
|
34182
|
+
{
|
|
34183
|
+
"input": "RR562174447CZ",
|
|
34184
|
+
"carrier": "intl-post",
|
|
34185
|
+
"confidence": "high",
|
|
34186
|
+
"candidates": [
|
|
34187
|
+
"intl-post"
|
|
34188
|
+
],
|
|
34189
|
+
"asked": [
|
|
34190
|
+
"ceska-posta",
|
|
34191
|
+
"chronopost"
|
|
34192
|
+
]
|
|
34193
|
+
},
|
|
34170
34194
|
{
|
|
34171
34195
|
"input": "RR568370453CL",
|
|
34172
34196
|
"carrier": "correos-chile",
|
package/dist/browser/scraper.js
CHANGED
|
@@ -3947,10 +3947,21 @@ var be = {
|
|
|
3947
3947
|
timezone: "Europe/Warsaw",
|
|
3948
3948
|
tracking: {
|
|
3949
3949
|
mode: "automatic",
|
|
3950
|
-
adapter: "
|
|
3950
|
+
adapter: "allegro-one",
|
|
3951
|
+
recognitionRank: 770
|
|
3951
3952
|
},
|
|
3952
|
-
canaryUrl: "https://allegro.pl/",
|
|
3953
|
-
|
|
3953
|
+
canaryUrl: "https://allegro.pl/allegrodelivery/sledzenie-paczki",
|
|
3954
|
+
trackingUrlTemplate: "https://allegro.pl/allegrodelivery/sledzenie-paczki?numer={trackingNumber}",
|
|
3955
|
+
trackingSearchUrl: "https://allegro.pl/allegrodelivery/sledzenie-paczki",
|
|
3956
|
+
linkRules: [{
|
|
3957
|
+
domains: ["allegro.pl"],
|
|
3958
|
+
pathPattern: "^/allegrodelivery/sledzenie-paczki/?$",
|
|
3959
|
+
params: ["numer"]
|
|
3960
|
+
}, {
|
|
3961
|
+
domains: ["allegro.pl"],
|
|
3962
|
+
pathPattern: "^/kampania/one/kurier/sledzenie-paczki/?$",
|
|
3963
|
+
params: ["numer"]
|
|
3964
|
+
}],
|
|
3954
3965
|
detectionRules: [{
|
|
3955
3966
|
pattern: "^A[0D][0-9A-Z]{8}$",
|
|
3956
3967
|
confidence: "high"
|
|
@@ -4145,7 +4156,9 @@ var be = {
|
|
|
4145
4156
|
timezone: "Europe/Prague",
|
|
4146
4157
|
tracking: {
|
|
4147
4158
|
mode: "automatic",
|
|
4148
|
-
adapter: "
|
|
4159
|
+
adapter: "ceska-posta",
|
|
4160
|
+
recognitionRank: 760,
|
|
4161
|
+
localClocks: !0
|
|
4149
4162
|
},
|
|
4150
4163
|
canaryUrl: "https://www.postaonline.cz/trackandtrace",
|
|
4151
4164
|
trackingUrlTemplate: "https://www.postaonline.cz/trackandtrace/-/zasilka/{trackingNumber}",
|
|
@@ -4157,6 +4170,11 @@ var be = {
|
|
|
4157
4170
|
detectionRules: [{
|
|
4158
4171
|
pattern: "^[BD][A-Z]\\d{10}[A-Z]$",
|
|
4159
4172
|
confidence: "high"
|
|
4173
|
+
}, {
|
|
4174
|
+
pattern: "^[A-Z]{2}\\d{9}CZ$",
|
|
4175
|
+
confidence: "low",
|
|
4176
|
+
checksum: "s10",
|
|
4177
|
+
preferred: !0
|
|
4160
4178
|
}]
|
|
4161
4179
|
},
|
|
4162
4180
|
"chit-chats": {
|
|
@@ -4950,7 +4968,9 @@ var be = {
|
|
|
4950
4968
|
timezone: "Europe/Budapest",
|
|
4951
4969
|
tracking: {
|
|
4952
4970
|
mode: "automatic",
|
|
4953
|
-
adapter: "
|
|
4971
|
+
adapter: "express-one",
|
|
4972
|
+
recognitionRank: 780,
|
|
4973
|
+
localClocks: !0
|
|
4954
4974
|
},
|
|
4955
4975
|
canaryUrl: "https://tracking.expressone.hu/",
|
|
4956
4976
|
trackingUrlTemplate: "https://tracking.expressone.hu/?plc_number={trackingNumber}",
|
|
@@ -7039,7 +7059,7 @@ var be = {
|
|
|
7039
7059
|
cargus: [],
|
|
7040
7060
|
cdek: [],
|
|
7041
7061
|
cdl: ["cdl-1", "cdl-2"],
|
|
7042
|
-
"ceska-posta": ["ceska-posta-1"],
|
|
7062
|
+
"ceska-posta": ["ceska-posta-1", "ceska-posta-s10"],
|
|
7043
7063
|
"chit-chats": ["chit-chats-1"],
|
|
7044
7064
|
cne: ["cne-1"],
|
|
7045
7065
|
"correos-de-mexico": [],
|
|
@@ -11856,6 +11876,7 @@ function yc(e) {
|
|
|
11856
11876
|
var bc = {
|
|
11857
11877
|
"acs-courier": 570,
|
|
11858
11878
|
aliexpress: 140,
|
|
11879
|
+
"allegro-one": 770,
|
|
11859
11880
|
"aras-kargo": 670,
|
|
11860
11881
|
"australia-post": 60,
|
|
11861
11882
|
"austrian-post": 390,
|
|
@@ -11865,6 +11886,7 @@ var bc = {
|
|
|
11865
11886
|
"canada-post": 150,
|
|
11866
11887
|
canpar: 340,
|
|
11867
11888
|
cargus: 730,
|
|
11889
|
+
"ceska-posta": 760,
|
|
11868
11890
|
chronopost: 450,
|
|
11869
11891
|
ciblex: 200,
|
|
11870
11892
|
"colis-prive": 300,
|
|
@@ -11884,6 +11906,7 @@ var bc = {
|
|
|
11884
11906
|
econt: 560,
|
|
11885
11907
|
elta: 550,
|
|
11886
11908
|
emile: 40,
|
|
11909
|
+
"express-one": 780,
|
|
11887
11910
|
"fan-courier": 510,
|
|
11888
11911
|
"four-px": 130,
|
|
11889
11912
|
foxpost: 520,
|
|
@@ -0,0 +1,15 @@
|
|
|
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 ALLEGRO_ONE_ENDPOINT = "https://edge.allegro.pl/one/tracking";
|
|
5
|
+
export declare function allegroOneTrackingUrl(raw: string): string;
|
|
6
|
+
export declare class AllegroOneTracker {
|
|
7
|
+
private readonly options;
|
|
8
|
+
constructor(options?: {
|
|
9
|
+
fetcher?: typeof fetch;
|
|
10
|
+
recorder?: StepRecorder;
|
|
11
|
+
userAgent?: string;
|
|
12
|
+
});
|
|
13
|
+
fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
|
|
14
|
+
}
|
|
15
|
+
export declare const adapter: AdapterFactory;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
|
|
2
|
+
import { ChallengeError, IndeterminateError, SchemaError } 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, userAgentOf } from '../../core/transport/index.js';
|
|
7
|
+
import { ALLEGRO_ONE_MAX_BYTES, normalizeAllegroOneNumber, parseAllegroOne, PROVIDER } from './parser.js';
|
|
8
|
+
// The service Allegro's tracking page asks through its edge gateway.
|
|
9
|
+
export const ALLEGRO_ONE_ENDPOINT = 'https://edge.allegro.pl/one/tracking';
|
|
10
|
+
// The gateway answers only the media type Allegro's own pages ask for.
|
|
11
|
+
const MEDIA_TYPE = 'application/vnd.allegro.internal.v1+json';
|
|
12
|
+
const REDIRECTS = [301, 302, 303, 307, 308];
|
|
13
|
+
// DataDome's interstitial, which guards Allegro's pages and could come instead of the reply.
|
|
14
|
+
const CHALLENGE = /captcha-delivery\.com|datadome/i;
|
|
15
|
+
export function allegroOneTrackingUrl(raw) {
|
|
16
|
+
return `${ALLEGRO_ONE_ENDPOINT}?packageNo=${encodeURIComponent(normalizeAllegroOneNumber(raw))}`;
|
|
17
|
+
}
|
|
18
|
+
export class AllegroOneTracker {
|
|
19
|
+
options;
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.options = options;
|
|
22
|
+
}
|
|
23
|
+
async fetch(raw, context = {}) {
|
|
24
|
+
const url = allegroOneTrackingUrl(raw);
|
|
25
|
+
const read = async (signal, timeoutMs) => {
|
|
26
|
+
// The scans are worded in the language asked for: Polish for `pl-PL` or
|
|
27
|
+
// `*`, English for `en-US`. The status map reads English.
|
|
28
|
+
const { response, bytes } = await fetchBounded(url, { signal, headers: {
|
|
29
|
+
Accept: MEDIA_TYPE, 'Accept-Language': 'en-US', 'User-Agent': userAgentOf(this.options.userAgent),
|
|
30
|
+
} }, { provider: PROVIDER, timeoutMs: Math.max(1, Math.floor(timeoutMs)), maxBytes: ALLEGRO_ONE_MAX_BYTES, redirect: 'manual',
|
|
31
|
+
allowHttpStatuses: [...REDIRECTS, 400, 404, 406, 410], fetcher: this.options.fetcher });
|
|
32
|
+
// A refused request, a retired route ("Feature unavailable") or a move says nothing about the parcel.
|
|
33
|
+
if (response.status !== 200)
|
|
34
|
+
throw new IndeterminateError(PROVIDER, 'Allegro One tracking service is unavailable');
|
|
35
|
+
const text = decodeText(bytes);
|
|
36
|
+
let reply;
|
|
37
|
+
try {
|
|
38
|
+
reply = JSON.parse(text);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (CHALLENGE.test(text))
|
|
42
|
+
throw new ChallengeError(PROVIDER, 'Allegro One answered with a bot check', { cause: error });
|
|
43
|
+
throw new SchemaError(PROVIDER, 'Allegro One returned a reply that is not JSON', { cause: error });
|
|
44
|
+
}
|
|
45
|
+
return parseAllegroOne(reply, raw);
|
|
46
|
+
};
|
|
47
|
+
// A request that fails to reach Allegro, or hangs, gets one more try: the
|
|
48
|
+
// first gets half of the budget, so the retry fits in the rest.
|
|
49
|
+
return runSteps({ carrier: 'allegro-one', budgetMs: context.budgetMs ?? 15_000, signal: context.signal,
|
|
50
|
+
recorder: this.options.recorder ?? NOOP_RECORDER }, [
|
|
51
|
+
{ id: 'direct', run: ({ signal, remainingMs }) => read(signal, firstAttemptMs(remainingMs)) },
|
|
52
|
+
networkRetryStep(({ signal, remainingMs }) => read(signal, remainingMs)),
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export const adapter = (environment) => {
|
|
57
|
+
const tracker = new AllegroOneTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
|
|
58
|
+
return {
|
|
59
|
+
id: 'allegro-one', recordsSteps: true, steps: ['direct', 'retry'],
|
|
60
|
+
track: (input, context) => tracker.fetch(input.number, context),
|
|
61
|
+
recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizeAllegroOneNumber(number))),
|
|
62
|
+
};
|
|
63
|
+
};
|
|
@@ -19,18 +19,66 @@
|
|
|
19
19
|
"timezone": "Europe/Warsaw",
|
|
20
20
|
"tracking": {
|
|
21
21
|
"mode": "automatic",
|
|
22
|
-
"adapter": "
|
|
23
|
-
"steps": [
|
|
22
|
+
"adapter": "allegro-one",
|
|
23
|
+
"steps": [
|
|
24
|
+
"direct",
|
|
25
|
+
"retry"
|
|
26
|
+
],
|
|
27
|
+
"recognition": {
|
|
28
|
+
"rank": 770
|
|
29
|
+
}
|
|
24
30
|
},
|
|
25
|
-
"capabilities": [
|
|
31
|
+
"capabilities": [
|
|
32
|
+
"history",
|
|
33
|
+
"delivered_at"
|
|
34
|
+
],
|
|
26
35
|
"portal": {
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
36
|
+
"url": "https://allegro.pl/allegrodelivery/sledzenie-paczki?numer={trackingNumber}",
|
|
37
|
+
"searchUrl": "https://allegro.pl/allegrodelivery/sledzenie-paczki",
|
|
38
|
+
"canaryUrl": "https://allegro.pl/allegrodelivery/sledzenie-paczki",
|
|
39
|
+
"shows": [
|
|
40
|
+
"status",
|
|
41
|
+
"history",
|
|
42
|
+
"delivered_at"
|
|
43
|
+
],
|
|
44
|
+
"scraped": [
|
|
45
|
+
"status",
|
|
46
|
+
"history",
|
|
47
|
+
"delivered_at",
|
|
48
|
+
"offset_timestamps"
|
|
49
|
+
],
|
|
30
50
|
"discarded": [],
|
|
31
|
-
"unavailable": [
|
|
51
|
+
"unavailable": [
|
|
52
|
+
"location",
|
|
53
|
+
"eta",
|
|
54
|
+
"pickup_point",
|
|
55
|
+
"weight",
|
|
56
|
+
"service_name",
|
|
57
|
+
"sender_name",
|
|
58
|
+
"recipient_name",
|
|
59
|
+
"delivery_partner"
|
|
60
|
+
]
|
|
32
61
|
},
|
|
33
|
-
"links": [
|
|
62
|
+
"links": [
|
|
63
|
+
{
|
|
64
|
+
"domains": [
|
|
65
|
+
"allegro.pl"
|
|
66
|
+
],
|
|
67
|
+
"pathPattern": "^/allegrodelivery/sledzenie-paczki/?$",
|
|
68
|
+
"params": [
|
|
69
|
+
"numer"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"domains": [
|
|
74
|
+
"allegro.pl"
|
|
75
|
+
],
|
|
76
|
+
"pathPattern": "^/kampania/one/kurier/sledzenie-paczki/?$",
|
|
77
|
+
"params": [
|
|
78
|
+
"numer"
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
],
|
|
34
82
|
"detection": [
|
|
35
83
|
{
|
|
36
84
|
"id": "allegro-one-1",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"access": "full_page"
|
|
13
13
|
},
|
|
14
14
|
"context": {
|
|
15
|
-
"assessment": "
|
|
15
|
+
"assessment": "confirmed"
|
|
16
16
|
},
|
|
17
17
|
"expect": {
|
|
18
18
|
"carrier": "allegro-one",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"access": "full_page"
|
|
32
32
|
},
|
|
33
33
|
"context": {
|
|
34
|
-
"assessment": "
|
|
34
|
+
"assessment": "confirmed"
|
|
35
35
|
},
|
|
36
36
|
"expect": {
|
|
37
37
|
"carrier": "allegro-one",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"access": "full_page"
|
|
51
51
|
},
|
|
52
52
|
"context": {
|
|
53
|
-
"assessment": "
|
|
53
|
+
"assessment": "confirmed"
|
|
54
54
|
},
|
|
55
55
|
"expect": {
|
|
56
56
|
"carrier": "allegro-one",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"access": "full_page"
|
|
70
70
|
},
|
|
71
71
|
"context": {
|
|
72
|
-
"assessment": "
|
|
72
|
+
"assessment": "confirmed"
|
|
73
73
|
},
|
|
74
74
|
"expect": {
|
|
75
75
|
"carrier": "allegro-one",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"access": "full_page"
|
|
89
89
|
},
|
|
90
90
|
"context": {
|
|
91
|
-
"assessment": "
|
|
91
|
+
"assessment": "confirmed"
|
|
92
92
|
},
|
|
93
93
|
"expect": {
|
|
94
94
|
"carrier": "allegro-one",
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"access": "full_page"
|
|
108
108
|
},
|
|
109
109
|
"context": {
|
|
110
|
-
"assessment": "
|
|
110
|
+
"assessment": "confirmed"
|
|
111
111
|
},
|
|
112
112
|
"expect": {
|
|
113
113
|
"carrier": "allegro-one",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"access": "full_page"
|
|
127
127
|
},
|
|
128
128
|
"context": {
|
|
129
|
-
"assessment": "
|
|
129
|
+
"assessment": "confirmed"
|
|
130
130
|
},
|
|
131
131
|
"expect": {
|
|
132
132
|
"carrier": "allegro-one",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CarrierResult } from '../../core/result/index.js';
|
|
2
|
+
export declare const PROVIDER = "Allegro One";
|
|
3
|
+
export declare const ALLEGRO_ONE_MAX_BYTES: number;
|
|
4
|
+
/** An Allegro One number; anything else is a number Allegro One does not issue. */
|
|
5
|
+
export declare function normalizeAllegroOneNumber(raw: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* The reply of `GET /one/tracking?packageNo={number}` on Allegro's edge
|
|
8
|
+
* gateway: `{ "status": [...] }`, each scan a UTC instant and a wording. It
|
|
9
|
+
* does not echo the number: the scans are those of the number in the request.
|
|
10
|
+
* An empty or null list is what the tracker shows as "not found". Every A0
|
|
11
|
+
* number seen has scans from its label on, so an empty list is not found for
|
|
12
|
+
* them; every real AD number seen had an empty list, so for AD and the older
|
|
13
|
+
* ALE form it stays inconclusive.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseAllegroOne(payload: unknown, raw: string): CarrierResult;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { normalizeTrackingNumber } from '../../core/detection/index.js';
|
|
2
|
+
import { InvalidInputError, NoHistoryError, NotFoundError, SchemaError } from '../../core/errors/index.js';
|
|
3
|
+
import { classifyWording, languageStageStatus } from '../../core/status/index.js';
|
|
4
|
+
import { explicitOffsetTime } from '../../core/time/index.js';
|
|
5
|
+
import { clean } from '../../core/transport/index.js';
|
|
6
|
+
import { isRecord } from '../../core/types.js';
|
|
7
|
+
import { allegroOneStage } from './status.js';
|
|
8
|
+
export const PROVIDER = 'Allegro One';
|
|
9
|
+
// A parcel's reply is a list of short scans; a few kilobytes.
|
|
10
|
+
export const ALLEGRO_ONE_MAX_BYTES = 256 * 1024;
|
|
11
|
+
// Allegro One's ten-character numbers start A0, Allegro Delivery's start AD,
|
|
12
|
+
// and the older form the tracker still answers is ALE and twelve digits.
|
|
13
|
+
const NUMBER = /^(?:A[0D][0-9A-Z]{8}|ALE\d{12})$/;
|
|
14
|
+
const MAX_ROWS = 300;
|
|
15
|
+
const MAX_EVENTS = 100;
|
|
16
|
+
/** An Allegro One number; anything else is a number Allegro One does not issue. */
|
|
17
|
+
export function normalizeAllegroOneNumber(raw) {
|
|
18
|
+
const number = raw.length <= 64 ? normalizeTrackingNumber(raw) : '';
|
|
19
|
+
if (!NUMBER.test(number)) {
|
|
20
|
+
throw new InvalidInputError(PROVIDER, 'Allegro One numbers are A0 or AD and eight letters and digits, or ALE and twelve digits');
|
|
21
|
+
}
|
|
22
|
+
return number;
|
|
23
|
+
}
|
|
24
|
+
function projectScan(row, index) {
|
|
25
|
+
if (!isRecord(row) || typeof row.eventTimestamp !== 'string' || typeof row.description !== 'string') {
|
|
26
|
+
throw new SchemaError(PROVIDER, 'Allegro One returned an invalid scan');
|
|
27
|
+
}
|
|
28
|
+
const time = explicitOffsetTime(row.eventTimestamp);
|
|
29
|
+
if (!time)
|
|
30
|
+
throw new SchemaError(PROVIDER, 'Allegro One returned a scan without an offset time');
|
|
31
|
+
const description = clean(row.description, 300);
|
|
32
|
+
if (!description)
|
|
33
|
+
throw new SchemaError(PROVIDER, 'Allegro One returned a scan without wording');
|
|
34
|
+
const stage = allegroOneStage(description);
|
|
35
|
+
return {
|
|
36
|
+
event: { time: time.iso, description, ...(stage ? { stage, stage_source: 'carrier_map' } : {}) },
|
|
37
|
+
at: time.timestamp,
|
|
38
|
+
index,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The reply of `GET /one/tracking?packageNo={number}` on Allegro's edge
|
|
43
|
+
* gateway: `{ "status": [...] }`, each scan a UTC instant and a wording. It
|
|
44
|
+
* does not echo the number: the scans are those of the number in the request.
|
|
45
|
+
* An empty or null list is what the tracker shows as "not found". Every A0
|
|
46
|
+
* number seen has scans from its label on, so an empty list is not found for
|
|
47
|
+
* them; every real AD number seen had an empty list, so for AD and the older
|
|
48
|
+
* ALE form it stays inconclusive.
|
|
49
|
+
*/
|
|
50
|
+
export function parseAllegroOne(payload, raw) {
|
|
51
|
+
const number = normalizeAllegroOneNumber(raw);
|
|
52
|
+
if (!isRecord(payload) || !('status' in payload) || (payload.status !== null && !Array.isArray(payload.status))) {
|
|
53
|
+
throw new SchemaError(PROVIDER, 'Allegro One returned an unexpected reply');
|
|
54
|
+
}
|
|
55
|
+
const rows = (payload.status ?? []);
|
|
56
|
+
if (rows.length > MAX_ROWS)
|
|
57
|
+
throw new SchemaError(PROVIDER, 'Allegro One returned an oversized history');
|
|
58
|
+
if (!rows.length) {
|
|
59
|
+
if (number.startsWith('A0'))
|
|
60
|
+
throw new NotFoundError(PROVIDER);
|
|
61
|
+
throw new NoHistoryError(PROVIDER, 'Allegro One has no scans for this number');
|
|
62
|
+
}
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const scans = [];
|
|
65
|
+
rows.forEach((row, index) => {
|
|
66
|
+
const scan = projectScan(row, index);
|
|
67
|
+
const key = `${scan.event.time}\n${scan.event.description}`;
|
|
68
|
+
if (seen.has(key))
|
|
69
|
+
return;
|
|
70
|
+
seen.add(key);
|
|
71
|
+
scans.push(scan);
|
|
72
|
+
});
|
|
73
|
+
// The reply lists scans newest first and the tracker sorts them so too.
|
|
74
|
+
// Instants order them; equal ones keep the reply's order.
|
|
75
|
+
scans.sort((left, right) => right.at - left.at || left.index - right.index);
|
|
76
|
+
const newest = scans[0].event;
|
|
77
|
+
const current = newest.stage
|
|
78
|
+
? { stage: newest.stage, source: 'carrier_map' }
|
|
79
|
+
: classifyWording(newest.description, 'in_transit');
|
|
80
|
+
const stage = current.source !== 'none' ? current.stage : undefined;
|
|
81
|
+
return {
|
|
82
|
+
status: stage ? languageStageStatus(stage) : 'unknown',
|
|
83
|
+
...(stage ? { current_stage: stage, current_stage_source: current.source } : {}),
|
|
84
|
+
last_status_text: newest.description,
|
|
85
|
+
last_update: newest.time,
|
|
86
|
+
expected_delivery: null,
|
|
87
|
+
...(stage === 'delivered' ? { delivered_at: newest.time } : {}),
|
|
88
|
+
events: scans.slice(0, MAX_EVENTS).map((scan) => scan.event),
|
|
89
|
+
...(scans.length > MAX_EVENTS ? { history_truncated: true } : {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Stage } from '../../core/status/index.js';
|
|
2
|
+
import { type CarrierStatusMap } from '../../core/status/statusMap.js';
|
|
3
|
+
/**
|
|
4
|
+
* The tracker's English status wordings. The service sends no status code, so
|
|
5
|
+
* each scan is staged by its wording. A parcel goes to a One Box locker, a One
|
|
6
|
+
* Punkt pickup point or the recipient's door; the history does not say which.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ALLEGRO_ONE_SCANS: Readonly<Record<string, Stage>>;
|
|
9
|
+
/** The stage a scan's wording gives it, if any. */
|
|
10
|
+
export declare function allegroOneStage(wording: string): Stage | undefined;
|
|
11
|
+
/** The tracker's scans carry a wording and no status code. */
|
|
12
|
+
export declare const statusMap: CarrierStatusMap;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { normalizeStatusWording } from '../../core/status/statusMap.js';
|
|
2
|
+
/**
|
|
3
|
+
* The tracker's English status wordings. The service sends no status code, so
|
|
4
|
+
* each scan is staged by its wording. A parcel goes to a One Box locker, a One
|
|
5
|
+
* Punkt pickup point or the recipient's door; the history does not say which.
|
|
6
|
+
*/
|
|
7
|
+
export const ALLEGRO_ONE_SCANS = {
|
|
8
|
+
// The sender made the label; Allegro One has not handled the parcel yet.
|
|
9
|
+
'Parcel has been prepared by the sender': 'registered',
|
|
10
|
+
// The sender dropped the parcel in a locker or at a point.
|
|
11
|
+
'Parcel has been dispatched': 'accepted',
|
|
12
|
+
'Parcel has been dispatched at point': 'accepted',
|
|
13
|
+
'Parcel has been picked up by the courier': 'accepted',
|
|
14
|
+
// A courier collected the sender's drop-off from the locker or the point.
|
|
15
|
+
'Parcel has been picked up from parcel locker by courier': 'accepted',
|
|
16
|
+
'Parcel has been picked up from point by courier': 'accepted',
|
|
17
|
+
'Courier has transferred the parcel to the warehouse': 'in_transit',
|
|
18
|
+
'Parcel has been dispatched to its destination point': 'in_transit',
|
|
19
|
+
'Parcel has been accepted at the sorting center': 'in_transit',
|
|
20
|
+
'Parcel has been accepted at the branch': 'in_transit',
|
|
21
|
+
'Parcel has been redirected to a different point': 'in_transit',
|
|
22
|
+
// Notices while the parcel is on its way; neither stops it.
|
|
23
|
+
'Parcel may be delivered with delay': 'in_transit',
|
|
24
|
+
'Parcel delivery time has been arranged with the recipient': 'in_transit',
|
|
25
|
+
'Parcel has been released for delivery': 'out_for_delivery',
|
|
26
|
+
// The courier found the locker full and keeps the parcel.
|
|
27
|
+
'No space in the parcel locker': 'failed_attempt',
|
|
28
|
+
// The recipient is asked to choose another point.
|
|
29
|
+
'Parcel is waiting for an alternative point indication': 'exception',
|
|
30
|
+
'Parcel is awaiting pick-up': 'ready_for_pickup',
|
|
31
|
+
'Parcel is awaiting pick-up — pick-up deadline is about to expire': 'ready_for_pickup',
|
|
32
|
+
'Time for pick-up has been extended': 'ready_for_pickup',
|
|
33
|
+
'Time for parcel pick-up has expired': 'exception',
|
|
34
|
+
// The parcel was not where a scan expected it; a later scan can find it again.
|
|
35
|
+
'No parcel': 'exception',
|
|
36
|
+
'Parcel has been lost': 'exception',
|
|
37
|
+
'Parcel has been returned to the sender': 'returned',
|
|
38
|
+
'Parcel has been delivered': 'delivered',
|
|
39
|
+
};
|
|
40
|
+
const SCANS = new Map(Object.entries(ALLEGRO_ONE_SCANS).map(([wording, stage]) => [normalizeStatusWording(wording), stage]));
|
|
41
|
+
/** The stage a scan's wording gives it, if any. */
|
|
42
|
+
export function allegroOneStage(wording) {
|
|
43
|
+
return SCANS.get(normalizeStatusWording(wording));
|
|
44
|
+
}
|
|
45
|
+
/** The tracker's scans carry a wording and no status code. */
|
|
46
|
+
export const statusMap = {
|
|
47
|
+
stage: (code, wording) => code ? undefined : SCANS.get(wording),
|
|
48
|
+
gaps: [],
|
|
49
|
+
};
|
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"carrier": "allegro-one",
|
|
3
|
-
"entries": [
|
|
3
|
+
"entries": [
|
|
4
|
+
{"wording": "Parcel has been prepared by the sender", "language": "en", "stage": "registered", "confirmedBy": "live", "note": "The sender made the label; Allegro One has not handled the parcel yet."},
|
|
5
|
+
{"wording": "Parcel has been dispatched", "language": "en", "stage": "accepted", "confirmedBy": "live", "note": "The sender dropped the parcel off."},
|
|
6
|
+
{"wording": "Parcel has been dispatched at point", "language": "en", "stage": "accepted", "confirmedBy": "live", "note": "The sender dropped the parcel off at a pickup point."},
|
|
7
|
+
{"wording": "Parcel has been picked up by the courier", "language": "en", "stage": "accepted", "confirmedBy": "live", "note": "The courier collected the parcel from the sender."},
|
|
8
|
+
{"wording": "Parcel has been picked up from parcel locker by courier", "language": "en", "stage": "accepted", "confirmedBy": "live", "note": "A courier collected the sender's drop-off from a locker."},
|
|
9
|
+
{"wording": "Parcel has been picked up from point by courier", "language": "en", "stage": "accepted", "confirmedBy": "live", "note": "A courier collected the sender's drop-off from a pickup point."},
|
|
10
|
+
{"wording": "Courier has transferred the parcel to the warehouse", "language": "en", "stage": "in_transit", "confirmedBy": "live"},
|
|
11
|
+
{"wording": "Parcel has been dispatched to its destination point", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "The parcel left for the locker, point or branch serving the recipient."},
|
|
12
|
+
{"wording": "Parcel has been accepted at the sorting center", "language": "en", "stage": "in_transit", "confirmedBy": "live"},
|
|
13
|
+
{"wording": "Parcel has been accepted at the branch", "language": "en", "stage": "in_transit", "confirmedBy": "live"},
|
|
14
|
+
{"wording": "Parcel has been redirected to a different point", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "Follows the recipient's choice of another point."},
|
|
15
|
+
{"wording": "Parcel may be delivered with delay", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "A notice; the parcel keeps moving."},
|
|
16
|
+
{"wording": "Parcel delivery time has been arranged with the recipient", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "A notice; the parcel keeps moving."},
|
|
17
|
+
{"wording": "Parcel has been released for delivery", "language": "en", "stage": "out_for_delivery", "confirmedBy": "live", "note": "Also before a locker delivery."},
|
|
18
|
+
{"wording": "No space in the parcel locker", "language": "en", "stage": "failed_attempt", "confirmedBy": "live", "note": "The courier found the locker full and took the parcel back."},
|
|
19
|
+
{"wording": "Parcel is waiting for an alternative point indication", "language": "en", "stage": "exception", "confirmedBy": "live", "note": "The recipient is asked to choose another point."},
|
|
20
|
+
{"wording": "Parcel is awaiting pick-up", "language": "en", "stage": "ready_for_pickup", "confirmedBy": "live"},
|
|
21
|
+
{"wording": "Parcel is awaiting pick-up — pick-up deadline is about to expire", "language": "en", "stage": "ready_for_pickup", "confirmedBy": "live"},
|
|
22
|
+
{"wording": "Time for pick-up has been extended", "language": "en", "stage": "ready_for_pickup", "confirmedBy": "live"},
|
|
23
|
+
{"wording": "Time for parcel pick-up has expired", "language": "en", "stage": "exception", "confirmedBy": "live", "note": "A return to the sender follows."},
|
|
24
|
+
{"wording": "No parcel", "language": "en", "stage": "exception", "confirmedBy": "live", "note": "The parcel was not where a scan expected it; later scans can find it again."},
|
|
25
|
+
{"wording": "Parcel has been lost", "language": "en", "stage": "exception", "confirmedBy": "live", "note": "Later scans can find the parcel again."},
|
|
26
|
+
{"wording": "Parcel has been returned to the sender", "language": "en", "stage": "returned", "confirmedBy": "live"},
|
|
27
|
+
{"wording": "Parcel has been delivered", "language": "en", "stage": "delivered", "confirmedBy": "live", "note": "Also when the recipient collects the parcel from a locker or point."}
|
|
28
|
+
]
|
|
4
29
|
}
|