universal-parcel-scraper 0.95.0-main.992 → 0.96.0-main.994

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 **154 dedicated adapters** and **5 universal fallbacks**
16
+ **3,500+ carriers** through **155 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,298 such statuses, each filed under one stage.
158
+ The carrier folders record 4,349 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
@@ -7024,7 +7024,9 @@
7024
7024
  "timezone": "Europe/Vilnius",
7025
7025
  "tracking": {
7026
7026
  "mode": "automatic",
7027
- "adapter": "universal"
7027
+ "adapter": "lietuvos-pastas",
7028
+ "recognitionRank": 860,
7029
+ "localClocks": true
7028
7030
  },
7029
7031
  "canaryUrl": "https://lpexpress.lt/track",
7030
7032
  "trackingUrlTemplate": "https://lpexpress.lt/track?parcel={trackingNumber}",
@@ -7059,6 +7061,12 @@
7059
7061
  {
7060
7062
  "pattern": "^(?:CC|CH|HC)\\d{9}LT$",
7061
7063
  "confidence": "low"
7064
+ },
7065
+ {
7066
+ "pattern": "^[A-Z]{2}\\d{9}LT$",
7067
+ "confidence": "low",
7068
+ "checksum": "s10",
7069
+ "preferred": true
7062
7070
  }
7063
7071
  ]
7064
7072
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "catalog": 208,
3
- "activeAdapters": 154,
3
+ "activeAdapters": 155,
4
4
  "countries": 66,
5
5
  "referenceCarriers": 100,
6
6
  "comparison": {
@@ -24785,6 +24785,7 @@
24785
24785
  "intl-post"
24786
24786
  ],
24787
24787
  "asked": [
24788
+ "lietuvos-pastas",
24788
24789
  "chronopost"
24789
24790
  ]
24790
24791
  },
@@ -25654,6 +25655,7 @@
25654
25655
  "lietuvos-pastas"
25655
25656
  ],
25656
25657
  "asked": [
25658
+ "lietuvos-pastas",
25657
25659
  "chronopost"
25658
25660
  ]
25659
25661
  },
@@ -25666,6 +25668,7 @@
25666
25668
  "lietuvos-pastas"
25667
25669
  ],
25668
25670
  "asked": [
25671
+ "lietuvos-pastas",
25669
25672
  "chronopost"
25670
25673
  ]
25671
25674
  },
@@ -25677,6 +25680,19 @@
25677
25680
  "intl-post"
25678
25681
  ],
25679
25682
  "asked": [
25683
+ "lietuvos-pastas",
25684
+ "chronopost"
25685
+ ]
25686
+ },
25687
+ {
25688
+ "input": "CC929125681LT",
25689
+ "carrier": "intl-post",
25690
+ "confidence": "high",
25691
+ "candidates": [
25692
+ "intl-post"
25693
+ ],
25694
+ "asked": [
25695
+ "lietuvos-pastas",
25680
25696
  "chronopost"
25681
25697
  ]
25682
25698
  },
@@ -26052,6 +26068,7 @@
26052
26068
  "lietuvos-pastas"
26053
26069
  ],
26054
26070
  "asked": [
26071
+ "lietuvos-pastas",
26055
26072
  "chronopost"
26056
26073
  ]
26057
26074
  },
@@ -26064,6 +26081,7 @@
26064
26081
  "lietuvos-pastas"
26065
26082
  ],
26066
26083
  "asked": [
26084
+ "lietuvos-pastas",
26067
26085
  "chronopost"
26068
26086
  ]
26069
26087
  },
@@ -26088,6 +26106,7 @@
26088
26106
  "lietuvos-pastas"
26089
26107
  ],
26090
26108
  "asked": [
26109
+ "lietuvos-pastas",
26091
26110
  "chronopost"
26092
26111
  ]
26093
26112
  },
@@ -27580,6 +27599,7 @@
27580
27599
  "intl-post"
27581
27600
  ],
27582
27601
  "asked": [
27602
+ "lietuvos-pastas",
27583
27603
  "chronopost"
27584
27604
  ]
27585
27605
  },
@@ -29161,6 +29181,20 @@
29161
29181
  "lietuvos-pastas"
29162
29182
  ],
29163
29183
  "asked": [
29184
+ "lietuvos-pastas",
29185
+ "chronopost"
29186
+ ]
29187
+ },
29188
+ {
29189
+ "input": "HC019942965LT",
29190
+ "carrier": "unknown",
29191
+ "confidence": "low",
29192
+ "candidates": [
29193
+ "chronopost",
29194
+ "lietuvos-pastas"
29195
+ ],
29196
+ "asked": [
29197
+ "lietuvos-pastas",
29164
29198
  "chronopost"
29165
29199
  ]
29166
29200
  },
@@ -29173,6 +29207,7 @@
29173
29207
  "lietuvos-pastas"
29174
29208
  ],
29175
29209
  "asked": [
29210
+ "lietuvos-pastas",
29176
29211
  "chronopost"
29177
29212
  ]
29178
29213
  },
@@ -33473,6 +33508,7 @@
33473
33508
  "intl-post"
33474
33509
  ],
33475
33510
  "asked": [
33511
+ "lietuvos-pastas",
33476
33512
  "chronopost"
33477
33513
  ]
33478
33514
  },
@@ -33930,6 +33966,7 @@
33930
33966
  "intl-post"
33931
33967
  ],
33932
33968
  "asked": [
33969
+ "lietuvos-pastas",
33933
33970
  "chronopost"
33934
33971
  ]
33935
33972
  },
