universal-parcel-scraper 0.90.0-main.979 → 0.91.0-main.981

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 **148 dedicated adapters** and **5 universal fallbacks**
16
+ **3,500+ carriers** through **149 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,072 such statuses, each filed under one stage.
158
+ The carrier folders record 4,083 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
@@ -7590,7 +7590,8 @@
7590
7590
  "timezone": "Europe/Ljubljana",
7591
7591
  "tracking": {
7592
7592
  "mode": "automatic",
7593
- "adapter": "universal"
7593
+ "adapter": "posta-slovenije",
7594
+ "recognitionRank": 790
7594
7595
  },
7595
7596
  "canaryUrl": "https://moja.posta.si/tracking",
7596
7597
  "trackingUrlTemplate": "https://moja.posta.si/tracking?trackingNumber={trackingNumber}",
@@ -7606,7 +7607,14 @@
7606
7607
  ]
7607
7608
  }
7608
7609
  ],
7609
- "detectionRules": []
7610
+ "detectionRules": [
7611
+ {
7612
+ "pattern": "^[A-Z]{2}\\d{9}SI$",
7613
+ "confidence": "low",
7614
+ "checksum": "s10",
7615
+ "preferred": true
7616
+ }
7617
+ ]
7610
7618
  },
7611
7619
  "posta-srbije": {
7612
7620
  "displayName": "Pošta Srbije",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "catalog": 208,
3
- "activeAdapters": 148,
3
+ "activeAdapters": 149,
4
4
  "countries": 66,
5
5
  "referenceCarriers": 100,
6
6
  "comparison": {
@@ -25888,6 +25888,7 @@
25888
25888
  "intl-post"
25889
25889
  ],
25890
25890
  "asked": [
25891
+ "posta-slovenije",
25891
25892
  "chronopost"
25892
25893
  ]
25893
25894
  },
@@ -25899,6 +25900,7 @@
25899
25900
  "intl-post"
25900
25901
  ],
25901
25902
  "asked": [
25903
+ "posta-slovenije",
25902
25904
  "chronopost"
25903
25905
  ]
25904
25906
  },
@@ -25910,6 +25912,7 @@
25910
25912
  "intl-post"
25911
25913
  ],
25912
25914
  "asked": [
25915
+ "posta-slovenije",
25913
25916
  "chronopost"
25914
25917
  ]
25915
25918
  },
@@ -26031,6 +26034,7 @@
26031
26034
  "intl-post"
26032
26035
  ],
26033
26036
  "asked": [
26037
+ "posta-slovenije",
26034
26038
  "chronopost"
26035
26039
  ]
26036
26040
  },
@@ -27080,6 +27084,7 @@
27080
27084
  "intl-post"
27081
27085
  ],
27082
27086
  "asked": [
27087
+ "posta-slovenije",
27083
27088
  "chronopost"
27084
27089
  ]
27085
27090
  },
@@ -30489,6 +30494,7 @@
30489
30494
  "intl-post"
30490
30495
  ],
30491
30496
  "asked": [
30497
+ "posta-slovenije",
30492
30498
  "chronopost"
30493
30499
  ]
30494
30500
  },
@@ -30680,6 +30686,7 @@
30680
30686
  "intl-post"
30681
30687
  ],
30682
30688
  "asked": [
30689
+ "posta-slovenije",
30683
30690
  "chronopost"
30684
30691
  ]
30685
30692
  },
@@ -34145,6 +34152,7 @@
34145
34152
  "intl-post"
34146
34153
  ],
34147
34154
  "asked": [
34155
+ "posta-slovenije",
34148
34156
  "chronopost"
34149
34157
  ]
34150
34158
  },
@@ -6025,7 +6025,8 @@ var be = {
6025
6025
  timezone: "Europe/Ljubljana",
6026
6026
  tracking: {
6027
6027
  mode: "automatic",
6028
- adapter: "universal"
6028
+ adapter: "posta-slovenije",
6029
+ recognitionRank: 790
6029
6030
  },
6030
6031
  canaryUrl: "https://moja.posta.si/tracking",
6031
6032
  trackingUrlTemplate: "https://moja.posta.si/tracking?trackingNumber={trackingNumber}",
@@ -6035,7 +6036,12 @@ var be = {
6035
6036
  pathPattern: "^/tracking/?$",
6036
6037
  params: ["trackingNumber"]
6037
6038
  }],
6038
- detectionRules: []
6039
+ detectionRules: [{
6040
+ pattern: "^[A-Z]{2}\\d{9}SI$",
6041
+ confidence: "low",
6042
+ checksum: "s10",
6043
+ preferred: !0
6044
+ }]
6039
6045
  },
