universal-parcel-scraper 0.75.0-main.929 → 0.76.0-main.933
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 +7 -3
- package/data/coverage-summary.json +1 -1
- package/data/detection-golden.json +95 -0
- package/dist/browser/scraper.js +9 -3
- package/dist/carriers/geniki-taxydromiki/adapter.d.ts +12 -0
- package/dist/carriers/geniki-taxydromiki/adapter.js +51 -0
- package/dist/carriers/geniki-taxydromiki/carrier.json +40 -7
- package/dist/carriers/geniki-taxydromiki/numbers.json +14 -14
- package/dist/carriers/geniki-taxydromiki/parser.d.ts +11 -0
- package/dist/carriers/geniki-taxydromiki/parser.js +127 -0
- package/dist/carriers/geniki-taxydromiki/status.d.ts +11 -0
- package/dist/carriers/geniki-taxydromiki/status.js +44 -0
- package/dist/carriers/geniki-taxydromiki/statuses.json +110 -1
- package/dist/carriers/gls-it/adapter.d.ts +15 -0
- package/dist/carriers/gls-it/adapter.js +64 -0
- package/dist/carriers/gls-it/carrier.json +55 -8
- package/dist/carriers/gls-it/numbers.json +2 -2
- package/dist/carriers/gls-it/parser.d.ts +16 -0
- package/dist/carriers/gls-it/parser.js +145 -0
- package/dist/carriers/gls-it/status.d.ts +20 -0
- package/dist/carriers/gls-it/status.js +54 -0
- package/dist/carriers/gls-it/statuses.json +125 -1
- package/dist/carriers/mrw/numbers.json +199 -0
- package/dist/core/catalog/statusMaps.js +5 -3
- package/dist/data/catalog.json +7 -3
- package/dist/generated/catalog.d.ts +7 -3
- package/dist/generated/catalog.js +7 -3
- package/dist/generated/recognition.js +2 -0
- package/dist/generated/registry.js +6 -2
- 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 **132 dedicated adapters** and **5 universal fallbacks**
|
|
17
17
|
|
|
18
18
|
<sub>207 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 3,
|
|
158
|
+
The carrier folders record 3,704 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
|
@@ -6249,7 +6249,9 @@
|
|
|
6249
6249
|
"timezone": "Europe/Athens",
|
|
6250
6250
|
"tracking": {
|
|
6251
6251
|
"mode": "automatic",
|
|
6252
|
-
"adapter": "
|
|
6252
|
+
"adapter": "geniki-taxydromiki",
|
|
6253
|
+
"recognitionRank": 58,
|
|
6254
|
+
"localClocks": true
|
|
6253
6255
|
},
|
|
6254
6256
|
"canaryUrl": "https://taxydromiki.com/track/",
|
|
6255
6257
|
"trackingUrlTemplate": "https://taxydromiki.com/track/{trackingNumber}",
|
|
@@ -6325,10 +6327,12 @@
|
|
|
6325
6327
|
"timezone": "Europe/Rome",
|
|
6326
6328
|
"tracking": {
|
|
6327
6329
|
"mode": "automatic",
|
|
6328
|
-
"adapter": "
|
|
6330
|
+
"adapter": "gls-it",
|
|
6331
|
+
"recognitionRank": 59,
|
|
6332
|
+
"localClocks": true
|
|
6329
6333
|
},
|
|
6330
6334
|
"canaryUrl": "https://gls-group.com/IT/it/servizi-online/ricerca-spedizioni/",
|
|
6331
|
-
"trackingUrlTemplate": "https://gls-group.com/IT/it/servizi-online/ricerca-spedizioni/?match={trackingNumber}",
|
|
6335
|
+
"trackingUrlTemplate": "https://gls-group.com/IT/it/servizi-online/ricerca-spedizioni/?match={trackingNumber}&type=NAT",
|
|
6332
6336
|
"trackingSearchUrl": "https://gls-group.com/IT/it/servizi-online/ricerca-spedizioni/",
|
|
6333
6337
|
"linkRules": [
|
|
6334
6338
|
{
|
|
@@ -2278,6 +2278,14 @@
|
|
|
2278
2278
|
"brt"
|
|
2279
2279
|
]
|
|
2280
2280
|
},
|
|
2281
|
+
{
|
|
2282
|
+
"input": "01613j353188",
|
|
2283
|
+
"carrier": "mrw",
|
|
2284
|
+
"confidence": "high",
|
|
2285
|
+
"candidates": [
|
|
2286
|
+
"mrw"
|
|
2287
|
+
]
|
|
2288
|
+
},
|
|
2281
2289
|
{
|
|
2282
2290
|
"input": "0162534770",
|
|
2283
2291
|
"carrier": "unknown",
|
|
@@ -2620,6 +2628,30 @@
|
|
|
2620
2628
|
"mrw"
|
|
2621
2629
|
]
|
|
2622
2630
|
},
|
|
2631
|
+
{
|
|
2632
|
+
"input": "02680J332288",
|
|
2633
|
+
"carrier": "mrw",
|
|
2634
|
+
"confidence": "high",
|
|
2635
|
+
"candidates": [
|
|
2636
|
+
"mrw"
|
|
2637
|
+
]
|
|
2638
|
+
},
|
|
2639
|
+
{
|
|
2640
|
+
"input": "02680j313623",
|
|
2641
|
+
"carrier": "mrw",
|
|
2642
|
+
"confidence": "high",
|
|
2643
|
+
"candidates": [
|
|
2644
|
+
"mrw"
|
|
2645
|
+
]
|
|
2646
|
+
},
|
|
2647
|
+
{
|
|
2648
|
+
"input": "02692I374753",
|
|
2649
|
+
"carrier": "mrw",
|
|
2650
|
+
"confidence": "high",
|
|
2651
|
+
"candidates": [
|
|
2652
|
+
"mrw"
|
|
2653
|
+
]
|
|
2654
|
+
},
|
|
2623
2655
|
{
|
|
2624
2656
|
"input": "02692M027981",
|
|
2625
2657
|
"carrier": "mrw",
|
|
@@ -2644,6 +2676,30 @@
|
|
|
2644
2676
|
"mrw"
|
|
2645
2677
|
]
|
|
2646
2678
|
},
|
|
2679
|
+
{
|
|
2680
|
+
"input": "02692S954563",
|
|
2681
|
+
"carrier": "mrw",
|
|
2682
|
+
"confidence": "high",
|
|
2683
|
+
"candidates": [
|
|
2684
|
+
"mrw"
|
|
2685
|
+
]
|
|
2686
|
+
},
|
|
2687
|
+
{
|
|
2688
|
+
"input": "02692T001203",
|
|
2689
|
+
"carrier": "mrw",
|
|
2690
|
+
"confidence": "high",
|
|
2691
|
+
"candidates": [
|
|
2692
|
+
"mrw"
|
|
2693
|
+
]
|
|
2694
|
+
},
|
|
2695
|
+
{
|
|
2696
|
+
"input": "02695F393326",
|
|
2697
|
+
"carrier": "mrw",
|
|
2698
|
+
"confidence": "high",
|
|
2699
|
+
"candidates": [
|
|
2700
|
+
"mrw"
|
|
2701
|
+
]
|
|
2702
|
+
},
|
|
2647
2703
|
{
|
|
2648
2704
|
"input": "02720F088602",
|
|
2649
2705
|
"carrier": "mrw",
|
|
@@ -3025,6 +3081,14 @@
|
|
|
3025
3081
|
"mrw"
|
|
3026
3082
|
]
|
|
3027
3083
|
},
|
|
3084
|
+
{
|
|
3085
|
+
"input": "04316F631075",
|
|
3086
|
+
"carrier": "mrw",
|
|
3087
|
+
"confidence": "high",
|
|
3088
|
+
"candidates": [
|
|
3089
|
+
"mrw"
|
|
3090
|
+
]
|
|
3091
|
+
},
|
|
3028
3092
|
{
|
|
3029
3093
|
"input": "04319F171575",
|
|
3030
3094
|
"carrier": "mrw",
|
|
@@ -3707,6 +3771,14 @@
|
|
|
3707
3771
|
"mrw"
|
|
3708
3772
|
]
|
|
3709
3773
|
},
|
|
3774
|
+
{
|
|
3775
|
+
"input": "08080f060880",
|
|
3776
|
+
"carrier": "mrw",
|
|
3777
|
+
"confidence": "high",
|
|
3778
|
+
"candidates": [
|
|
3779
|
+
"mrw"
|
|
3780
|
+
]
|
|
3781
|
+
},
|
|
3710
3782
|
{
|
|
3711
3783
|
"input": "081730000038942441",
|
|
3712
3784
|
"carrier": "unknown",
|
|
@@ -3809,6 +3881,14 @@
|
|
|
3809
3881
|
"nacex"
|
|
3810
3882
|
]
|
|
3811
3883
|
},
|
|
3884
|
+
{
|
|
3885
|
+
"input": "08420F316215",
|
|
3886
|
+
"carrier": "mrw",
|
|
3887
|
+
"confidence": "high",
|
|
3888
|
+
"candidates": [
|
|
3889
|
+
"mrw"
|
|
3890
|
+
]
|
|
3891
|
+
},
|
|
3812
3892
|
{
|
|
3813
3893
|
"input": "08448877339657",
|
|
3814
3894
|
"carrier": "unknown",
|
|
@@ -24609,6 +24689,9 @@
|
|
|
24609
24689
|
"confidence": "low",
|
|
24610
24690
|
"candidates": [
|
|
24611
24691
|
"gls-it"
|
|
24692
|
+
],
|
|
24693
|
+
"asked": [
|
|
24694
|
+
"gls-it"
|
|
24612
24695
|
]
|
|
24613
24696
|
},
|
|
24614
24697
|
{
|
|
@@ -24617,6 +24700,9 @@
|
|
|
24617
24700
|
"confidence": "low",
|
|
24618
24701
|
"candidates": [
|
|
24619
24702
|
"gls-it"
|
|
24703
|
+
],
|
|
24704
|
+
"asked": [
|
|
24705
|
+
"gls-it"
|
|
24620
24706
|
]
|
|
24621
24707
|
},
|
|
24622
24708
|
{
|
|
@@ -25913,6 +25999,9 @@
|
|
|
25913
25999
|
"confidence": "low",
|
|
25914
26000
|
"candidates": [
|
|
25915
26001
|
"gls-it"
|
|
26002
|
+
],
|
|
26003
|
+
"asked": [
|
|
26004
|
+
"gls-it"
|
|
25916
26005
|
]
|
|
25917
26006
|
},
|
|
25918
26007
|
{
|
|
@@ -31892,6 +31981,9 @@
|
|
|
31892
31981
|
"confidence": "low",
|
|
31893
31982
|
"candidates": [
|
|
31894
31983
|
"gls-it"
|
|
31984
|
+
],
|
|
31985
|
+
"asked": [
|
|
31986
|
+
"gls-it"
|
|
31895
31987
|
]
|
|
31896
31988
|
},
|
|
31897
31989
|
{
|
|
@@ -35219,6 +35311,9 @@
|
|
|
35219
35311
|
"confidence": "low",
|
|
35220
35312
|
"candidates": [
|
|
35221
35313
|
"gls-it"
|
|
35314
|
+
],
|
|
35315
|
+
"asked": [
|
|
35316
|
+
"gls-it"
|
|
35222
35317
|
]
|
|
35223
35318
|
},
|
|
35224
35319
|
{
|
package/dist/browser/scraper.js
CHANGED
|
@@ -5014,7 +5014,9 @@ var ye = {
|
|
|
5014
5014
|
timezone: "Europe/Athens",
|
|
5015
5015
|
tracking: {
|
|
5016
5016
|
mode: "automatic",
|
|
5017
|
-
adapter: "
|
|
5017
|
+
adapter: "geniki-taxydromiki",
|
|
5018
|
+
recognitionRank: 58,
|
|
5019
|
+
localClocks: !0
|
|
5018
5020
|
},
|
|
5019
5021
|
canaryUrl: "https://taxydromiki.com/track/",
|
|
5020
5022
|
trackingUrlTemplate: "https://taxydromiki.com/track/{trackingNumber}",
|
|
@@ -5067,10 +5069,12 @@ var ye = {
|
|
|
5067
5069
|
timezone: "Europe/Rome",
|
|
5068
5070
|
tracking: {
|
|
5069
5071
|
mode: "automatic",
|
|
5070
|
-
adapter: "
|
|
5072
|
+
adapter: "gls-it",
|
|
5073
|
+
recognitionRank: 59,
|
|
5074
|
+
localClocks: !0
|
|
5071
5075
|
},
|
|
5072
5076
|
canaryUrl: "https://gls-group.com/IT/it/servizi-online/ricerca-spedizioni/",
|
|
5073
|
-
trackingUrlTemplate: "https://gls-group.com/IT/it/servizi-online/ricerca-spedizioni/?match={trackingNumber}",
|
|
5077
|
+
trackingUrlTemplate: "https://gls-group.com/IT/it/servizi-online/ricerca-spedizioni/?match={trackingNumber}&type=NAT",
|
|
5074
5078
|
trackingSearchUrl: "https://gls-group.com/IT/it/servizi-online/ricerca-spedizioni/",
|
|
5075
5079
|
linkRules: [{
|
|
5076
5080
|
domains: ["gls-group.com", "gls-group.eu"],
|
|
@@ -11713,9 +11717,11 @@ var _c = {
|
|
|
11713
11717
|
"fan-courier": 51,
|
|
11714
11718
|
"four-px": 13,
|
|
11715
11719
|
foxpost: 52,
|
|
11720
|
+
"geniki-taxydromiki": 58,
|
|
11716
11721
|
"gls-ch": 50,
|
|
11717
11722
|
"gls-de": 60,
|
|
11718
11723
|
"gls-fr": 61,
|
|
11724
|
+
"gls-it": 59,
|
|
11719
11725
|
"gofo-fr": 3,
|
|
11720
11726
|
"gofo-it": 2,
|
|
11721
11727
|
"hermes-de": 40,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type AdapterFactory, type TrackingContext } from '../../core/adapter/index.js';
|
|
2
|
+
import { type StepRecorder } from '../../core/telemetry/index.js';
|
|
3
|
+
export declare class GenikiTracker {
|
|
4
|
+
private readonly options;
|
|
5
|
+
constructor(options?: {
|
|
6
|
+
fetcher?: typeof fetch;
|
|
7
|
+
recorder?: StepRecorder;
|
|
8
|
+
userAgent?: string;
|
|
9
|
+
});
|
|
10
|
+
fetch(raw: string, context?: TrackingContext): Promise<import("../../index.js").CarrierResult>;
|
|
11
|
+
}
|
|
12
|
+
export declare const adapter: AdapterFactory;
|
|
@@ -0,0 +1,51 @@
|
|
|
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 { NOOP_RECORDER } from '../../core/telemetry/index.js';
|
|
5
|
+
import { decodeText, fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
|
|
6
|
+
import { normalizeGenikiNumber, parseGeniki, PROVIDER } from './parser.js';
|
|
7
|
+
/** The tracking page's own lookup, which relays Geniki's voucher web service. */
|
|
8
|
+
const ENDPOINT = 'https://taxydromiki.com/external_scripts/track-site.php';
|
|
9
|
+
// Cloudflare's interstitial, served with a 200 or a 403.
|
|
10
|
+
const CHALLENGE = /<title>\s*(?:Just a moment|Attention Required)|\bcf[-_]chl|challenge-platform/i;
|
|
11
|
+
export class GenikiTracker {
|
|
12
|
+
options;
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
async fetch(raw, context = {}) {
|
|
17
|
+
const number = normalizeGenikiNumber(raw);
|
|
18
|
+
return runSteps({ carrier: 'geniki-taxydromiki', budgetMs: context.budgetMs ?? 15_000, signal: context.signal,
|
|
19
|
+
recorder: this.options.recorder ?? NOOP_RECORDER }, [{ id: 'direct', run: async ({ signal, remainingMs }) => {
|
|
20
|
+
signal.throwIfAborted();
|
|
21
|
+
const { response, bytes } = await fetchBounded(ENDPOINT, { method: 'POST', signal,
|
|
22
|
+
// The page sends a JSON body under a form content type; English words the checkpoints in English.
|
|
23
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded',
|
|
24
|
+
'User-Agent': userAgentOf(this.options.userAgent) },
|
|
25
|
+
body: JSON.stringify({ lang: 'en', voucherNo: number }),
|
|
26
|
+
}, { provider: PROVIDER, timeoutMs: Math.max(1, Math.floor(remainingMs)), maxBytes: 1_000_000,
|
|
27
|
+
allowHttpStatuses: [404, 410], fetcher: this.options.fetcher });
|
|
28
|
+
// A missing lookup script says nothing about the parcel.
|
|
29
|
+
if (response.status !== 200)
|
|
30
|
+
throw new IndeterminateError(PROVIDER, 'Geniki Taxydromiki tracking is unavailable');
|
|
31
|
+
const text = decodeText(bytes.subarray(0, 4_096));
|
|
32
|
+
if (CHALLENGE.test(text))
|
|
33
|
+
throw new ChallengeError(PROVIDER, 'Geniki Taxydromiki asked for a browser check');
|
|
34
|
+
if (!text.trim())
|
|
35
|
+
throw new IndeterminateError(PROVIDER, 'Geniki Taxydromiki returned an empty reply');
|
|
36
|
+
let payload;
|
|
37
|
+
try {
|
|
38
|
+
payload = parseJsonBytes(bytes, PROVIDER);
|
|
39
|
+
}
|
|
40
|
+
catch (cause) {
|
|
41
|
+
throw new SchemaError(PROVIDER, 'Geniki Taxydromiki returned invalid tracking JSON', { cause });
|
|
42
|
+
}
|
|
43
|
+
return parseGeniki(payload, number);
|
|
44
|
+
} }]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export const adapter = environment => {
|
|
48
|
+
const tracker = new GenikiTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
|
|
49
|
+
return { id: 'geniki-taxydromiki', recordsSteps: true, steps: ['direct'], track: (input, context) => tracker.fetch(input.number, context),
|
|
50
|
+
recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizeGenikiNumber(number))) };
|
|
51
|
+
};
|
|
@@ -18,18 +18,51 @@
|
|
|
18
18
|
"timezone": "Europe/Athens",
|
|
19
19
|
"tracking": {
|
|
20
20
|
"mode": "automatic",
|
|
21
|
-
"adapter": "
|
|
22
|
-
"steps": [
|
|
21
|
+
"adapter": "geniki-taxydromiki",
|
|
22
|
+
"steps": [
|
|
23
|
+
"direct"
|
|
24
|
+
],
|
|
25
|
+
"recognition": {
|
|
26
|
+
"rank": 58
|
|
27
|
+
},
|
|
28
|
+
"localClocks": true
|
|
23
29
|
},
|
|
24
|
-
"capabilities": [
|
|
30
|
+
"capabilities": [
|
|
31
|
+
"history",
|
|
32
|
+
"location",
|
|
33
|
+
"provider_code"
|
|
34
|
+
],
|
|
25
35
|
"portal": {
|
|
26
36
|
"url": "https://taxydromiki.com/track/{trackingNumber}",
|
|
27
37
|
"searchUrl": "https://taxydromiki.com/track/",
|
|
28
38
|
"canaryUrl": "https://taxydromiki.com/track/",
|
|
29
|
-
"shows": [
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
"shows": [
|
|
40
|
+
"status",
|
|
41
|
+
"history",
|
|
42
|
+
"location",
|
|
43
|
+
"milestones",
|
|
44
|
+
"recipient_name"
|
|
45
|
+
],
|
|
46
|
+
"scraped": [
|
|
47
|
+
"status",
|
|
48
|
+
"history",
|
|
49
|
+
"location",
|
|
50
|
+
"provider_code"
|
|
51
|
+
],
|
|
52
|
+
"discarded": [
|
|
53
|
+
"milestones",
|
|
54
|
+
"recipient_name",
|
|
55
|
+
"coordinates",
|
|
56
|
+
"delivery_detail",
|
|
57
|
+
"return_number"
|
|
58
|
+
],
|
|
59
|
+
"unavailable": [
|
|
60
|
+
"eta",
|
|
61
|
+
"pickup_point",
|
|
62
|
+
"sender_name",
|
|
63
|
+
"weight",
|
|
64
|
+
"offset_timestamps"
|
|
65
|
+
]
|
|
33
66
|
},
|
|
34
67
|
"links": [
|
|
35
68
|
{
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "full_page"
|
|
41
41
|
},
|
|
42
42
|
"context": {
|
|
43
|
-
"assessment": "
|
|
43
|
+
"assessment": "confirmed"
|
|
44
44
|
},
|
|
45
45
|
"expect": {
|
|
46
46
|
"carrier": "unknown",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"dhl-ecommerce-es"
|
|
56
56
|
]
|
|
57
57
|
},
|
|
58
|
-
"note": "ParcelPanel's Geniki Taxydromiki page offered it as a sample to try on 11 Oct 2026;
|
|
58
|
+
"note": "ParcelPanel's Geniki Taxydromiki page offered it as a sample to try on 11 Oct 2026; Geniki Taxydromiki's tracking returns its history."
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
"number": "5191585066",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"access": "full_page"
|
|
69
69
|
},
|
|
70
70
|
"context": {
|
|
71
|
-
"assessment": "
|
|
71
|
+
"assessment": "confirmed"
|
|
72
72
|
},
|
|
73
73
|
"expect": {
|
|
74
74
|
"carrier": "unknown",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"dhl-ecommerce-es"
|
|
84
84
|
]
|
|
85
85
|
},
|
|
86
|
-
"note": "ParcelPanel's Geniki Taxydromiki page offered it as a sample to try on 11 Oct 2026;
|
|
86
|
+
"note": "ParcelPanel's Geniki Taxydromiki page offered it as a sample to try on 11 Oct 2026; Geniki Taxydromiki's tracking returns its history."
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
89
|
"number": "5191263103",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"access": "full_page"
|
|
97
97
|
},
|
|
98
98
|
"context": {
|
|
99
|
-
"assessment": "
|
|
99
|
+
"assessment": "confirmed"
|
|
100
100
|
},
|
|
101
101
|
"expect": {
|
|
102
102
|
"carrier": "unknown",
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"dhl-ecommerce-es"
|
|
112
112
|
]
|
|
113
113
|
},
|
|
114
|
-
"note": "ParcelPanel's Geniki Taxydromiki page offered it as a sample to try on 11 Oct 2026;
|
|
114
|
+
"note": "ParcelPanel's Geniki Taxydromiki page offered it as a sample to try on 11 Oct 2026; Geniki Taxydromiki's tracking returns its history."
|
|
115
115
|
},
|
|
116
116
|
{
|
|
117
117
|
"number": "5182618604",
|
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
"access": "full_page"
|
|
125
125
|
},
|
|
126
126
|
"context": {
|
|
127
|
-
"assessment": "
|
|
127
|
+
"assessment": "confirmed"
|
|
128
128
|
},
|
|
129
129
|
"expect": {
|
|
130
130
|
"carrier": "unknown",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"dhl-ecommerce-es"
|
|
140
140
|
]
|
|
141
141
|
},
|
|
142
|
-
"note": "ParcelPanel's Geniki Taxydromiki page offered it as a sample to try on 11 Oct 2026;
|
|
142
|
+
"note": "ParcelPanel's Geniki Taxydromiki page offered it as a sample to try on 11 Oct 2026; Geniki Taxydromiki's tracking returns its history."
|
|
143
143
|
},
|
|
144
144
|
{
|
|
145
145
|
"number": "5191585011",
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
"access": "full_page"
|
|
153
153
|
},
|
|
154
154
|
"context": {
|
|
155
|
-
"assessment": "
|
|
155
|
+
"assessment": "confirmed"
|
|
156
156
|
},
|
|
157
157
|
"expect": {
|
|
158
158
|
"carrier": "unknown",
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
"dhl-ecommerce-es"
|
|
168
168
|
]
|
|
169
169
|
},
|
|
170
|
-
"note": "ParcelPanel's Geniki Taxydromiki page offered it as a sample to try on 11 Oct 2026;
|
|
170
|
+
"note": "ParcelPanel's Geniki Taxydromiki page offered it as a sample to try on 11 Oct 2026; Geniki Taxydromiki's tracking returns its history."
|
|
171
171
|
},
|
|
172
172
|
{
|
|
173
173
|
"number": "5191590950",
|
|
@@ -180,7 +180,7 @@
|
|
|
180
180
|
"access": "full_page"
|
|
181
181
|
},
|
|
182
182
|
"context": {
|
|
183
|
-
"assessment": "
|
|
183
|
+
"assessment": "confirmed"
|
|
184
184
|
},
|
|
185
185
|
"expect": {
|
|
186
186
|
"carrier": "unknown",
|
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
"dhl-ecommerce-es"
|
|
196
196
|
]
|
|
197
197
|
},
|
|
198
|
-
"note": "ParcelPanel's Geniki Taxydromiki page offered it as a sample to try on 11 Oct 2026;
|
|
198
|
+
"note": "ParcelPanel's Geniki Taxydromiki page offered it as a sample to try on 11 Oct 2026; Geniki Taxydromiki's tracking returns its history."
|
|
199
199
|
},
|
|
200
200
|
{
|
|
201
201
|
"number": "5182251804",
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
"access": "full_page"
|
|
209
209
|
},
|
|
210
210
|
"context": {
|
|
211
|
-
"assessment": "
|
|
211
|
+
"assessment": "confirmed"
|
|
212
212
|
},
|
|
213
213
|
"expect": {
|
|
214
214
|
"carrier": "unknown",
|
|
@@ -223,7 +223,7 @@
|
|
|
223
223
|
"dhl-ecommerce-es"
|
|
224
224
|
]
|
|
225
225
|
},
|
|
226
|
-
"note": "ParcelPanel's Geniki Taxydromiki page offered it as a sample to try on 11 Oct 2026;
|
|
226
|
+
"note": "ParcelPanel's Geniki Taxydromiki page offered it as a sample to try on 11 Oct 2026; Geniki Taxydromiki's tracking returns its history."
|
|
227
227
|
},
|
|
228
228
|
{
|
|
229
229
|
"number": "2974623013",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CarrierResult } from '../../core/result/index.js';
|
|
2
|
+
export declare const PROVIDER = "Geniki Taxydromiki";
|
|
3
|
+
/** A ten-digit voucher, the only shape Geniki Taxydromiki's tracker, clients and shipments show. */
|
|
4
|
+
export declare function normalizeGenikiNumber(raw: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* The tracker's reply for one voucher: its checkpoints, oldest first, each with
|
|
7
|
+
* its code, wording, service point and wall clock. The name of whoever received
|
|
8
|
+
* the parcel, the scanning device's coordinates, the return voucher and the
|
|
9
|
+
* delivery summary are never read.
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseGeniki(payload: unknown, raw: string): CarrierResult;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { DateTime } from 'luxon';
|
|
2
|
+
import { normalizeTrackingNumber } from '../../core/detection/index.js';
|
|
3
|
+
import { IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
|
|
4
|
+
import { languageStageStatus } from '../../core/status/index.js';
|
|
5
|
+
import { clean } from '../../core/transport/index.js';
|
|
6
|
+
import { isRecord } from '../../core/types.js';
|
|
7
|
+
import { genikiStage } from './status.js';
|
|
8
|
+
export const PROVIDER = 'Geniki Taxydromiki';
|
|
9
|
+
const MAX_EVENTS = 500;
|
|
10
|
+
// The voucher web service's result codes, which the tracker passes on.
|
|
11
|
+
const FOUND = 0;
|
|
12
|
+
const ABSENT = 9;
|
|
13
|
+
// Where a scan names the sender or the recipient instead of a service point.
|
|
14
|
+
const ROLES = new Set(['SHIPPER', 'RECIPIENT', 'ΑΠΟΣΤΟΛΕΑΣ', 'ΠΑΡΑΛΗΠΤΗΣ']);
|
|
15
|
+
// The tracking page hides a scan and the scan that cancels it, the last such pair of each kind.
|
|
16
|
+
const CANCELLED = [['C_L1', 'C_L2'], ['C_E1', 'C_E2'], ['C_K8', 'C_K9']];
|
|
17
|
+
/** A ten-digit voucher, the only shape Geniki Taxydromiki's tracker, clients and shipments show. */
|
|
18
|
+
export function normalizeGenikiNumber(raw) {
|
|
19
|
+
const number = normalizeTrackingNumber(raw);
|
|
20
|
+
if (!/^\d{10}$/.test(number))
|
|
21
|
+
throw new InvalidInputError(PROVIDER, 'Geniki Taxydromiki requires a ten-digit voucher number');
|
|
22
|
+
return number;
|
|
23
|
+
}
|
|
24
|
+
/** A scan's wall clock. The reply gives no offset, so the clock stays local; fractions of a second are dropped. */
|
|
25
|
+
function clock(raw) {
|
|
26
|
+
if (raw != null && typeof raw !== 'string')
|
|
27
|
+
throw new SchemaError(PROVIDER, 'Geniki Taxydromiki returned an invalid scan clock');
|
|
28
|
+
const text = clean(raw, 40);
|
|
29
|
+
const wall = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.\d{1,7})?$/.exec(text)?.[1];
|
|
30
|
+
const parsed = wall ? DateTime.fromISO(wall, { zone: 'UTC' }) : null;
|
|
31
|
+
// The web service's empty date is a year-1 or 1900 placeholder.
|
|
32
|
+
if (parsed?.isValid && parsed.year < 2000)
|
|
33
|
+
return {};
|
|
34
|
+
return parsed?.isValid ? { local_time: wall } : text ? { provider_time_text: text } : {};
|
|
35
|
+
}
|
|
36
|
+
function cancelledRows(codes) {
|
|
37
|
+
const hidden = new Set();
|
|
38
|
+
for (const [scan, cancellation] of CANCELLED) {
|
|
39
|
+
let pair;
|
|
40
|
+
codes.forEach((code, index) => {
|
|
41
|
+
const later = code === scan ? codes.indexOf(cancellation, index + 1) : -1;
|
|
42
|
+
if (later !== -1)
|
|
43
|
+
pair = [index, later];
|
|
44
|
+
});
|
|
45
|
+
if (pair)
|
|
46
|
+
pair.forEach(index => hidden.add(index));
|
|
47
|
+
}
|
|
48
|
+
return hidden;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The tracker's reply for one voucher: its checkpoints, oldest first, each with
|
|
52
|
+
* its code, wording, service point and wall clock. The name of whoever received
|
|
53
|
+
* the parcel, the scanning device's coordinates, the return voucher and the
|
|
54
|
+
* delivery summary are never read.
|
|
55
|
+
*/
|
|
56
|
+
export function parseGeniki(payload, raw) {
|
|
57
|
+
const number = normalizeGenikiNumber(raw);
|
|
58
|
+
if (!isRecord(payload))
|
|
59
|
+
throw new SchemaError(PROVIDER);
|
|
60
|
+
// The tracker's own refusal, such as a missing number, or its expired service key.
|
|
61
|
+
if (payload.success === false || payload.invalid_token !== undefined) {
|
|
62
|
+
throw new IndeterminateError(PROVIDER, 'Geniki Taxydromiki tracking did not answer');
|
|
63
|
+
}
|
|
64
|
+
if (payload.success !== true)
|
|
65
|
+
throw new SchemaError(PROVIDER);
|
|
66
|
+
const keys = Object.keys(payload).filter(key => key !== 'success');
|
|
67
|
+
if (keys.length !== 1 || keys[0] !== number)
|
|
68
|
+
throw new SchemaError(PROVIDER, 'Geniki Taxydromiki answered for a different voucher');
|
|
69
|
+
const entry = payload[number];
|
|
70
|
+
const reply = isRecord(entry) ? entry.TrackAndTraceResult : undefined;
|
|
71
|
+
if (!isRecord(reply) || typeof reply.Result !== 'number' || !Number.isInteger(reply.Result)) {
|
|
72
|
+
throw new SchemaError(PROVIDER, 'Geniki Taxydromiki returned no tracking result');
|
|
73
|
+
}
|
|
74
|
+
if (reply.Result === ABSENT)
|
|
75
|
+
throw new NotFoundError(PROVIDER);
|
|
76
|
+
// 10 withholds a voucher from the tracker's account and 11 is its expired
|
|
77
|
+
// session: neither says anything about the parcel.
|
|
78
|
+
if (reply.Result !== FOUND)
|
|
79
|
+
throw new IndeterminateError(PROVIDER, `Geniki Taxydromiki answered with result ${reply.Result}`);
|
|
80
|
+
if (!isRecord(reply.Checkpoints))
|
|
81
|
+
throw new SchemaError(PROVIDER, 'Geniki Taxydromiki returned invalid checkpoints');
|
|
82
|
+
const listed = reply.Checkpoints.Checkpoint;
|
|
83
|
+
// The web service collapses a single checkpoint into an object.
|
|
84
|
+
const rows = listed === undefined ? [] : Array.isArray(listed) ? listed : [listed];
|
|
85
|
+
if (rows.length > MAX_EVENTS)
|
|
86
|
+
throw new SchemaError(PROVIDER, 'Geniki Taxydromiki returned invalid checkpoints');
|
|
87
|
+
// The page shows its not-found notice for a voucher without checkpoints, but the
|
|
88
|
+
// service found the voucher: that proves neither a parcel on its way nor absence.
|
|
89
|
+
if (!rows.length)
|
|
90
|
+
throw new IndeterminateError(PROVIDER, 'Geniki Taxydromiki returned no checkpoints for the voucher');
|
|
91
|
+
const scans = rows.map(row => {
|
|
92
|
+
if (!isRecord(row))
|
|
93
|
+
throw new SchemaError(PROVIDER, 'Geniki Taxydromiki returned an invalid checkpoint');
|
|
94
|
+
const code = typeof row.StatusCode === 'string' ? row.StatusCode.trim() : '';
|
|
95
|
+
const description = clean(row.Status, 300);
|
|
96
|
+
if (!/^C_[A-Z0-9_]{1,16}$/.test(code) || !description) {
|
|
97
|
+
throw new SchemaError(PROVIDER, 'Geniki Taxydromiki returned a checkpoint without its code or wording');
|
|
98
|
+
}
|
|
99
|
+
if (row.Shop != null && typeof row.Shop !== 'string')
|
|
100
|
+
throw new SchemaError(PROVIDER, 'Geniki Taxydromiki returned an invalid service point');
|
|
101
|
+
const shop = clean(row.Shop, 100);
|
|
102
|
+
return { code, description, location: ROLES.has(shop.toUpperCase()) ? '' : shop, time: clock(row.StatusDate) };
|
|
103
|
+
});
|
|
104
|
+
const hidden = cancelledRows(scans.map(scan => scan.code));
|
|
105
|
+
const events = [];
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
// Newest first; equal clocks keep the reply's order.
|
|
108
|
+
for (const [index, scan] of [...scans.entries()].reverse()) {
|
|
109
|
+
if (hidden.has(index))
|
|
110
|
+
continue;
|
|
111
|
+
const stage = genikiStage(scan.code);
|
|
112
|
+
const event = { description: scan.description, provider_code: scan.code, ...scan.time,
|
|
113
|
+
...(scan.location ? { location: scan.location } : {}), ...(stage ? { stage } : {}) };
|
|
114
|
+
const key = JSON.stringify(event);
|
|
115
|
+
if (!seen.has(key)) {
|
|
116
|
+
seen.add(key);
|
|
117
|
+
events.push(event);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (!events.length)
|
|
121
|
+
throw new IndeterminateError(PROVIDER, 'Geniki Taxydromiki returned only cancelled checkpoints');
|
|
122
|
+
const latest = events[0];
|
|
123
|
+
const stage = genikiStage(latest.provider_code);
|
|
124
|
+
return { status: stage ? languageStageStatus(stage) : 'unknown', ...(stage ? { current_stage: stage } : {}),
|
|
125
|
+
last_status_text: latest.description ?? null, last_update: null, last_update_local: latest.local_time ?? null,
|
|
126
|
+
expected_delivery: null, events };
|
|
127
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Stage } from '../../core/status/index.js';
|
|
2
|
+
import type { CarrierStatusMap } from '../../core/status/statusMap.js';
|
|
3
|
+
/** The stage of one checkpoint code, if the map gives it one. */
|
|
4
|
+
export declare function genikiStage(code: string): Stage | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* What the map says about one scan. Every scan carries its code, so wording
|
|
7
|
+
* alone gets nothing. The page's error codes (C_EA_*), cancellations (C_E2,
|
|
8
|
+
* C_L2, C_K9, C_P4, C_S5, C_S8) and the other codes it counts as in progress
|
|
9
|
+
* have no wording any source settles, so they stay unknown, open for review.
|
|
10
|
+
*/
|
|
11
|
+
export declare const statusMap: CarrierStatusMap;
|