@@ -5589,7 +5589,9 @@ var be = {
5589
5589
  timezone: "Europe/Vilnius",
5590
5590
  tracking: {
5591
5591
  mode: "automatic",
5592
- adapter: "universal"
5592
+ adapter: "lietuvos-pastas",
5593
+ recognitionRank: 860,
5594
+ localClocks: !0
5593
5595
  },
5594
5596
  canaryUrl: "https://lpexpress.lt/track",
5595
5597
  trackingUrlTemplate: "https://lpexpress.lt/track?parcel={trackingNumber}",
@@ -5613,6 +5615,11 @@ var be = {
5613
5615
  detectionRules: [{
5614
5616
  pattern: "^(?:CC|CH|HC)\\d{9}LT$",
5615
5617
  confidence: "low"
5618
+ }, {
5619
+ pattern: "^[A-Z]{2}\\d{9}LT$",
5620
+ confidence: "low",
5621
+ checksum: "s10",
5622
+ preferred: !0
5616
5623
  }]
5617
5624
  },
5618
5625
  "loomis-express": {
@@ -7168,7 +7175,7 @@ var be = {
7168
7175
  "kuehne-nagel": ["kuehne-nagel-1", "kuehne-nagel-2"],
7169
7176
  "latvijas-pasts": ["latvijas-pasts-s10"],
7170
7177
  "lbc-express": ["lbc-domestic"],
7171
- "lietuvos-pastas": ["lietuvos-pastas-1"],
7178
+ "lietuvos-pastas": ["lietuvos-pastas-1", "lietuvos-pastas-s10"],
7172
7179
  "loomis-express": ["loomis-express-1", "loomis-express-2"],
7173
7180
  lso: ["lso-1"],
7174
7181
  "magyar-posta": [],
@@ -11962,6 +11969,7 @@ var bc = {
11962
11969
  "j-and-t-cargo": 470,
11963
11970
  "la-poste": 800,
11964
11971
  "latvijas-pasts": 820,
11972
+ "lietuvos-pastas": 860,
11965
11973
  matkahuolto: 690,
11966
11974
  "mng-kargo": 740,
11967
11975
  nacex: 410,
@@ -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 LIETUVOS_PASTAS_ENDPOINT = "https://api-esavitarna.post.lt/v2/tracking";
5
+ export declare function lietuvosPastasUrl(number: string): string;
6
+ export declare class LietuvosPastasTracker {
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,65 @@
1
+ import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
2
+ import { ChallengeError, 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 { LIETUVOS_PASTAS_MAX_BYTES, normalizeLietuvosPastasNumber, parseLietuvosPastas, PROVIDER } from './parser.js';
8
+ // The self-service API behind LP Express's tracking page, which answers
9
+ // without a key, cookie or token. Its second version groups the rows by mail
10
+ // and tells an unknown number from a known one without rows.
11
+ export const LIETUVOS_PASTAS_ENDPOINT = 'https://api-esavitarna.post.lt/v2/tracking';
12
+ // The F5 firewall in front of the API rejects some clients with an HTML page.
13
+ const BLOCKED = /Request Rejected|The requested URL was rejected|support ID|TSPD/i;
14
+ export function lietuvosPastasUrl(number) {
15
+ return `${LIETUVOS_PASTAS_ENDPOINT}/${encodeURIComponent(number)}/events`;
16
+ }
17
+ export class LietuvosPastasTracker {
18
+ options;
19
+ constructor(options = {}) {
20
+ this.options = options;
21
+ }
22
+ async fetch(raw, context = {}) {
23
+ const number = normalizeLietuvosPastasNumber(raw);
24
+ const read = async (signal, timeoutMs) => {
25
+ let bytes;
26
+ try {
27
+ // The language header picks the wording, Lithuanian by default.
28
+ ({ bytes } = await fetchBounded(lietuvosPastasUrl(number), { signal,
29
+ headers: { Accept: 'application/json', 'Accept-Language': 'en', 'User-Agent': userAgentOf(this.options.userAgent) } }, { provider: PROVIDER, timeoutMs: Math.max(1, Math.floor(timeoutMs)), maxBytes: LIETUVOS_PASTAS_MAX_BYTES, fetcher: this.options.fetcher }));
30
+ }
31
+ catch (error) {
32
+ // An unknown number has its own answer; a missing route is not a missing parcel.
33
+ if (error instanceof UpstreamHttpError && [404, 410].includes(error.status)) {
34
+ throw new TransportError(PROVIDER, 'Lietuvos paštas tracking service is unavailable', { cause: error });
35
+ }
36
+ throw error;
37
+ }
38
+ let payload;
39
+ try {
40
+ payload = parseJsonBytes(bytes, PROVIDER);
41
+ }
42
+ catch (cause) {
43
+ if (BLOCKED.test(decodeText(bytes.subarray(0, 64_000))))
44
+ throw new ChallengeError(PROVIDER, 'Lietuvos paštas answered with a block page', { cause });
45
+ throw new SchemaError(PROVIDER, 'Lietuvos paštas returned invalid tracking JSON', { cause });
46
+ }
47
+ return parseLietuvosPastas(payload, number);
48
+ };
49
+ // A request that fails to reach the service, or hangs, gets one more try:
50
+ // the first gets half of the budget, so the retry fits in the rest.
51
+ return runSteps({ carrier: 'lietuvos-pastas', budgetMs: context.budgetMs ?? 15_000, signal: context.signal,
52
+ recorder: this.options.recorder ?? NOOP_RECORDER }, [
53
+ { id: 'direct', run: ({ signal, remainingMs }) => read(signal, firstAttemptMs(remainingMs)) },
54
+ networkRetryStep(({ signal, remainingMs }) => read(signal, remainingMs)),
55
+ ]);
56
+ }
57
+ }
58
+ export const adapter = (environment) => {
59
+ const tracker = new LietuvosPastasTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
60
+ return {
61
+ id: 'lietuvos-pastas', recordsSteps: true, steps: ['direct', 'retry'],
62
+ track: (input, context) => tracker.fetch(input.number, context),
63
+ recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizeLietuvosPastasNumber(number))),
64
+ };
65
+ };
@@ -19,18 +19,56 @@
19
19
  "timezone": "Europe/Vilnius",
20
20
  "tracking": {
21
21
  "mode": "automatic",
22
- "adapter": "universal",
23
- "steps": []
22
+ "adapter": "lietuvos-pastas",
23
+ "steps": [
24
+ "direct",
25
+ "retry"
26
+ ],
27
+ "recognition": {
28
+ "rank": 860
29
+ },
30
+ "localClocks": true
24
31
  },
25
- "capabilities": [],
32
+ "capabilities": [
33
+ "history",
34
+ "location",
35
+ "provider_code",
36
+ "delivered_at",
37
+ "pickup_point"
38
+ ],
26
39
  "portal": {
27
40
  "url": "https://lpexpress.lt/track?parcel={trackingNumber}",
28
41
  "searchUrl": "https://lpexpress.lt/track",
29
42
  "canaryUrl": "https://lpexpress.lt/track",
30
- "shows": [],
31
- "scraped": [],
32
- "discarded": [],
33
- "unavailable": []
43
+ "shows": [
44
+ "status",
45
+ "history",
46
+ "location",
47
+ "delivered_at",
48
+ "pickup_point",
49
+ "pickup_point_address",
50
+ "postcodes",
51
+ "progress_rail"
52
+ ],
53
+ "scraped": [
54
+ "status",
55
+ "history",
56
+ "location",
57
+ "provider_code",
58
+ "delivered_at",
59
+ "pickup_point"
60
+ ],
61
+ "discarded": [
62
+ "pickup_point_address",
63
+ "postcodes",
64
+ "progress_rail"
65
+ ],
66
+ "unavailable": [
67
+ "eta",
68
+ "weight",
69
+ "service_name",
70
+ "proof_of_delivery"
71
+ ]
34
72
  },
35
73
  "links": [
36
74
  {
@@ -63,6 +101,13 @@
63
101
  "id": "lietuvos-pastas-1",
64
102
  "pattern": "^(?:CC|CH|HC)\\d{9}LT$",
65
103
  "confidence": "low"
104
+ },
105
+ {
106
+ "id": "lietuvos-pastas-s10",
107
+ "pattern": "^[A-Z]{2}\\d{9}LT$",
108
+ "confidence": "low",
109
+ "preferred": true,
110
+ "checksum": "s10"
66
111
  }
67
112
  ]
68
113
  }
@@ -155,19 +155,38 @@
155
155
  "role": "shipment",
156
156
  "evidence": "public_shipment_report",
157
157
  "source": {
158
- "url": "https://web.archive.org/web/20250421183500/https://lpexpress.lt/track?parcel=CC928806445LT",
158
+ "url": "https://web.archive.org/web/20250421183509/https://api-esavitarna.post.lt/tracking/CC928806445LT/events",
159
159
  "date": "2025-04-21",
160
160
  "dateKind": "capture",
161
- "access": "indexed_excerpt"
161
+ "access": "full_page"
162
162
  },
163
163
  "context": {
164
- "assessment": "candidate"
164
+ "assessment": "confirmed"
165
165
  },
166
166
  "expect": {
167
167
  "carrier": "intl-post",
168
168
  "confidence": "high"
169
169
  },
170
- "note": "Looked up on LP Express's own tracker, as an archived address shows; the capture was not opened."
170
+ "note": "Lietuvos paštas's own tracking service reply, archived by the Wayback Machine, shows the parcel posted at a parcel locker and collected from it."
171
+ },
172
+ {
173
+ "number": "CC929125681LT",
174
+ "role": "shipment",
175
+ "evidence": "public_shipment_report",
176
+ "source": {
177
+ "url": "https://web.archive.org/web/20250423151126/https://api-esavitarna.post.lt/tracking/CC929125681LT/events",
178
+ "date": "2025-04-23",
179
+ "dateKind": "capture",
180
+ "access": "full_page"
181
+ },
182
+ "context": {
183
+ "assessment": "confirmed"
184
+ },
185
+ "expect": {
186
+ "carrier": "intl-post",
187
+ "confidence": "high"
188
+ },
189
+ "note": "Lietuvos paštas's own tracking service reply, archived by the Wayback Machine, shows the parcel collected from the sender's parcel locker and at a sorting centre."
171
190
  },
172
191
  {
173
192
  "number": "CH800300288LT",
@@ -192,6 +211,29 @@
192
211
  },
193
212
  "note": "Looked up on LP Express's own tracker, as an archived address shows; the capture was not opened."
194
213
  },
214
+ {
215
+ "number": "HC019942965LT",
216
+ "role": "shipment",
217
+ "evidence": "public_shipment_report",
218
+ "source": {
219
+ "url": "https://web.archive.org/web/20251104001421/https://api-esavitarna.post.lt/tracking/HC019942965LT/events",
220
+ "date": "2025-11-04",
221
+ "dateKind": "capture",
222
+ "access": "full_page"
223
+ },
224
+ "context": {
225
+ "assessment": "confirmed"
226
+ },
227
+ "expect": {
228
+ "carrier": "unknown",
229
+ "confidence": "low",
230
+ "candidates": [
231
+ "chronopost",
232
+ "lietuvos-pastas"
233
+ ]
234
+ },
235
+ "note": "Lietuvos paštas's own tracking service reply, archived by the Wayback Machine, shows the parcel through three sorting centres to a parcel locker, then collected."
236
+ },
195
237
  {
196
238
  "number": "CC800855513LT",
197
239
  "role": "shipment",
@@ -220,13 +262,13 @@
220
262
  "role": "shipment",
221
263
  "evidence": "public_shipment_report",
222
264
  "source": {
223
- "url": "https://web.archive.org/web/20260706093449/https://lpexpress.lt/track?parcel=HC025271224LT",
224
- "date": "2026-07-06",
265
+ "url": "https://web.archive.org/web/20260707135945/https://api-esavitarna.post.lt/tracking/HC025271224LT/events",
266
+ "date": "2026-07-07",
225
267
  "dateKind": "capture",
226
- "access": "indexed_excerpt"
268
+ "access": "full_page"
227
269
  },
228
270
  "context": {
229
- "assessment": "candidate"
271
+ "assessment": "confirmed"
230
272
  },
231
273
  "expect": {
232
274
  "carrier": "unknown",
@@ -236,7 +278,7 @@
236
278
  "lietuvos-pastas"
237
279
  ]
238
280
  },
239
- "note": "Looked up on LP Express's own tracker, as an archived address shows; the capture was not opened."
281
+ "note": "Lietuvos paštas's own tracking service reply, archived by the Wayback Machine, shows the parcel accepted and moving between sorting centres."
240
282
  }