6040
6046
  "posta-srbije": {
6041
6047
  displayName: "Pošta Srbije",
@@ -7153,7 +7159,7 @@ var be = {
7153
7159
  paquetexpress: [],
7154
7160
  passport: ["passport-1"],
7155
7161
  "post-luxembourg": [],
7156
- "posta-slovenije": [],
7162
+ "posta-slovenije": ["posta-slovenije-s10"],
7157
7163
  "posta-srbije": [],
7158
7164
  ppl: [],
7159
7165
  ptt: ["ptt-1", "ptt-s10"],
@@ -11933,6 +11939,7 @@ var bc = {
11933
11939
  ontrac: 210,
11934
11940
  "orlen-paczka": 490,
11935
11941
  "poczta-polska": 310,
11942
+ "posta-slovenije": 790,
11936
11943
  "poste-italiane": 460,
11937
11944
  posti: 270,
11938
11945
  postlogistics: 260,
@@ -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 POSTA_SLOVENIJE_ENDPOINT = "https://moja.posta.si/api/v1/parcels/track/";
5
+ export declare const POSTA_SLOVENIJE_USER_AGENT = "MojaPosta/2.5.0 (android)";
6
+ export declare class PostaSlovenijeTracker {
7
+ private readonly options;
8
+ constructor(options?: {
9
+ fetcher?: typeof fetch;
10
+ recorder?: StepRecorder;
11
+ });
12
+ fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
13
+ }
14
+ export declare const adapter: AdapterFactory;
@@ -0,0 +1,70 @@
1
+ import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
2
+ import { ChallengeError, NotFoundError, SchemaError, TransportError } 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 } from '../../core/transport/index.js';
7
+ import { isNotFoundReply, normalizePostaSlovenijeNumber, parsePostaSlovenije, POSTA_SLOVENIJE_MAX_BYTES, PROVIDER } from './parser.js';
8
+ // The guest lookup of Pošta Slovenije's MojaPošta app and web tracker. The
9
+ // web tracker sends an invisible reCAPTCHA token with it; the app sends none,
10
+ // and the service answers a client that names itself as the app.
11
+ export const POSTA_SLOVENIJE_ENDPOINT = 'https://moja.posta.si/api/v1/parcels/track/';
12
+ // The app's own User-Agent, "MojaPosta/<version> (<platform>)". The service
13
+ // refuses other clients without a reCAPTCHA token.
14
+ export const POSTA_SLOVENIJE_USER_AGENT = 'MojaPosta/2.5.0 (android)';
15
+ // The service's own JSON refusals: an unknown number, and a client it does not let in.
16
+ const ANSWERS = [403, 404];
17
+ // An edge or firewall page, which can come back with HTTP 200.
18
+ const BLOCKED = /<title>\s*(?:Just a moment|Access Denied|Attention Required|403 Forbidden)|_cf_chl_opt|cf-turnstile|Azure Front Door|Request blocked/i;
19
+ export class PostaSlovenijeTracker {
20
+ options;
21
+ constructor(options = {}) {
22
+ this.options = options;
23
+ }
24
+ async fetch(raw, context = {}) {
25
+ const number = normalizePostaSlovenijeNumber(raw);
26
+ const read = async (signal, timeoutMs) => {
27
+ // No Content-Type: on a GET the service reads it as an empty JSON body and answers 400.
28
+ const { response, bytes } = await fetchBounded(`${POSTA_SLOVENIJE_ENDPOINT}?TrackingNumber=${encodeURIComponent(number)}`, {
29
+ signal, headers: { Accept: 'application/json', 'User-Agent': POSTA_SLOVENIJE_USER_AGENT },
30
+ }, { provider: PROVIDER, timeoutMs: Math.max(1, Math.floor(timeoutMs)), maxBytes: POSTA_SLOVENIJE_MAX_BYTES,
31
+ allowHttpStatuses: ANSWERS, fetcher: this.options.fetcher });
32
+ let payload;
33
+ try {
34
+ payload = parseJsonBytes(bytes, PROVIDER);
35
+ }
36
+ catch (cause) {
37
+ if (response.status === 403 || BLOCKED.test(decodeText(bytes.subarray(0, 64_000)))) {
38
+ throw new ChallengeError(PROVIDER, 'Pošta Slovenije answered with a block page', { cause });
39
+ }
40
+ if (response.status === 404)
41
+ throw new TransportError(PROVIDER, 'Pošta Slovenije tracking service is unavailable', { cause });
42
+ throw new SchemaError(PROVIDER, 'Pošta Slovenije returned invalid tracking JSON', { cause });
43
+ }
44
+ if (response.status === 404) {
45
+ // Only the service's own notice for this number is a missing parcel; a missing route is not.
46
+ if (isNotFoundReply(payload, number))
47
+ throw new NotFoundError(PROVIDER);
48
+ throw new TransportError(PROVIDER, 'Pošta Slovenije tracking service is unavailable');
49
+ }
50
+ if (response.status === 403)
51
+ throw new ChallengeError(PROVIDER, 'Pošta Slovenije refused the app\'s guest lookup');
52
+ return parsePostaSlovenije(payload, number);
53
+ };
54
+ // A request that fails to reach Pošta Slovenije, or hangs, gets one more
55
+ // try: the first gets half of the budget, so the retry fits in the rest.
56
+ return runSteps({ carrier: 'posta-slovenije', budgetMs: context.budgetMs ?? 15_000, signal: context.signal,
57
+ recorder: this.options.recorder ?? NOOP_RECORDER }, [
58
+ { id: 'direct', run: ({ signal, remainingMs }) => read(signal, firstAttemptMs(remainingMs)) },
59
+ networkRetryStep(({ signal, remainingMs }) => read(signal, remainingMs)),
60
+ ]);
61
+ }
62
+ }
63
+ export const adapter = (environment) => {
64
+ const tracker = new PostaSlovenijeTracker({ fetcher: environment.fetcher, recorder: environment.recorder });
65
+ return {
66
+ id: 'posta-slovenije', recordsSteps: true, steps: ['direct', 'retry'],
67
+ track: (input, context) => tracker.fetch(input.number, context),
68
+ recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizePostaSlovenijeNumber(number))),
69
+ };
70
+ };
@@ -18,18 +18,53 @@
18
18
  "timezone": "Europe/Ljubljana",
