universal-parcel-scraper 0.92.0-main.985 → 0.93.0-main.987

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 **150 dedicated adapters** and **5 universal fallbacks**
16
+ **3,500+ carriers** through **151 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,098 such statuses, each filed under one stage.
158
+ The carrier folders record 4,117 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
@@ -6938,7 +6938,9 @@
6938
6938
  "timezone": "Europe/Riga",
6939
6939
  "tracking": {
6940
6940
  "mode": "automatic",
6941
- "adapter": "universal"
6941
+ "adapter": "latvijas-pasts",
6942
+ "recognitionRank": 820,
6943
+ "localClocks": true
6942
6944
  },
6943
6945
  "canaryUrl": "https://www.pasts.lv/izsekot-sutijumu",
6944
6946
  "trackingUrlTemplate": "https://mans.pasts.lv/track?locale=lv-LV&id={trackingNumber}",
@@ -6954,7 +6956,14 @@
6954
6956
  ]
6955
6957
  }
6956
6958
  ],
6957
- "detectionRules": []
6959
+ "detectionRules": [
6960
+ {
6961
+ "pattern": "^[A-Z]{2}\\d{9}LV$",
6962
+ "confidence": "low",
6963
+ "checksum": "s10",
6964
+ "preferred": true
6965
+ }
6966
+ ]
6958
6967
  },
6959
6968
  "lbc-express": {
6960
6969
  "displayName": "LBC Express",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "catalog": 208,
3
- "activeAdapters": 150,
3
+ "activeAdapters": 151,
4
4
  "countries": 66,
5
5
  "referenceCarriers": 100,
6
6
  "comparison": {
@@ -25585,6 +25585,7 @@
25585
25585
  "intl-post"
25586
25586
  ],
25587
25587
  "asked": [
25588
+ "latvijas-pasts",
25588
25589
  "chronopost"
25589
25590
  ]
25590
25591
  },
@@ -25596,6 +25597,7 @@
25596
25597
  "intl-post"
25597
25598
  ],
25598
25599
  "asked": [
25600
+ "latvijas-pasts",
25599
25601
  "chronopost"
25600
25602
  ]
25601
25603
  },
@@ -25607,6 +25609,7 @@
25607
25609
  "intl-post"
25608
25610
  ],
25609
25611
  "asked": [
25612
+ "latvijas-pasts",
25610
25613
  "chronopost"
25611
25614
  ]
25612
25615
  },
@@ -27047,6 +27050,7 @@
27047
27050
  "intl-post"
27048
27051
  ],
27049
27052
  "asked": [
27053
+ "latvijas-pasts",
27050
27054
  "chronopost"
27051
27055
  ]
27052
27056
  },
@@ -31655,6 +31659,7 @@
31655
31659
  "intl-post"
31656
31660
  ],
31657
31661
  "asked": [
31662
+ "latvijas-pasts",
31658
31663
  "chronopost"
31659
31664
  ]
31660
31665
  },
@@ -31666,6 +31671,7 @@
31666
31671
  "intl-post"
31667
31672
  ],
31668
31673
  "asked": [
31674
+ "latvijas-pasts",
31669
31675
  "chronopost"
31670
31676
  ]
31671
31677
  },
@@ -31677,6 +31683,7 @@
31677
31683
  "intl-post"
31678
31684
  ],
31679
31685
  "asked": [
31686
+ "latvijas-pasts",
31680
31687
  "chronopost"
31681
31688
  ]
31682
31689
  },
@@ -31688,6 +31695,7 @@
31688
31695
  "intl-post"
31689
31696
  ],
31690
31697
  "asked": [
31698
+ "latvijas-pasts",
31691
31699
  "chronopost"
31692
31700
  ]
31693
31701
  },
@@ -31699,6 +31707,7 @@
31699
31707
  "intl-post"
31700
31708
  ],
31701
31709
  "asked": [
31710
+ "latvijas-pasts",
31702
31711
  "chronopost"
31703
31712
  ]
31704
31713
  },
@@ -31710,6 +31719,7 @@
31710
31719
  "intl-post"
31711
31720
  ],
31712
31721
  "asked": [
31722
+ "latvijas-pasts",
31713
31723
  "chronopost"
31714
31724
  ]
31715
31725
  },
@@ -31721,6 +31731,7 @@
31721
31731
  "intl-post"
31722
31732
  ],
31723
31733
  "asked": [
31734
+ "latvijas-pasts",
31724
31735
  "chronopost"
31725
31736
  ]
31726
31737
  },
@@ -31732,6 +31743,7 @@
31732
31743
  "intl-post"
31733
31744
  ],
31734
31745
  "asked": [
31746
+ "latvijas-pasts",
31735
31747
  "chronopost"
31736
31748
  ]
31737
31749
  },