241
283
  ]
242
284
  }
@@ -0,0 +1,15 @@
1
+ import type { CarrierResult } from '../../core/result/index.js';
2
+ export declare const PROVIDER = "Lietuvos pa\u0161tas";
3
+ export declare const LIETUVOS_PASTAS_MAX_BYTES = 512000;
4
+ /**
5
+ * Lietuvos paštas's numbers: two letters, nine digits and LT, its own S10
6
+ * items and LP Express parcels (CC, CH and HC, which mostly fail the S10 check
7
+ * digit), and S10 items other posts send to Lithuania.
8
+ */
9
+ export declare function normalizeLietuvosPastasNumber(raw: string): string;
10
+ /**
11
+ * The tracking service's answer for one number: the mail it knows under that
12
+ * number, if any, and its rows, oldest first, each with a public event type,
13
+ * an English wording, a clock, a country and often a place.
14
+ */
15
+ export declare function parseLietuvosPastas(payload: unknown, rawNumber: string): CarrierResult;
@@ -0,0 +1,198 @@
1
+ import { isValidS10TrackingNumber, normalizeTrackingNumber } from '../../core/detection/index.js';
2
+ import { InvalidInputError, NoHistoryError, NotFoundError, SchemaError } from '../../core/errors/index.js';
3
+ import { lastMovement } from '../../core/result/pickup.js';
4
+ import { languageStageStatus } from '../../core/status/index.js';
5
+ import { countryCode, countryTimeZone, explicitOffsetTime, isoTime } from '../../core/time/index.js';
6
+ import { clean } from '../../core/transport/index.js';
7
+ import { isRecord } from '../../core/types.js';
8
+ import { isLietuvosPastasNotice, lietuvosPastasStage, RETURN_STARTS } from './status.js';
9
+ export const PROVIDER = 'Lietuvos paštas';
10
+ // A long history is a few kilobytes.
11
+ export const LIETUVOS_PASTAS_MAX_BYTES = 512_000;
12
+ const MAX_MAILS = 20;
13
+ const MAX_ROWS = 500;
14
+ const MAX_EVENTS = 100;
15
+ const HOME = 'LT';
16
+ const CODE = /^[A-Z][A-Z0-9_]{1,63}$/;
17
+ const GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
18
+ // The answer for a number the service does not know.
19
+ const NO_MAIL = '00000000-0000-0000-0000-000000000000';
20
+ const STAMP = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/;
21
+ // A mail the page leaves out: the label for a customer return, not this parcel's journey.
22
+ const RETURN_MAIL = 'return';
23
+ // The label created online; the self-service site is not a place.
24
+ const SELF_SERVICE = /^savitarna$/i;
25
+ const POSTCODE = /^(?:LT-?)?\d{5}$/i;
26
+ // A parcel locker or post office leads with its kind.
27
+ const POINT = /paštomat|paštas|pašto skyrius|terminal/i;
28
+ const LITHUANIAN_REGIONS = new Intl.DisplayNames(['lt'], { type: 'region' });
29
+ const ENGLISH_REGIONS = new Intl.DisplayNames(['en'], { type: 'region' });
30
+ /**
31
+ * Lietuvos paštas's numbers: two letters, nine digits and LT, its own S10
32
+ * items and LP Express parcels (CC, CH and HC, which mostly fail the S10 check
33
+ * digit), and S10 items other posts send to Lithuania.
34
+ */
35
+ export function normalizeLietuvosPastasNumber(raw) {
36
+ const number = normalizeTrackingNumber(raw);
37
+ if (/^[A-Z]{2}\d{9}LT$/.test(number) || isValidS10TrackingNumber(number))
38
+ return number;
39
+ throw new InvalidInputError(PROVIDER, 'Lietuvos paštas tracks item codes of two letters, nine digits and two letters');
40
+ }
41
+ /**
42
+ * The feed labels every row with Vilnius's offset. Its own rows are Vilnius
43
+ * times; a row from abroad carries that country's wall clock instead, which is
44
+ * read in the country's zone, or kept as a wall clock when it has several.
45
+ */
46
+ function clockOf(value, country) {
47
+ const match = typeof value === 'string' ? STAMP.exec(value.trim()) : null;
48
+ const labeled = match ? explicitOffsetTime(`${match[1]}${match[2]}`) : null;
49
+ if (!match || !labeled)
50
+ return null;
51
+ if (!country || country === HOME)
52
+ return { order: labeled.timestamp, time: labeled.iso };
53
+ const zone = countryTimeZone(country);
54
+ const local = zone ? isoTime(match[1], zone) : null;
55
+ return local ? { order: labeled.timestamp, time: local.iso } : { order: labeled.timestamp, localTime: match[1] };
56
+ }
57
+ /**
58
+ * A row's place: a town, a sorting centre or a country, or a parcel locker or
59
+ * post office with its street, town and postcode. The street and postcode are
60
+ * left out, and so is any other place with several parts, which could be a
61
+ * door address.
62
+ */
63
+ function placeOf(value, country) {
64
+ const parts = clean(value, 300).split(',').map(part => part.trim()).filter(Boolean);
65
+ if (parts.length > 1 && POSTCODE.test(parts.at(-1)))
66
+ parts.pop();
67
+ if (!parts.length)
68
+ return {};
69
+ const [first] = parts;
70
+ if (parts.length === 1) {
71
+ if (SELF_SERVICE.test(first))
72
+ return {};
73
+ if (POINT.test(first))
74
+ return { location: first, point: first };
75
+ if (/\d/.test(first))
76
+ return {};
77
+ // A row from abroad names its country in Lithuanian.
78
+ if (country && country !== HOME && LITHUANIAN_REGIONS.of(country) === first)
79
+ return { location: ENGLISH_REGIONS.of(country) ?? first };
80
+ return { location: first };
81
+ }
82
+ if (!POINT.test(first))
83
+ return {};
84
+ const town = parts.at(-1);
85
+ const name = parts.slice(0, parts.length > 2 ? -2 : 1).join(', ');
86
+ if (/\d/.test(town))
87
+ return { location: name, point: name };
88
+ return { location: `${name}, ${town}`, point: `${name}\n${town}` };
89
+ }
90
+ function rowOf(value, index) {
91
+ if (!isRecord(value))
92
+ throw new SchemaError(PROVIDER, 'Lietuvos paštas returned an invalid row');
93
+ const code = clean(value.publicEventType, 64);
94
+ const raw = typeof value.countryCode === 'string' ? value.countryCode.trim().toUpperCase() : '';
95
+ const country = /^[A-Z]{2}$/.test(raw) ? countryCode(raw) : null;
96
+ const clock = clockOf(value.eventDate, country);
97
+ if (!CODE.test(code) || !clock)
98
+ throw new SchemaError(PROVIDER, 'Lietuvos paštas returned a row without a type or clock');
99
+ return { code, clock, text: clean(value.publicEventText, 300), place: placeOf(value.location, country), index };
100
+ }
101
+ /**
102
+ * The tracking service's answer for one number: the mail it knows under that
103
+ * number, if any, and its rows, oldest first, each with a public event type,
104
+ * an English wording, a clock, a country and often a place.
105
+ */
106
+ export function parseLietuvosPastas(payload, rawNumber) {
107
+ const number = normalizeLietuvosPastasNumber(rawNumber);
108
+ if (!isRecord(payload) || typeof payload.mailGuid !== 'string' || !GUID.test(payload.mailGuid)
109
+ || !Array.isArray(payload.mails) || payload.mails.length > MAX_MAILS) {
110
+ throw new SchemaError(PROVIDER, 'Lietuvos paštas did not return a mail');
111
+ }
112
+ if (typeof payload.mailBarcode !== 'string' || payload.mailBarcode.trim().toUpperCase() !== number) {
113
+ throw new SchemaError(PROVIDER, 'Lietuvos paštas answered for a different item');
114
+ }
115
+ if (payload.mailGuid === NO_MAIL) {
116
+ if (payload.mails.length)
117
+ throw new SchemaError(PROVIDER, 'Lietuvos paštas returned mails without an identifier');
118
+ throw new NotFoundError(PROVIDER);
119
+ }
120
+ const raws = [];
121
+ for (const mail of payload.mails) {
122
+ if (!isRecord(mail) || !Array.isArray(mail.events))
123
+ throw new SchemaError(PROVIDER, 'Lietuvos paštas returned an invalid mail');
124
+ // The tracking page shows every mail but the return label's.
125
+ if (typeof mail.type === 'string' && mail.type.trim().toLowerCase() === RETURN_MAIL)
126
+ continue;
127
+ raws.push(...mail.events);
128
+ }
129
+ if (raws.length > MAX_ROWS)
130
+ throw new SchemaError(PROVIDER, 'Lietuvos paštas returned an invalid history');
131
+ // A mail the service knows with no rows yet, or whose rows it no longer shows.
132
+ if (!raws.length)
133
+ throw new NoHistoryError(PROVIDER);
134
+ const rows = raws.map(rowOf);
135
+ const scans = [];
136
+ const seen = new Set();
137
+ for (const row of rows) {
138
+ const event = {
139
+ ...(row.clock.time ? { time: row.clock.time } : { local_time: row.clock.localTime }),
140
+ description: row.text || row.code, provider_code: row.code,
141
+ ...(row.place.location ? { location: row.place.location } : {}),
142
+ };
143
+ const identity = JSON.stringify(event);
144
+ if (seen.has(identity))
145
+ continue;
146
+ seen.add(identity);
147
+ scans.push({ event, row });
148
+ }
149
+ // Newest first; rows at the same labeled instant follow the reply's order, reversed.
150
+ scans.sort((left, right) => right.row.clock.order - left.row.clock.order || right.row.index - left.row.index);
151
+ // Oldest first: a notice keeps the stage of the row before it, and a
152
+ // delivery after the parcel was sent back reaches the sender.
153
+ let returning = false;
154
+ let previous;
155
+ for (const { event, row } of [...scans].reverse()) {
156
+ if (RETURN_STARTS.has(row.code))
157
+ returning = true;
158
+ if (isLietuvosPastasNotice(row.code)) {
159
+ if (previous)
160
+ Object.assign(event, { stage: previous, stage_source: 'none' });
161
+ continue;
162
+ }
163
+ const classified = lietuvosPastasStage(row.code, row.text);
164
+ const stage = classified?.stage === 'delivered' && returning ? 'returned' : classified?.stage;
165
+ if (stage)
166
+ Object.assign(event, { stage, stage_source: classified.source });
167
+ previous = stage;
168
+ }
169
+ const events = scans.map(scan => scan.event);
170
+ const decider = scans.find(scan => !isLietuvosPastasNotice(scan.row.code)) ?? scans[0];
171
+ const stage = decider.event.stage;
172
+ // The locker or office a parcel waits at is its pickup point; a delivered
173
+ // parcel was collected there when it waited there last. A return's is the sender's.
174
+ let point = '';
175
+ if (!returning) {
176
+ if (stage === 'ready_for_pickup')
177
+ point = decider.row.place.point ?? '';
178
+ else if (stage === 'delivered') {
179
+ const earlier = scans.slice(scans.indexOf(decider) + 1);
180
+ if (lastMovement(earlier.map(scan => scan.event)) === 'ready_for_pickup') {
181
+ point = earlier.find(scan => scan.event.stage === 'ready_for_pickup' && !isLietuvosPastasNotice(scan.row.code))?.row.place.point ?? '';
182
+ }
183
+ }
184
+ }
185
+ const newest = events[0];
186
+ return {
187
+ status: stage ? languageStageStatus(stage) : 'unknown',
188
+ ...(stage ? { current_stage: stage, current_stage_source: decider.event.stage_source } : {}),
189
+ last_status_text: decider.event.description,
190
+ last_update: newest.time ?? null,
191
+ ...(typeof newest.local_time === 'string' ? { last_update_local: newest.local_time } : {}),
192
+ expected_delivery: null,
193
+ ...(stage === 'delivered' && decider.event.time ? { delivered_at: decider.event.time } : {}),
194
+ ...(point ? { pickup_point: point } : {}),
195
+ events: events.slice(0, MAX_EVENTS),
196
+ ...(events.length > MAX_EVENTS ? { history_truncated: true } : {}),
197
+ };
198
+ }
@@ -0,0 +1,13 @@
1
+ import { type ClassifiedWording, type Stage } from '../../core/status/index.js';
2
+ import type { CarrierStatusMap } from '../../core/status/statusMap.js';
3
+ /** The types that send a parcel back: a delivery after one reaches the sender. */
4
+ export declare const RETURN_STARTS: ReadonlySet<string>;
5
+ export declare function lietuvosPastasCodeStage(code: string): Stage | undefined;
6
+ export declare function isLietuvosPastasNotice(code: string): boolean;
7
+ /**
8
+ * A row's stage from the type map, else from the shared wording rules. A type
9
+ * neither knows stays without a stage, so it remains visible for review.
10
+ */
11
+ export declare function lietuvosPastasStage(code: string, wording: string): ClassifiedWording | null;
12
+ /** Every row carries its public event type; a type the map leaves open takes the wording's stage, as rows do. */
13
+ export declare const statusMap: CarrierStatusMap;
@@ -0,0 +1,95 @@
1
+ import { classifyWording } from '../../core/status/index.js';
2
+ // Every row names a public event type, which Lietuvos paštas lists in its
3
+ // self-service API documentation with the public state it belongs to. Types
4
+ // ending in a UPU event code (EDB, EXA and others) carry that code's meaning.
5
+ const CODES = {
6
+ // The sender's label, before Lietuvos paštas has the parcel.
7
+ LABEL_CREATED: 'registered',
8
+ ACCEPTED: 'accepted',
9
+ ACCEPTED_TERMINAL: 'accepted',
10
+ // Collected from the sender's parcel locker.
11
+ RECEIVED_TERMINAL_OUT: 'in_transit',
12
+ RECEIVED_LC: 'in_transit',
13
+ RECEIVED_SENDER_LC: 'in_transit',
14
+ RECEIVED_RECEIVER_LC: 'in_transit',
15
+ DEPARTED_RECEIVER_LC: 'in_transit',
16
+ RECEIVED_DELIVERY_POST: 'in_transit',
17
+ DELIVERY_TRANSFER: 'in_transit',
18
+ DELIVERY_TRANSFER2: 'in_transit',
19
+ DELIVERY_EXPORTED: 'in_transit',
20
+ EXPORT_PENDING: 'in_transit',
21
+ DELIVERY_REDIRECTING: 'in_transit',
22
+ DELIVERY_EDD: 'in_transit',
23
+ DELIVERY_EDE: 'in_transit',
24
+ TRANSIT_EMJ: 'in_transit',
25
+ TRANSIT_EMK: 'in_transit',
26
+ // Released by customs.
27
+ CUSTOMS_EDC: 'in_transit',
28
+ CUSTOMS_EXC: 'in_transit',
29
+ CUSTOMS_EDB: 'customs',
30
+ CUSTOMS_EME: 'customs',
31
+ CUSTOMS_EXA: 'customs',
32
+ CUSTOMS_EXB: 'customs',
33
+ CUSTOMS_ON_HOLD: 'customs',
34
+ CUSTOMS_AWAITING_PAYMENT: 'customs',
35
+ CUSTOMS_PREPAID: 'customs',
36
+ CUSTOMS_DECLARATION_REQUIRED: 'customs',
37
+ CUSTOMS_DECLARATION_REVIEW_PENDING: 'customs',
38
+ CUSTOMS_DECLARATION_SUBMITTED: 'customs',
39
+ TRANSFERRED_FOR_DELIVERY_COURIER: 'out_for_delivery',
40
+ TRANSFERRED_FOR_DELIVERY_POSTMAN: 'out_for_delivery',
41
+ DELIVERY_UNSUCCESSFUL_DELIVERY: 'failed_attempt',
42
+ // In the recipient's parcel locker, or at a collection point.
43
+ RECEIVED_TERMINAL: 'ready_for_pickup',
44
+ DELIVERY_EDH: 'ready_for_pickup',
45
+ DELIVERY_DELIVERED: 'delivered',
46
+ // On its way back to the sender: not delivered and not back yet.
47
+ DELIVERY_RETURNED: 'exception',
48
+ DELIVERY_REFUNDING: 'exception',
49
+ DELIVERY_PARCEL_LOST: 'exception',
50
+ DELIVERY_DESTROY: 'exception',
51
+ DELIVERY_EXX: 'exception',
52
+ HANDED_TO_GOVERNMENT: 'exception',
53
+ LABEL_CANCELLED: 'exception',
54
+ };
55
+ /** The types that send a parcel back: a delivery after one reaches the sender. */
56
+ export const RETURN_STARTS = new Set(['DELIVERY_RETURNED', 'DELIVERY_REFUNDING']);
57
+ // Messages to and from the recipient: no movement, so the parcel keeps its stage.
58
+ const NOTICES = new Map([
59
+ ['NOTIFICATIONS_INFORMED', 'The recipient was told about the parcel.'],
60
+ ['NOTIFICATIONS_PDA_UNABLE_TO_CONTACT', 'The courier could not reach the recipient.'],
61
+ ['ORDERS_PARCEL_DEMAND', 'The recipient asked for the parcel.'],
62
+ ]);
63
+ // Holds whose meaning for the delivery is unclear: the row's wording decides.
64
+ const UNSTAGED = new Map([
65
+ ['DELIVERY_EDA', 'A hold on arrival in the destination country; the type does not say why.'],
66
+ ['DELIVERY_EDF', 'Held at the delivery office; the type does not say whether for collection or for a round.'],
67
+ ['DELIVERY_EXD', 'Held at the office of exchange before leaving; the type does not say why.'],
68
+ ['DELIVERY_HOLDING', 'Held; the type does not say where or why.'],
69
+ ['DELIVERY_STORING', 'Stored; the type does not say whether for collection.'],
70
+ ]);
71
+ export function lietuvosPastasCodeStage(code) {
72
+ return Object.hasOwn(CODES, code) ? CODES[code] : undefined;
73
+ }
74
+ export function isLietuvosPastasNotice(code) {
75
+ return NOTICES.has(code);
76
+ }
77
+ /**
78
+ * A row's stage from the type map, else from the shared wording rules. A type
79
+ * neither knows stays without a stage, so it remains visible for review.
80
+ */
81
+ export function lietuvosPastasStage(code, wording) {
82
+ const mapped = lietuvosPastasCodeStage(code);
83
+ if (mapped)
84
+ return { stage: mapped, source: 'carrier_map' };
85
+ const worded = classifyWording(wording, 'in_transit');
86
+ return worded.source === 'none' ? null : worded;
87
+ }
88
+ /** Every row carries its public event type; a type the map leaves open takes the wording's stage, as rows do. */
89
+ export const statusMap = {
90
+ stage: (code, wording) => (code && !isLietuvosPastasNotice(code) ? lietuvosPastasStage(code, wording)?.stage : undefined),
91
+ gaps: [
92
+ ...[...NOTICES].map(([code, note]) => ({ code, note: `${note} Not a movement: the parcel keeps the stage it had.` })),
93
+ ...[...UNSTAGED].map(([code, note]) => ({ code, note })),
94
+ ],
95
+ };
@@ -1,4 +1,56 @@
1
1
  {
2
2
  "carrier": "lietuvos-pastas",
3
- "entries": []
3
+ "entries": [
4
+ {"code": "LABEL_CREATED", "wording": "The label for the parcel has been created", "stage": "registered", "language": "en", "confirmedBy": "live", "note": "The sender's label, before Lietuvos paštas has the parcel."},
5
+ {"code": "ACCEPTED", "wording": "Parcel received", "stage": "accepted", "language": "en", "confirmedBy": "live"},
6
+ {"code": "RECEIVED_LC", "wording": "The parcel is at the distribution center", "stage": "in_transit", "language": "en", "confirmedBy": "live"},
7
+ {"code": "DELIVERY_TRANSFER", "wording": "The parcel is transported to another distribution center", "stage": "in_transit", "language": "en", "confirmedBy": "live"},
8
+ {"code": "DELIVERY_EXPORTED", "wording": "The parcel has been sent abroad", "stage": "in_transit", "language": "en", "confirmedBy": "live"},
9
+ {"code": "RECEIVED_RECEIVER_LC", "wording": "The parcel was received at the distribution center of the recipient's (or transit) country", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "A row from abroad: its clock is that country's wall clock."},
10
+ {"code": "DEPARTED_RECEIVER_LC", "wording": "The parcel is sent from the distribution center of the recipient's (or transit) country", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "A row from abroad: its clock is that country's wall clock."},
11
+ {"code": "TRANSFERRED_FOR_DELIVERY_COURIER", "wording": "The parcel has been forwarded for delivery", "stage": "out_for_delivery", "language": "en", "confirmedBy": "live", "note": "Also before a courier takes the parcel to a parcel locker."},
12
+ {"code": "RECEIVED_TERMINAL", "wording": "The parcel was delivered to the UNISEND parcel locker", "stage": "ready_for_pickup", "language": "en", "confirmedBy": "live", "note": "The row names the parcel locker the parcel waits in."},
13
+ {"code": "DELIVERY_DELIVERED", "wording": "The parcel has been delivered to the recipient", "stage": "delivered", "language": "en", "confirmedBy": "live", "note": "After a return starts, the delivery is the sender's and reads as returned."},
14
+ {"code": "ACCEPTED_TERMINAL", "wording": "The parcel has been placed in the UNISEND parcel locker for sending", "stage": "accepted", "language": "en", "confirmedBy": "portal", "note": "Seen in archived replies of the tracking service."},
15
+ {"code": "RECEIVED_TERMINAL_OUT", "wording": "The parcel was picked up by the courier from the UNISEND parcel locker", "stage": "in_transit", "language": "en", "confirmedBy": "portal", "note": "Collected from the sender's parcel locker. Seen in archived replies of the tracking service."},
16
+ {"code": "RECEIVED_SENDER_LC", "stage": "in_transit", "confirmedBy": "official-doc", "note": "At a distribution centre in the sender's country. Listed in https://www.post.lt/savitarna/api_doc.html."},
17
+ {"code": "RECEIVED_DELIVERY_POST", "stage": "in_transit", "confirmedBy": "official-doc", "note": "At the delivering post office. Listed in https://www.post.lt/savitarna/api_doc.html."},
18
+ {"code": "DELIVERY_TRANSFER2", "stage": "in_transit", "confirmedBy": "official-doc", "note": "Listed in https://www.post.lt/savitarna/api_doc.html."},
19
+ {"code": "EXPORT_PENDING", "stage": "in_transit", "confirmedBy": "official-doc", "note": "Listed in https://www.post.lt/savitarna/api_doc.html."},
20
+ {"code": "DELIVERY_REDIRECTING", "stage": "in_transit", "confirmedBy": "official-doc", "note": "Redirected to another address or point. Listed in https://www.post.lt/savitarna/api_doc.html."},
21
+ {"code": "DELIVERY_EDD", "stage": "in_transit", "confirmedBy": "official-doc", "note": "UPU EDD: into a sorting centre. Listed in https://www.post.lt/savitarna/api_doc.html."},
22
+ {"code": "DELIVERY_EDE", "stage": "in_transit", "confirmedBy": "official-doc", "note": "UPU EDE: out of a sorting centre. Listed in https://www.post.lt/savitarna/api_doc.html."},
23
+ {"code": "TRANSIT_EMJ", "stage": "in_transit", "confirmedBy": "official-doc", "note": "UPU EMJ: arrival at a transit office of exchange. Listed in https://www.post.lt/savitarna/api_doc.html."},
24
+ {"code": "TRANSIT_EMK", "stage": "in_transit", "confirmedBy": "official-doc", "note": "UPU EMK: departure from a transit office of exchange. Listed in https://www.post.lt/savitarna/api_doc.html."},
25
+ {"code": "CUSTOMS_EDC", "stage": "in_transit", "confirmedBy": "official-doc", "note": "UPU EDC: returned from import customs. Listed in https://www.post.lt/savitarna/api_doc.html."},
26
+ {"code": "CUSTOMS_EXC", "stage": "in_transit", "confirmedBy": "official-doc", "note": "UPU EXC: returned from export customs. Listed in https://www.post.lt/savitarna/api_doc.html."},
27
+ {"code": "CUSTOMS_EDB", "stage": "customs", "confirmedBy": "official-doc", "note": "UPU EDB: presented to import customs. Listed in https://www.post.lt/savitarna/api_doc.html."},
28
+ {"code": "CUSTOMS_EME", "stage": "customs", "confirmedBy": "official-doc", "note": "UPU EME: held at the inward office of exchange. Listed in https://www.post.lt/savitarna/api_doc.html."},
29
+ {"code": "CUSTOMS_EXA", "stage": "customs", "confirmedBy": "official-doc", "note": "UPU EXA: presented to export customs. Listed in https://www.post.lt/savitarna/api_doc.html."},
30
+ {"code": "CUSTOMS_EXB", "stage": "customs", "confirmedBy": "official-doc", "note": "UPU EXB: held by export customs. Listed in https://www.post.lt/savitarna/api_doc.html."},
31
+ {"code": "CUSTOMS_ON_HOLD", "stage": "customs", "confirmedBy": "official-doc", "note": "Listed in https://www.post.lt/savitarna/api_doc.html."},
32
+ {"code": "CUSTOMS_AWAITING_PAYMENT", "stage": "customs", "confirmedBy": "official-doc", "note": "Awaiting payment of duties. Listed in https://www.post.lt/savitarna/api_doc.html."},
33
+ {"code": "CUSTOMS_PREPAID", "stage": "customs", "confirmedBy": "official-doc", "note": "Listed in https://www.post.lt/savitarna/api_doc.html."},
34
+ {"code": "CUSTOMS_DECLARATION_REQUIRED", "stage": "customs", "confirmedBy": "official-doc", "note": "Listed in https://www.post.lt/savitarna/api_doc.html."},
35
+ {"code": "CUSTOMS_DECLARATION_REVIEW_PENDING", "stage": "customs", "confirmedBy": "official-doc", "note": "Listed in https://www.post.lt/savitarna/api_doc.html."},
36
+ {"code": "CUSTOMS_DECLARATION_SUBMITTED", "stage": "customs", "confirmedBy": "official-doc", "note": "Listed in https://www.post.lt/savitarna/api_doc.html."},
37
+ {"code": "TRANSFERRED_FOR_DELIVERY_POSTMAN", "stage": "out_for_delivery", "confirmedBy": "official-doc", "note": "Listed in https://www.post.lt/savitarna/api_doc.html."},
38
+ {"code": "DELIVERY_UNSUCCESSFUL_DELIVERY", "stage": "failed_attempt", "confirmedBy": "official-doc", "note": "Listed in https://www.post.lt/savitarna/api_doc.html."},
39
+ {"code": "DELIVERY_EDH", "stage": "ready_for_pickup", "confirmedBy": "official-doc", "note": "UPU EDH: arrival at a collection point. Listed in https://www.post.lt/savitarna/api_doc.html."},
40
+ {"code": "DELIVERY_RETURNED", "stage": "exception", "confirmedBy": "official-doc", "note": "A return under way. A delivery after it is the sender's. Listed in https://www.post.lt/savitarna/api_doc.html."},
41
+ {"code": "DELIVERY_REFUNDING", "stage": "exception", "confirmedBy": "official-doc", "note": "Listed with the returning state. A delivery after it is the sender's. Listed in https://www.post.lt/savitarna/api_doc.html."},
42
+ {"code": "DELIVERY_PARCEL_LOST", "stage": "exception", "confirmedBy": "official-doc", "note": "Lost. Listed in https://www.post.lt/savitarna/api_doc.html."},
43
+ {"code": "DELIVERY_DESTROY", "stage": "exception", "confirmedBy": "official-doc", "note": "Destroyed. Listed in https://www.post.lt/savitarna/api_doc.html."},
44
+ {"code": "DELIVERY_EXX", "stage": "exception", "confirmedBy": "official-doc", "note": "UPU EXX: export cancelled. Listed in https://www.post.lt/savitarna/api_doc.html."},
45
+ {"code": "HANDED_TO_GOVERNMENT", "stage": "exception", "confirmedBy": "official-doc", "note": "Handed to the authorities. Listed in https://www.post.lt/savitarna/api_doc.html."},
46
+ {"code": "LABEL_CANCELLED", "stage": "exception", "confirmedBy": "official-doc", "note": "The label was cancelled. Listed in https://www.post.lt/savitarna/api_doc.html."},
47
+ {"code": "NOTIFICATIONS_INFORMED", "confirmedBy": "official-doc", "note": "The recipient was told about the parcel. Not a movement: the parcel keeps the stage it had. Listed in https://www.post.lt/savitarna/api_doc.html."},
48
+ {"code": "NOTIFICATIONS_PDA_UNABLE_TO_CONTACT", "confirmedBy": "official-doc", "note": "The courier could not reach the recipient. Not a movement: the parcel keeps the stage it had. Listed in https://www.post.lt/savitarna/api_doc.html."},
49
+ {"code": "ORDERS_PARCEL_DEMAND", "confirmedBy": "official-doc", "note": "The recipient asked for the parcel. Not a movement: the parcel keeps the stage it had. Listed in https://www.post.lt/savitarna/api_doc.html."},
50
+ {"code": "DELIVERY_EDA", "confirmedBy": "official-doc", "note": "A hold on arrival in the destination country; the type does not say why, so the row's wording decides. Listed in https://www.post.lt/savitarna/api_doc.html."},
51
+ {"code": "DELIVERY_EDF", "confirmedBy": "official-doc", "note": "Held at the delivery office; the type does not say whether for collection or for a round, so the row's wording decides. Listed in https://www.post.lt/savitarna/api_doc.html."},
52
+ {"code": "DELIVERY_EXD", "confirmedBy": "official-doc", "note": "Held at the office of exchange before leaving; the type does not say why, so the row's wording decides. Listed in https://www.post.lt/savitarna/api_doc.html."},
53
+ {"code": "DELIVERY_HOLDING", "confirmedBy": "official-doc", "note": "Held; the type does not say where or why, so the row's wording decides. Listed in https://www.post.lt/savitarna/api_doc.html."},
54
+ {"code": "DELIVERY_STORING", "confirmedBy": "official-doc", "note": "Stored; the type does not say whether for collection, so the row's wording decides. Listed in https://www.post.lt/savitarna/api_doc.html."}
55
+ ]
4
56
  }