19
19
  "tracking": {
20
20
  "mode": "automatic",
21
- "adapter": "universal",
22
- "steps": []
21
+ "adapter": "posta-slovenije",
22
+ "steps": [
23
+ "direct",
24
+ "retry"
25
+ ],
26
+ "recognition": {
27
+ "rank": 790
28
+ }
23
29
  },
24
- "capabilities": [],
30
+ "capabilities": [
31
+ "history",
32
+ "location",
33
+ "provider_code",
34
+ "delivered_at",
35
+ "pickup_point"
36
+ ],
25
37
  "portal": {
26
38
  "url": "https://moja.posta.si/tracking?trackingNumber={trackingNumber}",
27
39
  "searchUrl": "https://moja.posta.si/tracking",
28
40
  "canaryUrl": "https://moja.posta.si/tracking",
29
- "shows": [],
30
- "scraped": [],
31
- "discarded": [],
32
- "unavailable": []
41
+ "shows": [
42
+ "status",
43
+ "history",
44
+ "location",
45
+ "postcodes"
46
+ ],
47
+ "scraped": [
48
+ "status",
49
+ "history",
50
+ "location",
51
+ "provider_code",
52
+ "delivered_at",
53
+ "pickup_point"
54
+ ],
55
+ "discarded": [
56
+ "postcodes",
57
+ "pickup_point_address",
58
+ "signatory",
59
+ "pickup_code"
60
+ ],
61
+ "unavailable": [
62
+ "eta",
63
+ "weight",
64
+ "service_name",
65
+ "sender_name",
66
+ "proof_of_delivery"
67
+ ]
33
68
  },
34
69
  "links": [
35
70
  {
@@ -42,5 +77,13 @@
42
77
  ]
43
78
  }
44
79
  ],
45
- "detection": []
80
+ "detection": [
81
+ {
82
+ "id": "posta-slovenije-s10",
83
+ "pattern": "^[A-Z]{2}\\d{9}SI$",
84
+ "confidence": "low",
85
+ "preferred": true,
86
+ "checksum": "s10"
87
+ }
88
+ ]
46
89
  }
@@ -67,13 +67,13 @@
67
67
  "access": "full_page"
68
68
  },
69
69
  "context": {
70
- "assessment": "candidate"
70
+ "assessment": "confirmed"
71
71
  },
72
72
  "expect": {
73
73
  "carrier": "intl-post",
74
74
  "confidence": "high"
75
75
  },
76
- "note": "ParcelPanel's Slovenia Post page offered it as a sample to try on 11 Oct 2026; it is not described as a live shipment."
76
+ "note": "ParcelPanel's Slovenia Post page offered it as a sample to try. Pošta Slovenije's own tracking returns its history; detection selects the generic postal carrier."
77
77
  },
78
78
  {
79
79
  "number": "CF369007664SI",
@@ -86,13 +86,13 @@
86
86
  "access": "full_page"
87
87
  },
88
88
  "context": {
89
- "assessment": "candidate"
89
+ "assessment": "confirmed"
90
90
  },
91
91
  "expect": {
92
92
  "carrier": "intl-post",
93
93
  "confidence": "high"
94
94
  },
95
- "note": "ParcelPanel's Slovenia Post page offered it as a sample to try on 11 Oct 2026; it is not described as a live shipment."
95
+ "note": "ParcelPanel's Slovenia Post page offered it as a sample to try. Pošta Slovenije's own tracking returns its history; detection selects the generic postal carrier."
96
96
  },
97
97
  {
98
98
  "number": "CF369340877SI",
@@ -105,13 +105,13 @@
105
105
  "access": "full_page"
106
106
  },
107
107
  "context": {
108
- "assessment": "candidate"
108
+ "assessment": "confirmed"
109
109
  },
110
110
  "expect": {
111
111
  "carrier": "intl-post",
112
112
  "confidence": "high"
113
113
  },