@@ -31743,6 +31755,7 @@
31743
31755
  "intl-post"
31744
31756
  ],
31745
31757
  "asked": [
31758
+ "latvijas-pasts",
31746
31759
  "chronopost"
31747
31760
  ]
31748
31761
  },
@@ -34250,6 +34263,7 @@
34250
34263
  "intl-post"
34251
34264
  ],
34252
34265
  "asked": [
34266
+ "latvijas-pasts",
34253
34267
  "chronopost"
34254
34268
  ]
34255
34269
  },
@@ -35419,6 +35433,7 @@
35419
35433
  "intl-post"
35420
35434
  ],
35421
35435
  "asked": [
35436
+ "latvijas-pasts",
35422
35437
  "chronopost"
35423
35438
  ]
35424
35439
  },
@@ -36887,6 +36902,7 @@
36887
36902
  "intl-post"
36888
36903
  ],
36889
36904
  "asked": [
36905
+ "latvijas-pasts",
36890
36906
  "chronopost"
36891
36907
  ]
36892
36908
  },
@@ -37173,6 +37189,7 @@
37173
37189
  "intl-post"
37174
37190
  ],
37175
37191
  "asked": [
37192
+ "latvijas-pasts",
37176
37193
  "chronopost"
37177
37194
  ]
37178
37195
  },
@@ -5527,7 +5527,9 @@ var be = {
5527
5527
  timezone: "Europe/Riga",
5528
5528
  tracking: {
5529
5529
  mode: "automatic",
5530
- adapter: "universal"
5530
+ adapter: "latvijas-pasts",
5531
+ recognitionRank: 820,
5532
+ localClocks: !0
5531
5533
  },
5532
5534
  canaryUrl: "https://www.pasts.lv/izsekot-sutijumu",
5533
5535
  trackingUrlTemplate: "https://mans.pasts.lv/track?locale=lv-LV&id={trackingNumber}",
@@ -5537,7 +5539,12 @@ var be = {
5537
5539
  pathPattern: "^/track/?$",
5538
5540
  params: ["id"]
5539
5541
  }],
5540
- detectionRules: []
5542
+ detectionRules: [{
5543
+ pattern: "^[A-Z]{2}\\d{9}LV$",
5544
+ confidence: "low",
5545
+ checksum: "s10",
5546
+ preferred: !0
5547
+ }]
5541
5548
  },