@@ -46,6 +46,7 @@ import { statusMap as jAndT } from '../../carriers/j-and-t/status.js';
46
46
  import { statusMap as jntCargo } from '../../carriers/j-and-t-cargo/status.js';
47
47
  import { statusMap as laPoste } from '../../carriers/la-poste/status.js';
48
48
  import { statusMap as latvijasPasts } from '../../carriers/latvijas-pasts/status.js';
49
+ import { statusMap as lietuvosPastas } from '../../carriers/lietuvos-pastas/status.js';
49
50
  import { statusMap as matkahuolto } from '../../carriers/matkahuolto/status.js';
50
51
  import { statusMap as mngKargo } from '../../carriers/mng-kargo/status.js';
51
52
  import { statusMap as oldDominion } from '../../carriers/old-dominion/status.js';
@@ -80,10 +81,10 @@ export const STATUS_MAPS = {
80
81
  'ceska-posta': ceskaPosta, 'china-post': chinaPost, chronopost, 'correos-spain': correosSpain, dao, 'dhl-express': dhlExpress, dpd, 'dpd-de': dpdDe,
81
82
  'dpd-fr': dpdFr, 'dpd-ie': dpdIe, 'dpd-pl': dpdPl, econt, elta, emile, 'express-one': expressOne, 'fan-courier': fanCourier, foxpost,
82
83
  'geniki-taxydromiki': geniki, 'gls-es': glsEs, 'gls-it': glsIt, 'gofo-fr': gofoFr, 'gofo-it': gofoIt, helthjem, 'hrvatska-posta': hrvatskaPosta,
83
- 'j-and-t': jAndT, 'j-and-t-cargo': jntCargo, 'la-poste': laPoste, 'latvijas-pasts': latvijasPasts, matkahuolto, 'mng-kargo': mngKargo,
84
- 'old-dominion': oldDominion, omgo, omniva, 'orlen-paczka': orlenPaczka, 'posta-slovenije': postaSlovenije, postlogistics, ppl, ptt, sagawa,
85
- sameday, 'slovak-parcel-service': slovakParcelService, 'slovenska-posta': slovenskaPosta, speedx, speedy, sto, 'swiss-post': swissPost,
86
- 'thailand-post': thailandPost, tnt, ups, venipak, 'vinted-go': vintedGo, yunexpress, 'yurtici-kargo': yurticiKargo,
84
+ 'j-and-t': jAndT, 'j-and-t-cargo': jntCargo, 'la-poste': laPoste, 'latvijas-pasts': latvijasPasts, 'lietuvos-pastas': lietuvosPastas, matkahuolto,
85
+ 'mng-kargo': mngKargo, 'old-dominion': oldDominion, omgo, omniva, 'orlen-paczka': orlenPaczka, 'posta-slovenije': postaSlovenije, postlogistics,
86
+ ppl, ptt, sagawa, sameday, 'slovak-parcel-service': slovakParcelService, 'slovenska-posta': slovenskaPosta, speedx, speedy, sto,
87
+ 'swiss-post': swissPost, 'thailand-post': thailandPost, tnt, ups, venipak, 'vinted-go': vintedGo, yunexpress, 'yurtici-kargo': yurticiKargo,
87
88
  };