114
- "note": "ParcelPanel's Slovenia Post page offered it as a sample to try on 11 Oct 2026; it is not described as a live shipment."
114
+ "note": "ParcelPanel's Slovenia Post page offered it as a sample to try. Pošta Slovenije's own tracking returns its history; detection selects the generic postal carrier."
115
115
  },
116
116
  {
117
117
  "number": "LH013496345SI",
@@ -143,13 +143,13 @@
143
143
  "access": "full_page"
144
144
  },
145
145
  "context": {
146
- "assessment": "candidate"
146
+ "assessment": "confirmed"
147
147
  },
148
148
  "expect": {
149
149
  "carrier": "intl-post",
150
150
  "confidence": "high"
151
151
  },
152
- "note": "ParcelPanel's Slovenia Post page offered it as a sample to try on 11 Oct 2026; it is not described as a live shipment."
152
+ "note": "ParcelPanel's Slovenia Post page offered it as a sample to try. Pošta Slovenije's own tracking returns its history; detection selects the generic postal carrier."
153
153
  }
154
154
  ]
155
155
  }
@@ -0,0 +1,17 @@
1
+ import type { CarrierResult } from '../../core/result/index.js';
2
+ export declare const PROVIDER = "Po\u0161ta Slovenije";
3
+ export declare const POSTA_SLOVENIJE_MAX_BYTES = 512000;
4
+ /**
5
+ * Pošta Slovenije tracks UPU S10 items: the ones it issues, which end in SI,
6
+ * and the ones other posts hand it to deliver.
7
+ */
8
+ export declare function normalizePostaSlovenijeNumber(raw: string): string;
9
+ /** The service's not-found reply names the number it looked for: "Record not found for Id '…'". */
10
+ export declare function isNotFoundReply(payload: unknown, number: string): boolean;
11
+ /**
12
+ * The guest lookup's reply for one number: the parcel and its scans, oldest
13
+ * first, each with a code, wording in Slovenian and English, a UTC clock and
14
+ * the post office it happened at. Sender, recipient, signatory, pickup codes
15
+ * and addresses are left out.
16
+ */
17
+ export declare function parsePostaSlovenije(payload: unknown, rawNumber: string): CarrierResult;
@@ -0,0 +1,131 @@
1
+ import { isValidS10TrackingNumber, normalizeTrackingNumber } from '../../core/detection/index.js';
2
+ import { IndeterminateError, InvalidInputError, SchemaError } from '../../core/errors/index.js';
3
+ import { lastMovement } from '../../core/result/pickup.js';
4
+ import { classifyWording, languageStageStatus } from '../../core/status/index.js';
5
+ import { explicitOffsetTime } from '../../core/time/index.js';
6
+ import { clean } from '../../core/transport/index.js';
7
+ import { isRecord } from '../../core/types.js';
8
+ import { postaSlovenijeStage, startsReturn } from './status.js';
9
+ export const PROVIDER = 'Pošta Slovenije';
10
+ // A long history is a few kilobytes.
11
+ export const POSTA_SLOVENIJE_MAX_BYTES = 512_000;
12
+ const MAX_ROWS = 500;
13
+ const MAX_EVENTS = 100;
14
+ const CODE = /^[A-Z0-9]{1,20}$/;
15
+ // The unit type of a post office; a parcel locker is 8.
16
+ const POST_OFFICE = 99;
17
+ /**
18
+ * Pošta Slovenije tracks UPU S10 items: the ones it issues, which end in SI,
19
+ * and the ones other posts hand it to deliver.
20
+ */
21
+ export function normalizePostaSlovenijeNumber(raw) {
22
+ const number = normalizeTrackingNumber(raw);
23
+ if (isValidS10TrackingNumber(number))
24
+ return number;
25
+ throw new InvalidInputError(PROVIDER, 'Pošta Slovenije tracks S10 item codes');
26
+ }
27
+ /** The service's not-found reply names the number it looked for: "Record not found for Id '…'". */
28
+ export function isNotFoundReply(payload, number) {
29
+ if (!isRecord(payload) || payload.errorCode !== 'error.not.found')
30
+ return false;
31
+ const named = /\bId '([^']*)'/.exec(clean(payload.errorMessage, 200))?.[1];
32
+ return named === undefined || named.trim().toUpperCase() === number;
33
+ }
34
+ /** The scan's English heading, its detail line and the next step after a failed delivery, as the tracker lists them. */
35
+ function wordingOf(row) {
36
+ const parts = [clean(row.codeDescriptionEN, 300) || clean(row.codeDescriptionSI, 300),
37
+ clean(row.codeDetailedDescriptionEN, 300), clean(row.nondeliveryActionEN, 300)]
38
+ .map(part => part.replace(/[.\s]+$/, '')).filter(Boolean);
39
+ return [...new Set(parts)].join('. ');
40
+ }
41
+ /**
42
+ * The guest lookup's reply for one number: the parcel and its scans, oldest
43
+ * first, each with a code, wording in Slovenian and English, a UTC clock and
44
+ * the post office it happened at. Sender, recipient, signatory, pickup codes
45
+ * and addresses are left out.
46
+ */
47
+ export function parsePostaSlovenije(payload, rawNumber) {
48
+ const number = normalizePostaSlovenijeNumber(rawNumber);
49
+ const result = isRecord(payload) ? payload.result : undefined;
50
+ if (!isRecord(result) || !isRecord(result.parcel))
51
+ throw new SchemaError(PROVIDER, 'Pošta Slovenije did not return a parcel');
52
+ const parcel = result.parcel;
53
+ if (typeof parcel.trackingNumber !== 'string' || parcel.trackingNumber.trim().toUpperCase() !== number) {
54
+ throw new SchemaError(PROVIDER, 'Pošta Slovenije answered for a different parcel');
55
+ }
56
+ const rows = result.statuses ?? [];
57
+ if (!Array.isArray(rows) || rows.length > MAX_ROWS)
58
+ throw new SchemaError(PROVIDER, 'Pošta Slovenije returned an invalid parcel history');
59
+ const scans = [];
60
+ const seen = new Set();
61
+ rows.forEach((row, index) => {
62
+ if (!isRecord(row))
63
+ throw new SchemaError(PROVIDER, 'Pošta Slovenije returned an invalid scan');
64
+ const code = clean(row.code, 40);
65
+ const at = explicitOffsetTime(row.statusDateTime);
66
+ if (!CODE.test(code) || !at)
67
+ throw new SchemaError(PROVIDER, 'Pošta Slovenije returned a scan without a code or clock');
68
+ // The post office's name; its number, a postcode, is left out.
69
+ const place = clean(row.postalName, 120);
70
+ const event = { time: at.iso, description: wordingOf(row) || code, provider_code: code, ...(place ? { location: place } : {}) };
71
+ const identity = JSON.stringify(event);
72
+ if (seen.has(identity))
73
+ return;
74
+ seen.add(identity);
75
+ scans.push({ event, at: at.timestamp, code, postOffice: row.unitType === POST_OFFICE, index });
76
+ });
77
+ // Newest first; scans at the same instant follow the reply's order, reversed.
78
+ scans.sort((left, right) => right.at - left.at || right.index - left.index);
79
+ // Oldest first: a delivery after the item was sent back reaches its sender.
80
+ let returning = false;
81
+ for (const scan of [...scans].reverse()) {
82
+ const description = scan.event.description;
83
+ const classified = postaSlovenijeStage(scan.code, description);
84
+ if (startsReturn(scan.code, description))
85
+ returning = true;
86
+ if (!classified)
87
+ continue;
88
+ const stage = classified.stage === 'delivered' && returning ? 'returned' : classified.stage;
89
+ Object.assign(scan.event, { stage, stage_source: classified.source });
90
+ }
91
+ const events = scans.map(scan => scan.event);
92
+ if (!scans.length)
93
+ return summaryOf(parcel);
94
+ const decider = scans[0];
95
+ const stage = decider.event.stage;
96
+ // The post office an item waits at is its pickup point. A delivered item
97
+ // was collected there when it waited there last before the delivery.
98
+ const earlier = scans.slice(1);
99
+ const waiting = stage === 'ready_for_pickup' ? decider
100
+ : stage === 'delivered' && lastMovement(earlier.map(scan => scan.event)) === 'ready_for_pickup'
101
+ ? earlier.find(scan => scan.event.stage === 'ready_for_pickup') : undefined;
102
+ const point = waiting?.postOffice ? waiting.event.location ?? '' : '';
103
+ return {
104
+ status: stage ? languageStageStatus(stage) : 'unknown',
105
+ ...(stage ? { current_stage: stage, current_stage_source: decider.event.stage_source } : {}),
106
+ last_status_text: decider.event.description,
107
+ last_update: decider.event.time,
108
+ expected_delivery: null,
109
+ ...(stage === 'delivered' ? { delivered_at: decider.event.time } : {}),
110
+ ...(point ? { pickup_point: point } : {}),
111
+ events: events.slice(0, MAX_EVENTS),
112
+ ...(events.length > MAX_EVENTS ? { history_truncated: true } : {}),
113
+ };
114
+ }
115
+ /** A parcel without scans: its own heading and the clock of its state. */
116
+ function summaryOf(parcel) {
117
+ const wording = clean(parcel.codeDescriptionEN, 300) || clean(parcel.codeDescriptionSI, 300);
118
+ if (!wording)
119
+ throw new IndeterminateError(PROVIDER, 'Pošta Slovenije returned a parcel without a state or scans');
120
+ const classified = classifyWording(wording, 'in_transit');
121
+ const stage = classified.source === 'none' ? undefined : classified.stage;
122
+ return {
123
+ status: stage ? languageStageStatus(stage) : 'unknown',
124
+ ...(stage ? { current_stage: stage, current_stage_source: classified.source } : {}),
125
+ last_status_text: wording,
126
+ summary_only: true,
127
+ last_update: explicitOffsetTime(parcel.currentStatusDateTime)?.iso ?? null,
128
+ expected_delivery: null,
129
+ events: [],
130
+ };
131
+ }
@@ -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
+ /** Whether a scan sends the item back to its sender: a delivery after it reaches the sender. */
4
+ export declare function startsReturn(code: string, wording: string): boolean;
5
+ /** The stage the carrier's code gives a scan, if the map knows it. */
6
+ export declare function postaSlovenijeCodeStage(code: string, wording: string): Stage | undefined;
7
+ /**
8
+ * A scan's stage from its code, else from the shared rules for its English
9
+ * wording. Wording neither knows stays without a stage, so it remains visible.
10
+ */
11
+ export declare function postaSlovenijeStage(code: string, wording: string): ClassifiedWording | null;
12
+ /** Every scan carries a code; V reads its next step from the wording. */
13
+ export declare const statusMap: CarrierStatusMap;
@@ -0,0 +1,56 @@
1
+ import { classifyWording } from '../../core/status/index.js';
2
+ /**
3
+ * Pošta Slovenije names each scan with a short code of its own, such as VL,
4
+ * and words it in Slovenian and English, with a detail line and, after a
5
+ * failed delivery, what happens next. The Slovenian wording is the more exact
6
+ * one, and the notes below follow it.
7
+ */
8
+ const CODES = {
9
+ // The sender's data arrived; the item is not handed over yet.
10
+ NAJ: 'registered',
11
+ // The item arrived at Pošta Slovenije and was sent on.
12
+ S: 'accepted',
13
+ // "Pošiljka bo predvidoma dostavljena danes": with the postman, on its way to the address.
14
+ N: 'out_for_delivery',
15
+ // On its way to the parcel locker the recipient chose.
16
+ VLPAKP: 'out_for_delivery',
17
+ // Put in the recipient's letterbox or post office box, or handed over at the address.
18
+ VL: 'delivered',
19
+ // Nobody collected it from the parcel locker in time; it goes to a post office.
20
+ POTPKTM: 'failed_attempt',
21
+ // "Pošiljka ni bila prevzeta": not collected. What follows is in the scan's next step.
22
+ V: 'failed_attempt',
23
+ // It could not be delivered and goes to the post office, where it can be collected.
24
+ VDO: 'failed_attempt',
25
+ // Waiting at the post office for the recipient.
26
+ IZR: 'ready_for_pickup',
27
+ PVI: 'ready_for_pickup',
28
+ };
29
+ /** The next step a failed delivery names when the item goes back to its sender. */
30
+ const RETURNING = /\breturned to the sender\b/i;
31
+ /** Whether a scan sends the item back to its sender: a delivery after it reaches the sender. */
32
+ export function startsReturn(code, wording) {
33
+ return code === 'V' && RETURNING.test(wording);
34
+ }
35
+ /** The stage the carrier's code gives a scan, if the map knows it. */
36
+ export function postaSlovenijeCodeStage(code, wording) {
37
+ if (startsReturn(code, wording))
38
+ return 'exception';
39
+ return Object.hasOwn(CODES, code) ? CODES[code] : undefined;
40
+ }
41
+ /**
42
+ * A scan's stage from its code, else from the shared rules for its English
43
+ * wording. Wording neither knows stays without a stage, so it remains visible.
44
+ */
45
+ export function postaSlovenijeStage(code, wording) {
46
+ const mapped = postaSlovenijeCodeStage(code, wording);
47
+ if (mapped)
48
+ return { stage: mapped, source: 'carrier_map' };
49
+ const worded = classifyWording(wording, 'in_transit');
50
+ return worded.source === 'none' ? null : worded;
51
+ }
52
+ /** Every scan carries a code; V reads its next step from the wording. */
53
+ export const statusMap = {
54
+ stage: (code, wording) => (code ? postaSlovenijeCodeStage(code, wording) : undefined),
55
+ gaps: [],
56
+ };
@@ -1,4 +1,16 @@
1
1
  {
2
2
  "carrier": "posta-slovenije",
3
- "entries": []
3
+ "entries": [
4
+ { "code": "NAJ", "wording": "The item information has been received in the Pošta Slovenije system", "language": "en", "stage": "registered", "confirmedBy": "live", "note": "The sender's data arrived; the detail line says the item is not handed over yet." },
5
+ { "code": "S", "wording": "The item has arrived at Pošta Slovenije", "language": "en", "stage": "accepted", "confirmedBy": "live", "note": "Accepted at the post office or centre the scan names. Inbound items get it when they reach Slovenia." },
6
+ { "code": "N", "wording": "The item is expected to be delivered today", "language": "en", "stage": "out_for_delivery", "confirmedBy": "live", "note": "Seen with the detail line \"The item is out for doorstep delivery\"." },
7
+ { "code": "VLPAKP", "wording": "The item is on its way to the selected parcel locker", "language": "en", "stage": "out_for_delivery", "confirmedBy": "live" },
8
+ { "code": "VL", "wording": "The item was delivered to the mailbox / P.O. box", "language": "en", "stage": "delivered", "confirmedBy": "live", "note": "Seen with the detail line \"The item was delivered to the doorstep\". After an item was sent back, the adapter reads it as returned." },
9
+ { "code": "POTPKTM", "wording": "Unsuccessful delivery", "language": "en", "stage": "failed_attempt", "confirmedBy": "live", "note": "The Slovenian wording is more exact: nobody collected the item from the parcel locker in time." },
10
+ { "code": "V", "wording": "Unsuccessful delivery attempt", "language": "en", "stage": "failed_attempt", "confirmedBy": "live", "note": "Not collected. The next step follows, such as \"The item is ready for pickup at the post office\"." },
11
+ { "code": "V", "wording": "Unsuccessful delivery attempt. Item is being returned to the sender", "language": "en", "stage": "exception", "confirmedBy": "live", "note": "Not collected and sent back to the sender. A delivery after it reaches the sender." },
12
+ { "code": "VDO", "wording": "Unsuccessful delivery attempt", "language": "en", "stage": "failed_attempt", "confirmedBy": "live", "note": "Could not be delivered; seen with the next step \"The item will be redirected to the post office\"." },
13
+ { "code": "IZR", "wording": "The item is ready for pickup at the post office", "language": "en", "stage": "ready_for_pickup", "confirmedBy": "live", "note": "Its place names the post office the item waits at." },
14
+ { "code": "PVI", "wording": "The item is ready for pickup at the post office", "language": "en", "stage": "ready_for_pickup", "confirmedBy": "live", "note": "Its place names the post office the item waits at, after a failed delivery." }
15
+ ]
4
16
  }