5542
5549
  "lbc-express": {
5543
5550
  displayName: "LBC Express",
@@ -7143,7 +7150,7 @@ var be = {
7143
7150
  "j-and-t-cargo": ["j-and-t-cargo-1", "j-and-t-cargo-2"],
7144
7151
  jitsu: [],
7145
7152
  "kuehne-nagel": ["kuehne-nagel-1", "kuehne-nagel-2"],
7146
- "latvijas-pasts": [],
7153
+ "latvijas-pasts": ["latvijas-pasts-s10"],
7147
7154
  "lbc-express": ["lbc-domestic"],
7148
7155
  "lietuvos-pastas": ["lietuvos-pastas-1"],
7149
7156
  "loomis-express": ["loomis-express-1", "loomis-express-2"],
@@ -11937,6 +11944,7 @@ var bc = {
11937
11944
  "j-and-t": 90,
11938
11945
  "j-and-t-cargo": 470,
11939
11946
  "la-poste": 800,
11947
+ "latvijas-pasts": 820,
11940
11948
  matkahuolto: 690,
11941
11949
  "mng-kargo": 740,
11942
11950
  nacex: 410,
@@ -0,0 +1,14 @@
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 LATVIJAS_PASTS_ENDPOINT = "https://mans.pasts.lv/tracking-api";
5
+ export declare class LatvijasPastsTracker {
6
+ private readonly options;
7
+ constructor(options?: {
8
+ fetcher?: typeof fetch;
9
+ recorder?: StepRecorder;
10
+ userAgent?: string;
11
+ });
12
+ fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
13
+ }
14
+ export declare const adapter: AdapterFactory;
@@ -0,0 +1,60 @@
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 { LATVIJAS_PASTS_MAX_BYTES, normalizeLatvijasPastsNumber, parseLatvijasPasts, PROVIDER } from './parser.js';
8
+ // The service mans.pasts.lv's tracking page reads. It answers JSON only when
9
+ // asked for it, and XML otherwise.
10
+ export const LATVIJAS_PASTS_ENDPOINT = 'https://mans.pasts.lv/tracking-api';
11
+ // A CloudFront or web application firewall block page.
12
+ const BLOCKED = /<title>\s*(?:ERROR: The request could not be satisfied|Request blocked|Access Denied|Just a moment|Attention Required)|cf-chl-|cf-turnstile|Generated by cloudfront/i;
13
+ export class LatvijasPastsTracker {
14
+ options;
15
+ constructor(options = {}) {
16
+ this.options = options;
17
+ }
18
+ async fetch(raw, context = {}) {
19
+ const number = normalizeLatvijasPastsNumber(raw);
20
+ const url = `${LATVIJAS_PASTS_ENDPOINT}?${new URLSearchParams({ id: number, lang: 'en-US', include_group: 'true' })}`;
21
+ const read = async (signal, timeoutMs) => {
22
+ let bytes;
23
+ try {
24
+ ({ bytes } = await fetchBounded(url, { signal, headers: { Accept: 'application/json', 'User-Agent': userAgentOf(this.options.userAgent) } }, { provider: PROVIDER, timeoutMs: Math.max(1, Math.floor(timeoutMs)), maxBytes: LATVIJAS_PASTS_MAX_BYTES, fetcher: this.options.fetcher }));
25
+ }
26
+ catch (error) {
27
+ // A missing route is not a missing item: the service answers unknown codes with a notice.
28
+ if (error instanceof UpstreamHttpError && [404, 410].includes(error.status)) {
29
+ throw new TransportError(PROVIDER, 'Latvijas Pasts tracking endpoint is unavailable', { cause: error });
30
+ }
31
+ throw error;
32
+ }
33
+ let payload;
34
+ try {
35
+ payload = parseJsonBytes(bytes, PROVIDER);
36
+ }
37
+ catch (cause) {
38
+ if (BLOCKED.test(decodeText(bytes.subarray(0, 64_000))))
39
+ throw new ChallengeError(PROVIDER, 'Latvijas Pasts answered with a block page', { cause });
40
+ throw new SchemaError(PROVIDER, 'Latvijas Pasts returned invalid tracking JSON', { cause });
41
+ }
42
+ return parseLatvijasPasts(payload, number);
43
+ };
44
+ // A request that fails to reach Latvijas Pasts, or hangs, gets one more
45
+ // try: the first gets half of the budget, so the retry fits in the rest.
46
+ return runSteps({ carrier: 'latvijas-pasts', budgetMs: context.budgetMs ?? 15_000, signal: context.signal,
47
+ recorder: this.options.recorder ?? NOOP_RECORDER }, [
48
+ { id: 'direct', run: ({ signal, remainingMs }) => read(signal, firstAttemptMs(remainingMs)) },
49
+ networkRetryStep(({ signal, remainingMs }) => read(signal, remainingMs)),
50
+ ]);
51
+ }
52
+ }
53
+ export const adapter = (environment) => {
54
+ const tracker = new LatvijasPastsTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
55
+ return {
56
+ id: 'latvijas-pasts', recordsSteps: true, steps: ['direct', 'retry'],
57
+ track: (input, context) => tracker.fetch(input.number, context),
58
+ recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizeLatvijasPastsNumber(number))),
59
+ };
60
+ };
@@ -17,18 +17,48 @@
17
17
  "timezone": "Europe/Riga",
18
18
  "tracking": {
19
19
  "mode": "automatic",
20
- "adapter": "universal",
21
- "steps": []
20
+ "adapter": "latvijas-pasts",
21
+ "steps": [
22
+ "direct",
23
+ "retry"
24
+ ],
25
+ "recognition": {
26
+ "rank": 820
27
+ },
28
+ "localClocks": true
22
29
  },
23
- "capabilities": [],
30
+ "capabilities": [
31
+ "history",
32
+ "location",
33
+ "provider_code",
34
+ "pickup_point"
35
+ ],
24
36
  "portal": {
25
37
  "url": "https://mans.pasts.lv/track?locale=lv-LV&id={trackingNumber}",
26
38
  "searchUrl": "https://www.pasts.lv/izsekot-sutijumu",
27
39
  "canaryUrl": "https://www.pasts.lv/izsekot-sutijumu",
28
- "shows": [],
29
- "scraped": [],
30
- "discarded": [],
31
- "unavailable": []
40
+ "shows": [
41
+ "status",
42
+ "history",
43
+ "location",
44
+ "event_details"
45
+ ],
46
+ "scraped": [
47
+ "status",
48
+ "history",
49
+ "location",
50
+ "provider_code",
51
+ "pickup_point"
52
+ ],
53
+ "discarded": [
54
+ "event_details"
55
+ ],
56
+ "unavailable": [
57
+ "eta",
58
+ "weight",
59
+ "service_name",
60
+ "proof_of_delivery"
61
+ ]
32
62
  },
33
63
  "links": [
34
64
  {
@@ -41,5 +71,13 @@
41
71
  ]
42
72
  }
43
73
  ],
44
- "detection": []
74
+ "detection": [
75
+ {
76
+ "id": "latvijas-pasts-s10",
77
+ "pattern": "^[A-Z]{2}\\d{9}LV$",
78
+ "confidence": "low",
79
+ "preferred": true,
80
+ "checksum": "s10"
81
+ }
82
+ ]
45
83
  }
