universal-parcel-scraper 0.75.0-main.929 → 0.76.0-main.932

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@ The engine behind [Peek](https://peektracker.com) ([GitHub](https://github.com/p
13
13
  [![License: Apache-2.0](https://img.shields.io/badge/core-Apache--2.0-blue.svg)](LICENSE)
14
14
 
15
15
  <!-- GENERATED:summary -->
16
- **3,500+ carriers** through **130 dedicated adapters** and **5 universal fallbacks**
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,675 such statuses, each filed under one stage.
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": "universal"
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": "universal"
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
  {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "catalog": 207,
3
- "activeAdapters": 130,
3
+ "activeAdapters": 132,
4
4
  "countries": 66,
5
5
  "referenceCarriers": 100,
6
6
  "comparison": {
@@ -24609,6 +24609,9 @@
24609
24609
  "confidence": "low",
24610
24610
  "candidates": [
24611
24611
  "gls-it"
24612
+ ],
24613
+ "asked": [
24614
+ "gls-it"
24612
24615
  ]
24613
24616
  },
24614
24617
  {
@@ -24617,6 +24620,9 @@
24617
24620
  "confidence": "low",
24618
24621
  "candidates": [
24619
24622
  "gls-it"
24623
+ ],
24624
+ "asked": [
24625
+ "gls-it"
24620
24626
  ]
24621
24627
  },
24622
24628
  {
@@ -25913,6 +25919,9 @@
25913
25919
  "confidence": "low",
25914
25920
  "candidates": [
25915
25921
  "gls-it"
25922
+ ],
25923
+ "asked": [
25924
+ "gls-it"
25916
25925
  ]
25917
25926
  },
25918
25927
  {
@@ -31892,6 +31901,9 @@
31892
31901
  "confidence": "low",
31893
31902
  "candidates": [
31894
31903
  "gls-it"
31904
+ ],
31905
+ "asked": [
31906
+ "gls-it"
31895
31907
  ]
31896
31908
  },
31897
31909
  {
@@ -35219,6 +35231,9 @@
35219
35231
  "confidence": "low",
35220
35232
  "candidates": [
35221
35233
  "gls-it"
35234
+ ],
35235
+ "asked": [
35236
+ "gls-it"
35222
35237
  ]
35223
35238
  },
35224
35239
  {
@@ -5014,7 +5014,9 @@ var ye = {
5014
5014
  timezone: "Europe/Athens",
5015
5015
  tracking: {
5016
5016
  mode: "automatic",
5017
- adapter: "universal"
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: "universal"
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": "universal",
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
- "scraped": [],
31
- "discarded": [],
32
- "unavailable": []
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": "candidate"
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; it is not described as a live shipment."
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": "candidate"
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; it is not described as a live shipment."
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": "candidate"
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; it is not described as a live shipment."
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": "candidate"
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; it is not described as a live shipment."
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": "candidate"
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; it is not described as a live shipment."
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": "candidate"
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; it is not described as a live shipment."
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": "candidate"
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; it is not described as a live shipment."
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;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Geniki Taxydromiki's checkpoint codes, the same in Greek and English. The
3
+ * wordings come from the tracker's replies. C_SC and C_S2 come only from the
4
+ * tracking page's own code, which files C_SC with C_NW as a voucher the
5
+ * customer created and C_S2 with the deliveries. The shared wording rules
6
+ * leave several of them unstaged, such as a collected parcel.
7
+ */
8
+ const CODES = new Map([
9
+ // The customer created the voucher and printed its label.
10
+ ['C_NW', 'registered'],
11
+ ['C_SC', 'registered'],
12
+ // A courier collected the parcel from its sender.
13
+ ['C_W3', 'accepted'],
14
+ // A service point registered the shipment it was handed: the page shows its
15
+ // date as the collection by Geniki and hides the customer's voucher step.
16
+ ['C_KK', 'accepted'],
17
+ // Departures from a service point, arrivals and departures at hubs, arrivals
18
+ // at a service point and a routing on to another one.
19
+ ['C_K8', 'in_transit'],
20
+ ['C_H1', 'in_transit'],
21
+ ['C_H2', 'in_transit'],
22
+ ['C_A1', 'in_transit'],
23
+ ['C_L1', 'in_transit'],
24
+ // Handed to a courier for delivery ("Χρέωση σε κούριερ").
25
+ ['C_A3', 'out_for_delivery'],
26
+ ['C_W2', 'delivered'],
27
+ // The delivery recorded at the service point, with the parcel marked delivered.
28
+ ['C_P1', 'delivered'],
29
+ ['C_S2', 'delivered'],
30
+ ]);
31
+ /** The stage of one checkpoint code, if the map gives it one. */
32
+ export function genikiStage(code) {
33
+ return CODES.get(code);
34
+ }
35
+ /**
36
+ * What the map says about one scan. Every scan carries its code, so wording
37
+ * alone gets nothing. The page's error codes (C_EA_*), cancellations (C_E2,
38
+ * C_L2, C_K9, C_P4, C_S5, C_S8) and the other codes it counts as in progress
39
+ * have no wording any source settles, so they stay unknown, open for review.
40
+ */
41
+ export const statusMap = {
42
+ stage: code => (code ? genikiStage(code) : undefined),
43
+ gaps: [],
44
+ };