@@ -49,6 +49,7 @@ import { statusMap as oldDominion } from '../../carriers/old-dominion/status.js'
49
49
  import { statusMap as omgo } from '../../carriers/omgo/status.js';
50
50
  import { statusMap as omniva } from '../../carriers/omniva/status.js';
51
51
  import { statusMap as orlenPaczka } from '../../carriers/orlen-paczka/status.js';
52
+ import { statusMap as postaSlovenije } from '../../carriers/posta-slovenije/status.js';
52
53
  import { statusMap as postlogistics } from '../../carriers/postlogistics/status.js';
53
54
  import { statusMap as ppl } from '../../carriers/ppl/status.js';
54
55
  import { statusMap as ptt } from '../../carriers/ptt/status.js';
@@ -75,7 +76,7 @@ export const STATUS_MAPS = {
75
76
  'dpd-fr': dpdFr, 'dpd-ie': dpdIe, 'dpd-pl': dpdPl, econt, elta, emile, 'express-one': expressOne, 'fan-courier': fanCourier, foxpost,
76
77
  'geniki-taxydromiki': geniki, 'gls-es': glsEs, 'gls-it': glsIt, 'gofo-fr': gofoFr, 'gofo-it': gofoIt, helthjem, 'j-and-t': jAndT,
77
78
  'j-and-t-cargo': jntCargo, 'la-poste': laPoste, matkahuolto, 'mng-kargo': mngKargo, 'old-dominion': oldDominion, omgo, omniva,
78
- 'orlen-paczka': orlenPaczka, postlogistics, ppl, ptt, sagawa, sameday, speedx, speedy, sto, 'swiss-post': swissPost, 'thailand-post': thailandPost,
79
+ 'orlen-paczka': orlenPaczka, 'posta-slovenije': postaSlovenije, postlogistics, ppl, ptt, sagawa, sameday, speedx, speedy, sto, 'swiss-post': swissPost, 'thailand-post': thailandPost,
79
80
  tnt, ups, venipak, 'vinted-go': vintedGo, yunexpress, 'yurtici-kargo': yurticiKargo,
80
81
  };