@@ -0,0 +1,12 @@
1
+ import type { CarrierResult } from '../../core/result/index.js';
2
+ export declare const PROVIDER = "Latvijas Pasts";
3
+ export declare const LATVIJAS_PASTS_MAX_BYTES = 256000;
4
+ /** Latvijas Pasts tracks UPU S10 items, its own and those it delivers for other posts. */
5
+ export declare function normalizeLatvijasPastsNumber(raw: string): string;
6
+ /**
7
+ * The tracker's reply for one item: its scans newest first, each with an event
8
+ * code, wording, a wall clock and the office, or the country of a foreign
9
+ * post's event, or a notice instead. The reply does not repeat the number; it
10
+ * answers for the one number the request names.
11
+ */
12
+ export declare function parseLatvijasPasts(payload: unknown, rawNumber: string): CarrierResult;
@@ -0,0 +1,121 @@
1
+ import { DateTime } from 'luxon';
2
+ import { isValidS10TrackingNumber, normalizeTrackingNumber } from '../../core/detection/index.js';
3
+ import { IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
4
+ import { lastMovement } from '../../core/result/pickup.js';
5
+ import { languageStageStatus } from '../../core/status/index.js';
6
+ import { clean, cleanScalar } from '../../core/transport/index.js';
7
+ import { isRecord } from '../../core/types.js';
8
+ import { latvijasPastsStage } from './status.js';
9
+ export const PROVIDER = 'Latvijas Pasts';
10
+ // A long history is a few kilobytes.
11
+ export const LATVIJAS_PASTS_MAX_BYTES = 256_000;
12
+ const MAX_ROWS = 500;
13
+ const MAX_EVENTS = 100;
14
+ const CODE = /^\d{1,6}$/;
15
+ const CLOCK = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})(?:\.\d{1,6})?$/;
16
+ // The notices the English tracker gives instead of a history.
17
+ const NOT_FOUND = 'No information received on the tracking of the postal item. Please, try again!';
18
+ const MALFORMED = 'Postal item number entered incorrectly. Please check it and enter it again.';
19
+ // An office without an English name comes back as the company's own name, which places nothing.
20
+ const NO_PLACE = /^(?:latvijas )?pasts$/i;
21
+ // An office's postcode, which the Latvian names carry.
22
+ const POSTCODE = /,?\s*\bLV-?\d{4}\b/gi;
23
+ /** Latvijas Pasts tracks UPU S10 items, its own and those it delivers for other posts. */
24
+ export function normalizeLatvijasPastsNumber(raw) {
25
+ const number = normalizeTrackingNumber(raw);
26
+ if (isValidS10TrackingNumber(number))
27
+ return number;
28
+ throw new InvalidInputError(PROVIDER, 'Latvijas Pasts tracks S10 item codes');
29
+ }
30
+ /** The scan's clock ("2026-03-04 09:15:00.000") as a wall clock. It carries no offset. */
31
+ function wallClock(value) {
32
+ const match = typeof value === 'string' ? CLOCK.exec(value.trim()) : null;
33
+ if (!match)
34
+ return null;
35
+ // UTC only checks the calendar fields. No offset is assigned.
36
+ const parsed = DateTime.fromFormat(`${match[1]} ${match[2]}`, 'yyyy-MM-dd HH:mm:ss', { zone: 'UTC' });
37
+ return parsed.isValid ? parsed.toFormat("yyyy-MM-dd'T'HH:mm:ss") : null;
38
+ }
39
+ /** The office the scan names, else the country a foreign post's event comes from. */
40
+ function placeOf(row) {
41
+ const office = clean(row.office, 200).replace(POSTCODE, '').trim();
42
+ if (office)
43
+ return { name: NO_PLACE.test(office) ? '' : office, office: true };
44
+ return { name: clean(row.country, 100), office: false };
45
+ }
46
+ /**
47
+ * The tracker's reply for one item: its scans newest first, each with an event
48
+ * code, wording, a wall clock and the office, or the country of a foreign
49
+ * post's event, or a notice instead. The reply does not repeat the number; it
50
+ * answers for the one number the request names.
51
+ */
52
+ export function parseLatvijasPasts(payload, rawNumber) {
53
+ normalizeLatvijasPastsNumber(rawNumber);
54
+ if (!isRecord(payload))
55
+ throw new SchemaError(PROVIDER, 'Latvijas Pasts did not return an item');
56
+ // The tracker shows the history when there is one, else the notice.
57
+ if (!Object.hasOwn(payload, 'data')) {
58
+ if (typeof payload.info !== 'string')
59
+ throw new SchemaError(PROVIDER, 'Latvijas Pasts returned neither a history nor a notice');
60
+ const notice = clean(payload.info, 300);
61
+ if (notice === NOT_FOUND)
62
+ throw new NotFoundError(PROVIDER);
63
+ if (notice === MALFORMED)
64
+ throw new InvalidInputError(PROVIDER, 'Latvijas Pasts does not accept this item code');
65
+ throw new IndeterminateError(PROVIDER, 'Latvijas Pasts returned a notice instead of the item history');
66
+ }
67
+ const rows = payload.data;
68
+ if (!Array.isArray(rows) || rows.length > MAX_ROWS)
69
+ throw new SchemaError(PROVIDER, 'Latvijas Pasts returned an invalid item history');
70
+ const scans = [];
71
+ const seen = new Set();
72
+ rows.forEach((row, index) => {
73
+ if (!isRecord(row))
74
+ throw new SchemaError(PROVIDER, 'Latvijas Pasts returned an invalid scan');
75
+ const code = cleanScalar(row.event_code, 16);
76
+ const time = wallClock(row.event_timestamp);
77
+ if (!CODE.test(code) || !time)
78
+ throw new SchemaError(PROVIDER, 'Latvijas Pasts returned a scan without a code or clock');
79
+ const place = placeOf(row);
80
+ const event = { local_time: time, description: clean(row.event, 300) || code, provider_code: code,
81
+ ...(place.name ? { location: place.name } : {}) };
82
+ // A foreign post's event can come twice.
83
+ const identity = JSON.stringify(event);
84
+ if (seen.has(identity))
85
+ return;
86
+ seen.add(identity);
87
+ const group = Number(row.event_group_sequence_number);
88
+ scans.push({ event, office: place.office, group: Number.isInteger(group) ? group : 0, index });
89
+ });
90
+ if (!scans.length)
91
+ throw new IndeterminateError(PROVIDER, 'Latvijas Pasts returned no scans');
92
+ // Newest first. Scans at the same second follow the tracker's progress
93
+ // groups, from created to delivered, then the reply's order.
94
+ scans.sort((left, right) => (right.event.local_time > left.event.local_time ? 1
95
+ : right.event.local_time < left.event.local_time ? -1 : right.group - left.group || left.index - right.index));
96
+ const events = scans.map(({ event }) => event);
97
+ for (const event of events) {
98
+ const classified = latvijasPastsStage(event.provider_code, event.description);
99
+ if (classified)
100
+ Object.assign(event, { stage: classified.stage, stage_source: classified.source });
101
+ }
102
+ const latest = events[0];
103
+ const stage = latest.stage;
104
+ // The Latvian office an item waits at is its pickup point. A delivered item
105
+ // was collected there when it waited there last before the delivery.
106
+ const waiting = stage === 'ready_for_pickup' ? scans[0]
107
+ : stage === 'delivered' && lastMovement(events.slice(1)) === 'ready_for_pickup'
108
+ ? scans.slice(1).find(scan => scan.event.stage === 'ready_for_pickup') : undefined;
109
+ const point = waiting?.office ? waiting.event.location ?? '' : '';
110
+ return {
111
+ status: stage ? languageStageStatus(stage) : 'unknown',
112
+ ...(stage ? { current_stage: stage, current_stage_source: latest.stage_source } : {}),
113
+ last_status_text: latest.description,
114
+ last_update: null,
115
+ last_update_local: latest.local_time,
116
+ expected_delivery: null,
117
+ ...(point ? { pickup_point: point } : {}),
118
+ events: events.slice(0, MAX_EVENTS),
119
+ ...(events.length > MAX_EVENTS ? { history_truncated: true } : {}),
120
+ };
121
+ }
@@ -0,0 +1,11 @@
1
+ import { type ClassifiedWording, type Stage } from '../../core/status/index.js';
2
+ import type { CarrierStatusMap } from '../../core/status/statusMap.js';
3
+ /** The stage Latvijas Pasts' event code gives a scan, if the map knows it. */
4
+ export declare function latvijasPastsCodeStage(code: string): Stage | undefined;
5
+ /**
6
+ * A scan's stage from its code, else from the shared wording rules. Wording
7
+ * neither knows stays without a stage, so it remains visible for review.
8
+ */
9
+ export declare function latvijasPastsStage(code: string, wording: string): ClassifiedWording | null;
10
+ /** Every scan carries a code; the map reads the code alone. */
11
+ export declare const statusMap: CarrierStatusMap;
@@ -0,0 +1,51 @@
1
+ import { classifyWording } from '../../core/status/index.js';
2
+ /**
3
+ * Latvijas Pasts names each scan with a numeric event code and words it in the
4
+ * requested language. Its own offices and the foreign posts' events it relays
5
+ * share one code list. Several codes read as the wrong milestone to the shared
6
+ * classifier: "Item returned from Customs" is a release, not a return.
7
+ */
8
+ const EVENTS = {
9
+ // Electronic data: the item is announced, not yet handed over.
10
+ 1112: 'registered',
11
+ 1114: 'registered',
12
+ 1: 'accepted',
13
+ 78: 'accepted',
14
+ 3: 'in_transit',
15
+ // Presented to customs at the office of exchange; code 7 is its release.
16
+ 4: 'customs',
17
+ 7: 'in_transit',
18
+ 8: 'in_transit',
19
+ 12: 'in_transit',
20
+ 30: 'in_transit',
21
+ 32: 'in_transit',
22
+ 35: 'in_transit',
23
+ 69: 'in_transit',
24
+ 77: 'in_transit',
25
+ 1113: 'in_transit',
26
+ 1120: 'in_transit',
27
+ 39: 'out_for_delivery',
28
+ // The delivery office printed the invitation to collect the item there.
29
+ 1134: 'ready_for_pickup',
30
+ 37: 'delivered',
31
+ };
32
+ /** The stage Latvijas Pasts' event code gives a scan, if the map knows it. */
33
+ export function latvijasPastsCodeStage(code) {
34
+ return Object.hasOwn(EVENTS, code) ? EVENTS[code] : undefined;
35
+ }
36
+ /**
37
+ * A scan's stage from its code, else from the shared wording rules. Wording
38
+ * neither knows stays without a stage, so it remains visible for review.
39
+ */
40
+ export function latvijasPastsStage(code, wording) {
41
+ const mapped = latvijasPastsCodeStage(code);
42
+ if (mapped)
43
+ return { stage: mapped, source: 'carrier_map' };
44
+ const worded = classifyWording(wording, 'in_transit');
45
+ return worded.source === 'none' ? null : worded;
46
+ }
47
+ /** Every scan carries a code; the map reads the code alone. */
48
+ export const statusMap = {
49
+ stage: code => (code ? latvijasPastsCodeStage(code) : undefined),
50
+ gaps: [],
51
+ };
@@ -1,4 +1,24 @@
1
1
  {
2
2
  "carrier": "latvijas-pasts",
3
- "entries": []
3
+ "entries": [
4
+ { "code": "1112", "wording": "Electronic data about shipment received", "language": "en", "stage": "registered", "confirmedBy": "live", "note": "The sender's data reached Latvijas Pasts, here imported from Mans Pasts; the item is not handed over yet." },
5
+ { "code": "1114", "wording": "Item information received", "language": "en", "stage": "registered", "confirmedBy": "live", "note": "The sender registered the item; Latvijas Pasts does not have it yet." },
6
+ { "code": "78", "wording": "Item received from sender", "language": "en", "stage": "accepted", "confirmedBy": "live", "note": "Comes before or beside the posting scan when a business sender hands items over." },
7
+ { "code": "1", "wording": "Item posted / received from customer", "language": "en", "stage": "accepted", "confirmedBy": "live", "note": "Also the foreign post's posting scan of an item from abroad." },
8
+ { "code": "3", "wording": "Item arrived at office of exchange", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "The outward office of exchange, in Riga or in the foreign post of an item from abroad." },
9
+ { "code": "4", "wording": "Item sent to the Customs", "language": "en", "stage": "customs", "confirmedBy": "live", "note": "Presented to export customs at the office of exchange." },
10
+ { "code": "7", "wording": "Item returned from Customs", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "Released by customs. The shared rules would read the wording as a return." },
11
+ { "code": "8", "wording": "Item sent abroad", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "Left the outward office of exchange." },
12
+ { "code": "12", "wording": "Item sent abroad (EDI-received)", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "The foreign post's dispatch of an item from abroad, relayed by its electronic data." },
13
+ { "code": "30", "wording": "Item arrived at office of exchange", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "The inward office of exchange, in Latvia or in the destination country." },
14
+ { "code": "32", "wording": "Item arrived at processing facility", "language": "en", "stage": "in_transit", "confirmedBy": "live" },
15
+ { "code": "35", "wording": "Item sent to next processing facility", "language": "en", "stage": "in_transit", "confirmedBy": "live" },
16
+ { "code": "77", "wording": "Item sent to next processing facility", "language": "en", "stage": "in_transit", "confirmedBy": "live" },
17
+ { "code": "69", "wording": "Item awaiting next planned cycle", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "Waiting for the next dispatch at the office of exchange." },
18
+ { "code": "1113", "wording": "Item sent to delivery facility", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "The reply also names the delivery office, which the adapter does not read." },
19
+ { "code": "1120", "wording": "Item received at delivery office", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "The item can still go out for delivery from there." },
20
+ { "code": "39", "wording": "Item handed over for delivery", "language": "en", "stage": "out_for_delivery", "confirmedBy": "live", "note": "Seen from foreign posts' events in the destination country." },
21
+ { "code": "1134", "wording": "Notification - pick-up invitation printed", "language": "en", "stage": "ready_for_pickup", "confirmedBy": "live", "note": "The delivery office printed the invitation to collect the item there; its office is the pickup point." },
22
+ { "code": "37", "wording": "Item delivered", "language": "en", "stage": "delivered", "confirmedBy": "live", "note": "Also the foreign post's delivery of an item sent abroad." }
23
+ ]
4
24
  }