88
89
  /** The carrier the app files the universal providers' scans under. */
89
90
  const UNIVERSAL_CARRIER = 'unknown';
@@ -7024,7 +7024,9 @@
7024
7024
  "timezone": "Europe/Vilnius",
7025
7025
  "tracking": {
7026
7026
  "mode": "automatic",
7027
- "adapter": "universal"
7027
+ "adapter": "lietuvos-pastas",
7028
+ "recognitionRank": 860,
7029
+ "localClocks": true
7028
7030
  },
7029
7031
  "canaryUrl": "https://lpexpress.lt/track",
7030
7032
  "trackingUrlTemplate": "https://lpexpress.lt/track?parcel={trackingNumber}",
@@ -7059,6 +7061,12 @@
7059
7061
  {
7060
7062
  "pattern": "^(?:CC|CH|HC)\\d{9}LT$",
7061
7063
  "confidence": "low"
7064
+ },
7065
+ {
7066
+ "pattern": "^[A-Z]{2}\\d{9}LT$",
7067
+ "confidence": "low",
7068
+ "checksum": "s10",
7069
+ "preferred": true
7062
7070
  }
7063
7071
  ]
7064
7072
  },
@@ -4730,7 +4730,9 @@ export declare const CARRIER_CATALOG: {
4730
4730
  readonly timezone: "Europe/Vilnius";
4731
4731
  readonly tracking: {
4732
4732
  readonly mode: "automatic";
4733
- readonly adapter: "universal";
4733
+ readonly adapter: "lietuvos-pastas";
4734
+ readonly recognitionRank: 860;
4735
+ readonly localClocks: true;
4734
4736
  };
4735
4737
  readonly canaryUrl: "https://lpexpress.lt/track";
4736
4738
  readonly trackingUrlTemplate: "https://lpexpress.lt/track?parcel={trackingNumber}";
@@ -4750,6 +4752,11 @@ export declare const CARRIER_CATALOG: {
4750
4752
  readonly detectionRules: readonly [{
4751
4753
  readonly pattern: "^(?:CC|CH|HC)\\d{9}LT$";
4752
4754
  readonly confidence: "low";
4755
+ }, {
4756
+ readonly pattern: "^[A-Z]{2}\\d{9}LT$";
4757
+ readonly confidence: "low";
4758
+ readonly checksum: "s10";
4759
+ readonly preferred: true;
4753
4760
  }];
4754
4761
  };
4755
4762
  readonly "loomis-express": {
@@ -7025,7 +7025,9 @@ export const CARRIER_CATALOG = {
7025
7025
  "timezone": "Europe/Vilnius",
7026
7026
  "tracking": {
7027
7027
  "mode": "automatic",
7028
- "adapter": "universal"
7028
+ "adapter": "lietuvos-pastas",
7029
+ "recognitionRank": 860,
7030
+ "localClocks": true
7029
7031
  },
7030
7032
  "canaryUrl": "https://lpexpress.lt/track",
7031
7033
  "trackingUrlTemplate": "https://lpexpress.lt/track?parcel={trackingNumber}",
@@ -7060,6 +7062,12 @@ export const CARRIER_CATALOG = {
7060
7062
  {
7061
7063
  "pattern": "^(?:CC|CH|HC)\\d{9}LT$",
7062
7064
  "confidence": "low"
7065
+ },
7066
+ {
7067
+ "pattern": "^[A-Z]{2}\\d{9}LT$",
7068
+ "confidence": "low",
7069
+ "checksum": "s10",
7070
+ "preferred": true
7063
7071
  }
7064
7072
  ]
7065
7073
  },
@@ -8940,7 +8948,7 @@ export const DETECTION_RULE_IDS = {
8940
8948
  "kuehne-nagel": ["kuehne-nagel-1", "kuehne-nagel-2"],
8941
8949
  "latvijas-pasts": ["latvijas-pasts-s10"],
8942
8950
  "lbc-express": ["lbc-domestic"],
8943
- "lietuvos-pastas": ["lietuvos-pastas-1"],
8951
+ "lietuvos-pastas": ["lietuvos-pastas-1", "lietuvos-pastas-s10"],
8944
8952
  "loomis-express": ["loomis-express-1", "loomis-express-2"],
8945
8953
  "lso": ["lso-1"],
8946
8954
  "magyar-posta": [],
@@ -53,6 +53,7 @@ export const CARRIER_RECOGNITION_RANKS = {
53
53
  "j-and-t-cargo": 470,
54
54
  "la-poste": 800,
55
55
  "latvijas-pasts": 820,
56
+ "lietuvos-pastas": 860,
56
57
  "matkahuolto": 690,
57
58
  "mng-kargo": 740,
58
59
  "nacex": 410,
@@ -91,6 +91,7 @@ import { adapter as laPoste } from '../carriers/la-poste/adapter.js';
91
91
  import { adapter as landmarkGlobal } from '../carriers/landmark-global/adapter.js';
92
92
  import { adapter as latvijasPasts } from '../carriers/latvijas-pasts/adapter.js';
93
93
  import { adapter as lbcExpress } from '../carriers/lbc-express/adapter.js';
94
+ import { adapter as lietuvosPastas } from '../carriers/lietuvos-pastas/adapter.js';
94
95
  import { adapter as matkahuolto } from '../carriers/matkahuolto/adapter.js';
95
96
  import { adapter as mngKargo } from '../carriers/mng-kargo/adapter.js';
96
97
  import { adapter as mondialRelay } from '../carriers/mondial-relay/adapter.js';
@@ -247,6 +248,7 @@ export const REGISTRY = {
247
248
  "landmark-global": landmarkGlobal,
248
249
  "latvijas-pasts": latvijasPasts,
249
250
  "lbc-express": lbcExpress,
251
+ "lietuvos-pastas": lietuvosPastas,
250
252
  "matkahuolto": matkahuolto,
251
253
  "mng-kargo": mngKargo,
252
254
  "mondial-relay": mondialRelay,
@@ -428,7 +430,7 @@ export const REGISTRY = {
428
430
  "landmark-global": "landmark-global",
429
431
  "latvijas-pasts": "latvijas-pasts",
430
432
  "lbc-express": "lbc-express",
431
- "lietuvos-pastas": "universal",
433
+ "lietuvos-pastas": "lietuvos-pastas",
432
434
  "loomis-express": "universal",
433
435
  "lso": "universal",
434
436
  "magyar-posta": "universal",
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.95.0"
5
+ "version": "0.96.0"
6
6
  },
7
7
  "paths": {
8
8
  "/health": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-parcel-scraper",
3
- "version": "0.95.0-main.992",
3
+ "version": "0.96.0-main.994",
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",