81
82
  /** The carrier the app files the universal providers' scans under. */
@@ -7590,7 +7590,8 @@
7590
7590
  "timezone": "Europe/Ljubljana",
7591
7591
  "tracking": {
7592
7592
  "mode": "automatic",
7593
- "adapter": "universal"
7593
+ "adapter": "posta-slovenije",
7594
+ "recognitionRank": 790
7594
7595
  },
7595
7596
  "canaryUrl": "https://moja.posta.si/tracking",
7596
7597
  "trackingUrlTemplate": "https://moja.posta.si/tracking?trackingNumber={trackingNumber}",
@@ -7606,7 +7607,14 @@
7606
7607
  ]
7607
7608
  }
7608
7609
  ],
7609
- "detectionRules": []
7610
+ "detectionRules": [
7611
+ {
7612
+ "pattern": "^[A-Z]{2}\\d{9}SI$",
7613
+ "confidence": "low",
7614
+ "checksum": "s10",
7615
+ "preferred": true
7616
+ }
7617
+ ]
7610
7618
  },
7611
7619
  "posta-srbije": {
7612
7620
  "displayName": "Pošta Srbije",
@@ -5104,7 +5104,8 @@ export declare const CARRIER_CATALOG: {
5104
5104
  readonly timezone: "Europe/Ljubljana";
5105
5105
  readonly tracking: {
5106
5106
  readonly mode: "automatic";
5107
- readonly adapter: "universal";
5107
+ readonly adapter: "posta-slovenije";
5108
+ readonly recognitionRank: 790;
5108
5109
  };
5109
5110
  readonly canaryUrl: "https://moja.posta.si/tracking";
5110
5111
  readonly trackingUrlTemplate: "https://moja.posta.si/tracking?trackingNumber={trackingNumber}";
@@ -5114,7 +5115,12 @@ export declare const CARRIER_CATALOG: {
5114
5115
  readonly pathPattern: "^/tracking/?$";
5115
5116
  readonly params: readonly ["trackingNumber"];
5116
5117
  }];
5117
- readonly detectionRules: readonly [];
5118
+ readonly detectionRules: readonly [{
5119
+ readonly pattern: "^[A-Z]{2}\\d{9}SI$";
5120
+ readonly confidence: "low";
5121
+ readonly checksum: "s10";
5122
+ readonly preferred: true;
5123
+ }];
5118
5124
  };