@@ -44,6 +44,7 @@ import { statusMap as hrvatskaPosta } from '../../carriers/hrvatska-posta/status
44
44
  import { statusMap as jAndT } from '../../carriers/j-and-t/status.js';
45
45
  import { statusMap as jntCargo } from '../../carriers/j-and-t-cargo/status.js';
46
46
  import { statusMap as laPoste } from '../../carriers/la-poste/status.js';
47
+ import { statusMap as latvijasPasts } from '../../carriers/latvijas-pasts/status.js';
47
48
  import { statusMap as matkahuolto } from '../../carriers/matkahuolto/status.js';
48
49
  import { statusMap as mngKargo } from '../../carriers/mng-kargo/status.js';
49
50
  import { statusMap as oldDominion } from '../../carriers/old-dominion/status.js';
@@ -76,7 +77,8 @@ export const STATUS_MAPS = {
76
77
  'ceska-posta': ceskaPosta, 'china-post': chinaPost, chronopost, 'correos-spain': correosSpain, 'dhl-express': dhlExpress, dpd, 'dpd-de': dpdDe,
77
78
  'dpd-fr': dpdFr, 'dpd-ie': dpdIe, 'dpd-pl': dpdPl, econt, elta, emile, 'express-one': expressOne, 'fan-courier': fanCourier, foxpost,
78
79
  'geniki-taxydromiki': geniki, 'gls-es': glsEs, 'gls-it': glsIt, 'gofo-fr': gofoFr, 'gofo-it': gofoIt, helthjem,
79
- 'hrvatska-posta': hrvatskaPosta, 'j-and-t': jAndT, 'j-and-t-cargo': jntCargo, 'la-poste': laPoste, matkahuolto, 'mng-kargo': mngKargo, 'old-dominion': oldDominion, omgo, omniva,
80
+ 'hrvatska-posta': hrvatskaPosta, 'j-and-t': jAndT, 'j-and-t-cargo': jntCargo, 'la-poste': laPoste, 'latvijas-pasts': latvijasPasts,
81
+ matkahuolto, 'mng-kargo': mngKargo, 'old-dominion': oldDominion, omgo, omniva,
80
82
  'orlen-paczka': orlenPaczka, 'posta-slovenije': postaSlovenije, postlogistics, ppl, ptt, sagawa, sameday, speedx, speedy, sto, 'swiss-post': swissPost, 'thailand-post': thailandPost,
81
83
  tnt, ups, venipak, 'vinted-go': vintedGo, yunexpress, 'yurtici-kargo': yurticiKargo,
82
84
  };
@@ -6938,7 +6938,9 @@
6938
6938
  "timezone": "Europe/Riga",
6939
6939
  "tracking": {
6940
6940
  "mode": "automatic",
6941
- "adapter": "universal"
6941
+ "adapter": "latvijas-pasts",
6942
+ "recognitionRank": 820,
6943
+ "localClocks": true
6942
6944
  },
6943
6945
  "canaryUrl": "https://www.pasts.lv/izsekot-sutijumu",
6944
6946
  "trackingUrlTemplate": "https://mans.pasts.lv/track?locale=lv-LV&id={trackingNumber}",
@@ -6954,7 +6956,14 @@
6954
6956
  ]
6955
6957
  }
6956
6958
  ],
6957
- "detectionRules": []
6959
+ "detectionRules": [
6960
+ {
6961
+ "pattern": "^[A-Z]{2}\\d{9}LV$",
6962
+ "confidence": "low",
6963
+ "checksum": "s10",
6964
+ "preferred": true
6965
+ }
6966
+ ]
6958
6967
  },
6959
6968
  "lbc-express": {
6960
6969
  "displayName": "LBC Express",
@@ -4673,7 +4673,9 @@ export declare const CARRIER_CATALOG: {
4673
4673
  readonly timezone: "Europe/Riga";
4674
4674
  readonly tracking: {
4675
4675
  readonly mode: "automatic";
4676
- readonly adapter: "universal";
4676
+ readonly adapter: "latvijas-pasts";
4677
+ readonly recognitionRank: 820;
4678
+ readonly localClocks: true;
4677
4679
  };
4678
4680
  readonly canaryUrl: "https://www.pasts.lv/izsekot-sutijumu";
4679
4681
  readonly trackingUrlTemplate: "https://mans.pasts.lv/track?locale=lv-LV&id={trackingNumber}";
@@ -4683,7 +4685,12 @@ export declare const CARRIER_CATALOG: {
4683
4685
  readonly pathPattern: "^/track/?$";
4684
4686
  readonly params: readonly ["id"];
4685
4687
  }];
4686
- readonly detectionRules: readonly [];
4688
+ readonly detectionRules: readonly [{
4689
+ readonly pattern: "^[A-Z]{2}\\d{9}LV$";
4690
+ readonly confidence: "low";
4691
+ readonly checksum: "s10";
4692
+ readonly preferred: true;
4693
+ }];
4687
4694
  };