5119
5125
  readonly "posta-srbije": {
5120
5126
  readonly displayName: "Pošta Srbije";
@@ -7591,7 +7591,8 @@ export const CARRIER_CATALOG = {
7591
7591
  "timezone": "Europe/Ljubljana",
7592
7592
  "tracking": {
7593
7593
  "mode": "automatic",
7594
- "adapter": "universal"
7594
+ "adapter": "posta-slovenije",
7595
+ "recognitionRank": 790
7595
7596
  },
7596
7597
  "canaryUrl": "https://moja.posta.si/tracking",
7597
7598
  "trackingUrlTemplate": "https://moja.posta.si/tracking?trackingNumber={trackingNumber}",
@@ -7607,7 +7608,14 @@ export const CARRIER_CATALOG = {
7607
7608
  ]
7608
7609
  }
7609
7610
  ],
7610
- "detectionRules": []
7611
+ "detectionRules": [
7612
+ {
7613
+ "pattern": "^[A-Z]{2}\\d{9}SI$",
7614
+ "confidence": "low",
7615
+ "checksum": "s10",
7616
+ "preferred": true
7617
+ }
7618
+ ]
7611
7619
  },
7612
7620
  "posta-srbije": {
7613
7621
  "displayName": "Pošta Srbije",
@@ -8911,7 +8919,7 @@ export const DETECTION_RULE_IDS = {
8911
8919
  "paquetexpress": [],
8912
8920
  "passport": ["passport-1"],
8913
8921
  "post-luxembourg": [],
8914
- "posta-slovenije": [],
8922
+ "posta-slovenije": ["posta-slovenije-s10"],
8915
8923
  "posta-srbije": [],
8916
8924
  "ppl": [],
8917
8925
  "ptt": ["ptt-1", "ptt-s10"],
@@ -60,6 +60,7 @@ export const CARRIER_RECOGNITION_RANKS = {
60
60
  "ontrac": 210,
61
61
  "orlen-paczka": 490,
62
62
  "poczta-polska": 310,
63
+ "posta-slovenije": 790,
63
64
  "poste-italiane": 460,
64
65
  "posti": 270,
65
66
  "postlogistics": 260,
@@ -106,6 +106,7 @@ import { adapter as packeta } from '../carriers/packeta/adapter.js';
106
106
  import { adapter as planzer } from '../carriers/planzer/adapter.js';
107
107
  import { adapter as pocztaPolska } from '../carriers/poczta-polska/adapter.js';
108
108
  import { adapter as posMalaysia } from '../carriers/pos-malaysia/adapter.js';
109
+ import { adapter as postaSlovenije } from '../carriers/posta-slovenije/adapter.js';
109
110
  import { adapter as posteItaliane } from '../carriers/poste-italiane/adapter.js';
110
111
  import { adapter as posti } from '../carriers/posti/adapter.js';
111
112
  import { adapter as postlogistics } from '../carriers/postlogistics/adapter.js';
@@ -256,6 +257,7 @@ export const REGISTRY = {
256
257
  "planzer": planzer,
257
258
  "poczta-polska": pocztaPolska,
258
259
  "pos-malaysia": posMalaysia,
260
+ "posta-slovenije": postaSlovenije,
259
261
  "poste-italiane": posteItaliane,
260
262
  "posti": posti,
261
263
  "postlogistics": postlogistics,
@@ -446,7 +448,7 @@ export const REGISTRY = {
446
448
  "poczta-polska": "poczta-polska",
447
449
  "pos-malaysia": "pos-malaysia",
448
450
  "post-luxembourg": "universal",
449
- "posta-slovenije": "universal",
451
+ "posta-slovenije": "posta-slovenije",
450
452
  "posta-srbije": "universal",
451
453
  "poste-italiane": "poste-italiane",
452
454
  "posti": "posti",
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.90.0"
5
+ "version": "0.91.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.90.0-main.979",
3
+ "version": "0.91.0-main.981",
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",