4688
4695
  readonly "lbc-express": {
4689
4696
  readonly displayName: "LBC Express";
@@ -6939,7 +6939,9 @@ export const CARRIER_CATALOG = {
6939
6939
  "timezone": "Europe/Riga",
6940
6940
  "tracking": {
6941
6941
  "mode": "automatic",
6942
- "adapter": "universal"
6942
+ "adapter": "latvijas-pasts",
6943
+ "recognitionRank": 820,
6944
+ "localClocks": true
6943
6945
  },
6944
6946
  "canaryUrl": "https://www.pasts.lv/izsekot-sutijumu",
6945
6947
  "trackingUrlTemplate": "https://mans.pasts.lv/track?locale=lv-LV&id={trackingNumber}",
@@ -6955,7 +6957,14 @@ export const CARRIER_CATALOG = {
6955
6957
  ]
6956
6958
  }
6957
6959
  ],
6958
- "detectionRules": []
6960
+ "detectionRules": [
6961
+ {
6962
+ "pattern": "^[A-Z]{2}\\d{9}LV$",
6963
+ "confidence": "low",
6964
+ "checksum": "s10",
6965
+ "preferred": true
6966
+ }
6967
+ ]
6959
6968
  },
6960
6969
  "lbc-express": {
6961
6970
  "displayName": "LBC Express",
@@ -8909,7 +8918,7 @@ export const DETECTION_RULE_IDS = {
8909
8918
  "j-and-t-cargo": ["j-and-t-cargo-1", "j-and-t-cargo-2"],
8910
8919
  "jitsu": [],
8911
8920
  "kuehne-nagel": ["kuehne-nagel-1", "kuehne-nagel-2"],
8912
- "latvijas-pasts": [],
8921
+ "latvijas-pasts": ["latvijas-pasts-s10"],
8913
8922
  "lbc-express": ["lbc-domestic"],
8914
8923
  "lietuvos-pastas": ["lietuvos-pastas-1"],
8915
8924
  "loomis-express": ["loomis-express-1", "loomis-express-2"],
@@ -51,6 +51,7 @@ export const CARRIER_RECOGNITION_RANKS = {
51
51
  "j-and-t": 90,
52
52
  "j-and-t-cargo": 470,
53
53
  "la-poste": 800,
54
+ "latvijas-pasts": 820,
54
55
  "matkahuolto": 690,
55
56
  "mng-kargo": 740,
56
57
  "nacex": 410,
@@ -88,6 +88,7 @@ import { adapter as jdLogistics } from '../carriers/jd-logistics/adapter.js';
88
88
  import { adapter as koreaPost } from '../carriers/korea-post/adapter.js';
89
89
  import { adapter as laPoste } from '../carriers/la-poste/adapter.js';
90
90
  import { adapter as landmarkGlobal } from '../carriers/landmark-global/adapter.js';
91
+ import { adapter as latvijasPasts } from '../carriers/latvijas-pasts/adapter.js';
91
92
  import { adapter as lbcExpress } from '../carriers/lbc-express/adapter.js';
92
93
  import { adapter as matkahuolto } from '../carriers/matkahuolto/adapter.js';
93
94
  import { adapter as mngKargo } from '../carriers/mng-kargo/adapter.js';
@@ -240,6 +241,7 @@ export const REGISTRY = {
240
241
  "korea-post": koreaPost,
241
242
  "la-poste": laPoste,
242
243
  "landmark-global": landmarkGlobal,
244
+ "latvijas-pasts": latvijasPasts,
243
245
  "lbc-express": lbcExpress,
244
246
  "matkahuolto": matkahuolto,
245
247
  "mng-kargo": mngKargo,
@@ -418,7 +420,7 @@ export const REGISTRY = {
418
420
  "kuehne-nagel": "universal",
419
421
  "la-poste": "la-poste",
420
422
  "landmark-global": "landmark-global",
421
- "latvijas-pasts": "universal",
423
+ "latvijas-pasts": "latvijas-pasts",
422
424
  "lbc-express": "lbc-express",
423
425
  "lietuvos-pastas": "universal",
424
426
  "loomis-express": "universal",
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.92.0"
5
+ "version": "0.93.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.92.0-main.985",
3
+ "version": "0.93.0-main.987",
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",