universal-parcel-scraper 0.91.0-main.981 → 0.92.0-main.985
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/data/catalog.json +11 -2
- package/data/coverage-summary.json +1 -1
- package/data/detection-golden.json +18 -0
- package/dist/browser/scraper.js +11 -3
- package/dist/carriers/allegro-one/adapter.d.ts +1 -1
- package/dist/carriers/allegro-one/adapter.js +3 -2
- package/dist/carriers/allegro-one/numbers.json +19 -0
- package/dist/carriers/allegro-one/parser.d.ts +1 -1
- package/dist/carriers/allegro-one/parser.js +6 -4
- package/dist/carriers/allegro-one/status.js +1 -0
- package/dist/carriers/allegro-one/statuses.json +1 -0
- package/dist/carriers/hrvatska-posta/adapter.d.ts +14 -0
- package/dist/carriers/hrvatska-posta/adapter.js +52 -0
- package/dist/carriers/hrvatska-posta/carrier.json +50 -8
- package/dist/carriers/hrvatska-posta/numbers.json +29 -29
- package/dist/carriers/hrvatska-posta/parser.d.ts +16 -0
- package/dist/carriers/hrvatska-posta/parser.js +170 -0
- package/dist/carriers/hrvatska-posta/status.d.ts +11 -0
- package/dist/carriers/hrvatska-posta/status.js +47 -0
- package/dist/carriers/hrvatska-posta/statuses.json +16 -1
- package/dist/core/catalog/statusMaps.js +3 -2
- package/dist/data/catalog.json +11 -2
- package/dist/generated/catalog.d.ts +9 -2
- package/dist/generated/catalog.js +12 -3
- package/dist/generated/recognition.js +1 -0
- package/dist/generated/registry.js +3 -1
- package/dist/server/openapi.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ The engine behind [Peek](https://peektracker.com) ([GitHub](https://github.com/p
|
|
|
13
13
|
[](LICENSE)
|
|
14
14
|
|
|
15
15
|
<!-- GENERATED:summary -->
|
|
16
|
-
**3,500+ carriers** through **
|
|
16
|
+
**3,500+ carriers** through **150 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,
|
|
158
|
+
The carrier folders record 4,098 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
|
@@ -6708,7 +6708,9 @@
|
|
|
6708
6708
|
"timezone": "Europe/Zagreb",
|
|
6709
6709
|
"tracking": {
|
|
6710
6710
|
"mode": "automatic",
|
|
6711
|
-
"adapter": "
|
|
6711
|
+
"adapter": "hrvatska-posta",
|
|
6712
|
+
"recognitionRank": 810,
|
|
6713
|
+
"localClocks": true
|
|
6712
6714
|
},
|
|
6713
6715
|
"canaryUrl": "https://posiljka.posta.hr/",
|
|
6714
6716
|
"trackingUrlTemplate": "https://posiljka.posta.hr/hr/tracking/trackingdata?barcode={trackingNumber}",
|
|
@@ -6724,7 +6726,14 @@
|
|
|
6724
6726
|
]
|
|
6725
6727
|
}
|
|
6726
6728
|
],
|
|
6727
|
-
"detectionRules": [
|
|
6729
|
+
"detectionRules": [
|
|
6730
|
+
{
|
|
6731
|
+
"pattern": "^[A-Z]{2}\\d{9}HR$",
|
|
6732
|
+
"confidence": "low",
|
|
6733
|
+
"checksum": "s10",
|
|
6734
|
+
"preferred": true
|
|
6735
|
+
}
|
|
6736
|
+
]
|
|
6728
6737
|
},
|
|
6729
6738
|
"instabox": {
|
|
6730
6739
|
"displayName": "Instabox",
|
|
@@ -24879,6 +24879,12 @@
|
|
|
24879
24879
|
"asendia"
|
|
24880
24880
|
]
|
|
24881
24881
|
},
|
|
24882
|
+
{
|
|
24883
|
+
"input": "ALE000021875620",
|
|
24884
|
+
"carrier": "unknown",
|
|
24885
|
+
"confidence": "none",
|
|
24886
|
+
"candidates": []
|
|
24887
|
+
},
|
|
24882
24888
|
{
|
|
24883
24889
|
"input": "AM363452-4",
|
|
24884
24890
|
"carrier": "unknown",
|
|
@@ -27916,6 +27922,7 @@
|
|
|
27916
27922
|
"intl-post"
|
|
27917
27923
|
],
|
|
27918
27924
|
"asked": [
|
|
27925
|
+
"hrvatska-posta",
|
|
27919
27926
|
"chronopost"
|
|
27920
27927
|
]
|
|
27921
27928
|
},
|
|
@@ -30424,6 +30431,7 @@
|
|
|
30424
30431
|
"intl-post"
|
|
30425
30432
|
],
|
|
30426
30433
|
"asked": [
|
|
30434
|
+
"hrvatska-posta",
|
|
30427
30435
|
"chronopost"
|
|
30428
30436
|
]
|
|
30429
30437
|
},
|
|
@@ -30550,6 +30558,7 @@
|
|
|
30550
30558
|
"intl-post"
|
|
30551
30559
|
],
|
|
30552
30560
|
"asked": [
|
|
30561
|
+
"hrvatska-posta",
|
|
30553
30562
|
"chronopost"
|
|
30554
30563
|
]
|
|
30555
30564
|
},
|
|
@@ -30569,6 +30578,7 @@
|
|
|
30569
30578
|
"intl-post"
|
|
30570
30579
|
],
|
|
30571
30580
|
"asked": [
|
|
30581
|
+
"hrvatska-posta",
|
|
30572
30582
|
"chronopost"
|
|
30573
30583
|
]
|
|
30574
30584
|
},
|
|
@@ -30618,6 +30628,7 @@
|
|
|
30618
30628
|
"intl-post"
|
|
30619
30629
|
],
|
|
30620
30630
|
"asked": [
|
|
30631
|
+
"hrvatska-posta",
|
|
30621
30632
|
"chronopost"
|
|
30622
30633
|
]
|
|
30623
30634
|
},
|
|
@@ -31475,6 +31486,7 @@
|
|
|
31475
31486
|
"intl-post"
|
|
31476
31487
|
],
|
|
31477
31488
|
"asked": [
|
|
31489
|
+
"hrvatska-posta",
|
|
31478
31490
|
"chronopost"
|
|
31479
31491
|
]
|
|
31480
31492
|
},
|
|
@@ -31494,6 +31506,7 @@
|
|
|
31494
31506
|
"intl-post"
|
|
31495
31507
|
],
|
|
31496
31508
|
"asked": [
|
|
31509
|
+
"hrvatska-posta",
|
|
31497
31510
|
"chronopost"
|
|
31498
31511
|
]
|
|
31499
31512
|
},
|
|
@@ -33540,6 +33553,7 @@
|
|
|
33540
33553
|
"intl-post"
|
|
33541
33554
|
],
|
|
33542
33555
|
"asked": [
|
|
33556
|
+
"hrvatska-posta",
|
|
33543
33557
|
"chronopost"
|
|
33544
33558
|
]
|
|
33545
33559
|
},
|
|
@@ -33551,6 +33565,7 @@
|
|
|
33551
33565
|
"intl-post"
|
|
33552
33566
|
],
|
|
33553
33567
|
"asked": [
|
|
33568
|
+
"hrvatska-posta",
|
|
33554
33569
|
"chronopost"
|
|
33555
33570
|
]
|
|
33556
33571
|
},
|
|
@@ -33605,6 +33620,7 @@
|
|
|
33605
33620
|
"intl-post"
|
|
33606
33621
|
],
|
|
33607
33622
|
"asked": [
|
|
33623
|
+
"hrvatska-posta",
|
|
33608
33624
|
"chronopost"
|
|
33609
33625
|
]
|
|
33610
33626
|
},
|
|
@@ -36105,6 +36121,7 @@
|
|
|
36105
36121
|
"intl-post"
|
|
36106
36122
|
],
|
|
36107
36123
|
"asked": [
|
|
36124
|
+
"hrvatska-posta",
|
|
36108
36125
|
"chronopost"
|
|
36109
36126
|
]
|
|
36110
36127
|
},
|
|
@@ -37144,6 +37161,7 @@
|
|
|
37144
37161
|
"intl-post"
|
|
37145
37162
|
],
|
|
37146
37163
|
"asked": [
|
|
37164
|
+
"hrvatska-posta",
|
|
37147
37165
|
"chronopost"
|
|
37148
37166
|
]
|
|
37149
37167
|
},
|
package/dist/browser/scraper.js
CHANGED
|
@@ -5351,7 +5351,9 @@ var be = {
|
|
|
5351
5351
|
timezone: "Europe/Zagreb",
|
|
5352
5352
|
tracking: {
|
|
5353
5353
|
mode: "automatic",
|
|
5354
|
-
adapter: "
|
|
5354
|
+
adapter: "hrvatska-posta",
|
|
5355
|
+
recognitionRank: 810,
|
|
5356
|
+
localClocks: !0
|
|
5355
5357
|
},
|
|
5356
5358
|
canaryUrl: "https://posiljka.posta.hr/",
|
|
5357
5359
|
trackingUrlTemplate: "https://posiljka.posta.hr/hr/tracking/trackingdata?barcode={trackingNumber}",
|
|
@@ -5361,7 +5363,12 @@ var be = {
|
|
|
5361
5363
|
pathPattern: "^/(?:hr/|en/)?tracking/trackingdata/?$",
|
|
5362
5364
|
params: ["barcode"]
|
|
5363
5365
|
}],
|
|
5364
|
-
detectionRules: [
|
|
5366
|
+
detectionRules: [{
|
|
5367
|
+
pattern: "^[A-Z]{2}\\d{9}HR$",
|
|
5368
|
+
confidence: "low",
|
|
5369
|
+
checksum: "s10",
|
|
5370
|
+
preferred: !0
|
|
5371
|
+
}]
|
|
5365
5372
|
},
|
|
5366
5373
|
instabox: {
|
|
5367
5374
|
displayName: "Instabox",
|
|
@@ -7126,7 +7133,7 @@ var be = {
|
|
|
7126
7133
|
"gofo-fr": ["gofo-fr-1", "gofo-fr-2"],
|
|
7127
7134
|
"gofo-it": ["gofo-it-1"],
|
|
7128
7135
|
helthjem: ["helthjem-1", "helthjem-2"],
|
|
7129
|
-
"hrvatska-posta": [],
|
|
7136
|
+
"hrvatska-posta": ["hrvatska-posta-s10"],
|
|
7130
7137
|
instabox: [],
|
|
7131
7138
|
intelcom: [
|
|
7132
7139
|
"intelcom-1",
|
|
@@ -11925,6 +11932,7 @@ var bc = {
|
|
|
11925
11932
|
"gofo-it": 20,
|
|
11926
11933
|
helthjem: 710,
|
|
11927
11934
|
"hermes-de": 400,
|
|
11935
|
+
"hrvatska-posta": 810,
|
|
11928
11936
|
inpost: 380,
|
|
11929
11937
|
"j-and-t": 90,
|
|
11930
11938
|
"j-and-t-cargo": 470,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type AdapterFactory, type TrackingContext } from '../../core/adapter/index.js';
|
|
2
2
|
import type { CarrierResult } from '../../core/result/index.js';
|
|
3
3
|
import { type StepRecorder } from '../../core/telemetry/index.js';
|
|
4
|
-
export declare const ALLEGRO_ONE_ENDPOINT = "https://edge.allegro.pl/
|
|
4
|
+
export declare const ALLEGRO_ONE_ENDPOINT = "https://edge.allegro.pl/ad/tracking";
|
|
5
5
|
export declare function allegroOneTrackingUrl(raw: string): string;
|
|
6
6
|
export declare class AllegroOneTracker {
|
|
7
7
|
private readonly options;
|
|
@@ -5,8 +5,9 @@ import { firstAttemptMs, networkRetryStep } from '../../core/runner/networkRetry
|
|
|
5
5
|
import { NOOP_RECORDER } from '../../core/telemetry/index.js';
|
|
6
6
|
import { decodeText, fetchBounded, userAgentOf } from '../../core/transport/index.js';
|
|
7
7
|
import { ALLEGRO_ONE_MAX_BYTES, normalizeAllegroOneNumber, parseAllegroOne, PROVIDER } from './parser.js';
|
|
8
|
-
// The service Allegro
|
|
9
|
-
|
|
8
|
+
// The service the Allegro Delivery tracking page asks through its edge gateway.
|
|
9
|
+
// `/one/tracking`, which the older Allegro One page asked, leaves out sorting-centre scans.
|
|
10
|
+
export const ALLEGRO_ONE_ENDPOINT = 'https://edge.allegro.pl/ad/tracking';
|
|
10
11
|
// The gateway answers only the media type Allegro's own pages ask for.
|
|
11
12
|
const MEDIA_TYPE = 'application/vnd.allegro.internal.v1+json';
|
|
12
13
|
const REDIRECTS = [301, 302, 303, 307, 308];
|
|
@@ -190,6 +190,25 @@
|
|
|
190
190
|
"confidence": "high"
|
|
191
191
|
},
|
|
192
192
|
"note": "A buyer on Allegro's community forum gives it as the parcel number of an Allegro Delivery parcel for an ORLEN Paczka locker."
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"number": "ALE000021875620",
|
|
196
|
+
"role": "shipment",
|
|
197
|
+
"evidence": "public_shipment_report",
|
|
198
|
+
"source": {
|
|
199
|
+
"url": "https://spolecznosc.allegro.pl/t5/allegro-one-dla-kupuj%C4%85cych/jak-sprawdzi%C4%87-numer-ups-dla-przesy%C5%82ek-allegro-one/m-p/441116",
|
|
200
|
+
"date": "2022-07-25",
|
|
201
|
+
"dateKind": "comment",
|
|
202
|
+
"access": "full_page"
|
|
203
|
+
},
|
|
204
|
+
"context": {
|
|
205
|
+
"assessment": "confirmed"
|
|
206
|
+
},
|
|
207
|
+
"expect": {
|
|
208
|
+
"carrier": "unknown",
|
|
209
|
+
"confidence": "none"
|
|
210
|
+
},
|
|
211
|
+
"note": "A buyer on Allegro's community forum gives it as an Allegro One parcel and asks for its UPS number. Allegro One's tracker returns its history; no rule claims the older ALE form."
|
|
193
212
|
}
|
|
194
213
|
]
|
|
195
214
|
}
|
|
@@ -4,7 +4,7 @@ export declare const ALLEGRO_ONE_MAX_BYTES: number;
|
|
|
4
4
|
/** An Allegro One number; anything else is a number Allegro One does not issue. */
|
|
5
5
|
export declare function normalizeAllegroOneNumber(raw: string): string;
|
|
6
6
|
/**
|
|
7
|
-
* The reply of `GET /
|
|
7
|
+
* The reply of `GET /ad/tracking?packageNo={number}` on Allegro's edge
|
|
8
8
|
* gateway: `{ "status": [...] }`, each scan a UTC instant and a wording. It
|
|
9
9
|
* does not echo the number: the scans are those of the number in the request.
|
|
10
10
|
* An empty or null list is what the tracker shows as "not found". Every A0
|
|
@@ -39,7 +39,7 @@ function projectScan(row, index) {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
* The reply of `GET /
|
|
42
|
+
* The reply of `GET /ad/tracking?packageNo={number}` on Allegro's edge
|
|
43
43
|
* gateway: `{ "status": [...] }`, each scan a UTC instant and a wording. It
|
|
44
44
|
* does not echo the number: the scans are those of the number in the request.
|
|
45
45
|
* An empty or null list is what the tracker shows as "not found". Every A0
|
|
@@ -70,9 +70,11 @@ export function parseAllegroOne(payload, raw) {
|
|
|
70
70
|
seen.add(key);
|
|
71
71
|
scans.push(scan);
|
|
72
72
|
});
|
|
73
|
-
// The reply lists scans newest first and
|
|
74
|
-
//
|
|
75
|
-
|
|
73
|
+
// The reply lists an older parcel's scans newest first and a newer one's
|
|
74
|
+
// oldest first; the tracker shows them newest first. Instants order them,
|
|
75
|
+
// and equal ones keep the order in which the reply logged them.
|
|
76
|
+
const oldestFirst = scans.length > 1 && scans[0].at < scans.at(-1).at;
|
|
77
|
+
scans.sort((left, right) => right.at - left.at || (oldestFirst ? right.index - left.index : left.index - right.index));
|
|
76
78
|
const newest = scans[0].event;
|
|
77
79
|
const current = newest.stage
|
|
78
80
|
? { stage: newest.stage, source: 'carrier_map' }
|
|
@@ -17,6 +17,7 @@ export const ALLEGRO_ONE_SCANS = {
|
|
|
17
17
|
'Courier has transferred the parcel to the warehouse': 'in_transit',
|
|
18
18
|
'Parcel has been dispatched to its destination point': 'in_transit',
|
|
19
19
|
'Parcel has been accepted at the sorting center': 'in_transit',
|
|
20
|
+
'Dispatched from sorting center': 'in_transit',
|
|
20
21
|
'Parcel has been accepted at the branch': 'in_transit',
|
|
21
22
|
'Parcel has been redirected to a different point': 'in_transit',
|
|
22
23
|
// Notices while the parcel is on its way; neither stops it.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
{"wording": "Courier has transferred the parcel to the warehouse", "language": "en", "stage": "in_transit", "confirmedBy": "live"},
|
|
11
11
|
{"wording": "Parcel has been dispatched to its destination point", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "The parcel left for the locker, point or branch serving the recipient."},
|
|
12
12
|
{"wording": "Parcel has been accepted at the sorting center", "language": "en", "stage": "in_transit", "confirmedBy": "live"},
|
|
13
|
+
{"wording": "Dispatched from sorting center", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "Listed by /ad/tracking; /one/tracking leaves it out."},
|
|
13
14
|
{"wording": "Parcel has been accepted at the branch", "language": "en", "stage": "in_transit", "confirmedBy": "live"},
|
|
14
15
|
{"wording": "Parcel has been redirected to a different point", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "Follows the recipient's choice of another point."},
|
|
15
16
|
{"wording": "Parcel may be delivered with delay", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "A notice; the parcel keeps moving."},
|
|
@@ -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 HRVATSKA_POSTA_ENDPOINT = "https://posiljka.posta.hr/en/tracking/trackingdata";
|
|
5
|
+
export declare class HrvatskaPostaTracker {
|
|
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,52 @@
|
|
|
1
|
+
import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
|
|
2
|
+
import { IndeterminateError, 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, userAgentOf } from '../../core/transport/index.js';
|
|
7
|
+
import { HRVATSKA_POSTA_MAX_BYTES, normalizeHrvatskaPostaNumber, parseHrvatskaPosta, PROVIDER } from './parser.js';
|
|
8
|
+
// The English page of Hrvatska pošta's tracker, which its tracking forms open with the number as `barcode`.
|
|
9
|
+
export const HRVATSKA_POSTA_ENDPOINT = 'https://posiljka.posta.hr/en/tracking/trackingdata';
|
|
10
|
+
const REDIRECTS = [301, 302, 303, 307, 308];
|
|
11
|
+
// A missing route is not a missing item: the tracker answers unknown numbers with a notice.
|
|
12
|
+
const GONE = [404, 410];
|
|
13
|
+
export class HrvatskaPostaTracker {
|
|
14
|
+
options;
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.options = options;
|
|
17
|
+
}
|
|
18
|
+
async fetch(raw, context = {}) {
|
|
19
|
+
const number = normalizeHrvatskaPostaNumber(raw);
|
|
20
|
+
const url = `${HRVATSKA_POSTA_ENDPOINT}?${new URLSearchParams({ barcode: number })}`;
|
|
21
|
+
const read = async (signal, timeoutMs) => {
|
|
22
|
+
const { response, bytes } = await fetchBounded(url, { method: 'GET', signal, headers: {
|
|
23
|
+
Accept: 'text/html,application/xhtml+xml',
|
|
24
|
+
'User-Agent': userAgentOf(this.options.userAgent),
|
|
25
|
+
} }, { provider: PROVIDER, timeoutMs: Math.max(1, Math.floor(timeoutMs)), maxBytes: HRVATSKA_POSTA_MAX_BYTES,
|
|
26
|
+
redirect: 'manual', allowHttpStatuses: [...REDIRECTS, ...GONE], fetcher: this.options.fetcher });
|
|
27
|
+
if (REDIRECTS.includes(response.status))
|
|
28
|
+
throw new IndeterminateError(PROVIDER, 'Hrvatska pošta sent the lookup to another page');
|
|
29
|
+
if (GONE.includes(response.status))
|
|
30
|
+
throw new TransportError(PROVIDER, 'Hrvatska pošta tracking page is unavailable');
|
|
31
|
+
const html = decodeText(bytes);
|
|
32
|
+
if (!html.trim())
|
|
33
|
+
throw new IndeterminateError(PROVIDER, 'Hrvatska pošta returned an empty page');
|
|
34
|
+
return parseHrvatskaPosta(html, number);
|
|
35
|
+
};
|
|
36
|
+
// A request that fails to reach Hrvatska pošta, or hangs, gets one more
|
|
37
|
+
// try: the first gets half of the budget, so the retry fits in the rest.
|
|
38
|
+
return runSteps({ carrier: 'hrvatska-posta', budgetMs: context.budgetMs ?? 15_000, signal: context.signal,
|
|
39
|
+
recorder: this.options.recorder ?? NOOP_RECORDER }, [
|
|
40
|
+
{ id: 'direct', run: ({ signal, remainingMs }) => read(signal, firstAttemptMs(remainingMs)) },
|
|
41
|
+
networkRetryStep(({ signal, remainingMs }) => read(signal, remainingMs)),
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export const adapter = (environment) => {
|
|
46
|
+
const tracker = new HrvatskaPostaTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
|
|
47
|
+
return {
|
|
48
|
+
id: 'hrvatska-posta', recordsSteps: true, steps: ['direct', 'retry'],
|
|
49
|
+
track: (input, context) => tracker.fetch(input.number, context),
|
|
50
|
+
recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizeHrvatskaPostaNumber(number))),
|
|
51
|
+
};
|
|
52
|
+
};
|
|
@@ -20,18 +20,52 @@
|
|
|
20
20
|
"timezone": "Europe/Zagreb",
|
|
21
21
|
"tracking": {
|
|
22
22
|
"mode": "automatic",
|
|
23
|
-
"adapter": "
|
|
24
|
-
"steps": [
|
|
23
|
+
"adapter": "hrvatska-posta",
|
|
24
|
+
"steps": [
|
|
25
|
+
"direct",
|
|
26
|
+
"retry"
|
|
27
|
+
],
|
|
28
|
+
"recognition": {
|
|
29
|
+
"rank": 810
|
|
30
|
+
},
|
|
31
|
+
"localClocks": true
|
|
25
32
|
},
|
|
26
|
-
"capabilities": [
|
|
33
|
+
"capabilities": [
|
|
34
|
+
"history",
|
|
35
|
+
"location",
|
|
36
|
+
"weight",
|
|
37
|
+
"pickup_point"
|
|
38
|
+
],
|
|
27
39
|
"portal": {
|
|
28
40
|
"url": "https://posiljka.posta.hr/hr/tracking/trackingdata?barcode={trackingNumber}",
|
|
29
41
|
"searchUrl": "https://posiljka.posta.hr/",
|
|
30
42
|
"canaryUrl": "https://posiljka.posta.hr/",
|
|
31
|
-
"shows": [
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
"shows": [
|
|
44
|
+
"status",
|
|
45
|
+
"history",
|
|
46
|
+
"location",
|
|
47
|
+
"weight",
|
|
48
|
+
"postcodes",
|
|
49
|
+
"delivery_unit_logo"
|
|
50
|
+
],
|
|
51
|
+
"scraped": [
|
|
52
|
+
"status",
|
|
53
|
+
"history",
|
|
54
|
+
"location",
|
|
55
|
+
"weight",
|
|
56
|
+
"pickup_point"
|
|
57
|
+
],
|
|
58
|
+
"discarded": [
|
|
59
|
+
"postcodes",
|
|
60
|
+
"delivery_unit_logo"
|
|
61
|
+
],
|
|
62
|
+
"unavailable": [
|
|
63
|
+
"eta",
|
|
64
|
+
"service_name",
|
|
65
|
+
"sender_name",
|
|
66
|
+
"recipient_name",
|
|
67
|
+
"offset_timestamps"
|
|
68
|
+
]
|
|
35
69
|
},
|
|
36
70
|
"links": [
|
|
37
71
|
{
|
|
@@ -44,5 +78,13 @@
|
|
|
44
78
|
]
|
|
45
79
|
}
|
|
46
80
|
],
|
|
47
|
-
"detection": [
|
|
81
|
+
"detection": [
|
|
82
|
+
{
|
|
83
|
+
"id": "hrvatska-posta-s10",
|
|
84
|
+
"pattern": "^[A-Z]{2}\\d{9}HR$",
|
|
85
|
+
"confidence": "low",
|
|
86
|
+
"preferred": true,
|
|
87
|
+
"checksum": "s10"
|
|
88
|
+
}
|
|
89
|
+
]
|
|
48
90
|
}
|
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
"url": "https://web.archive.org/web/20210918063710/https://posiljka.posta.hr/tracking/trackingdata?barcode=LE565787894HR",
|
|
10
10
|
"date": "2021-09-18",
|
|
11
11
|
"dateKind": "capture",
|
|
12
|
-
"access": "
|
|
12
|
+
"access": "full_page"
|
|
13
13
|
},
|
|
14
14
|
"context": {
|
|
15
|
-
"assessment": "
|
|
15
|
+
"assessment": "confirmed"
|
|
16
16
|
},
|
|
17
17
|
"expect": {
|
|
18
18
|
"carrier": "intl-post",
|
|
19
19
|
"confidence": "high"
|
|
20
20
|
},
|
|
21
|
-
"note": "Looked up on Hrvatska pošta's own tracker
|
|
21
|
+
"note": "Looked up on Hrvatska pošta's own tracker; the archived capture holds its history. Detection selects the generic postal carrier."
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"number": "RG229353469HR",
|
|
@@ -28,16 +28,16 @@
|
|
|
28
28
|
"url": "https://web.archive.org/web/20230204084910/https://posiljka.posta.hr/en/tracking/trackingdata?barcode=RG229353469HR",
|
|
29
29
|
"date": "2023-02-04",
|
|
30
30
|
"dateKind": "capture",
|
|
31
|
-
"access": "
|
|
31
|
+
"access": "full_page"
|
|
32
32
|
},
|
|
33
33
|
"context": {
|
|
34
|
-
"assessment": "
|
|
34
|
+
"assessment": "confirmed"
|
|
35
35
|
},
|
|
36
36
|
"expect": {
|
|
37
37
|
"carrier": "intl-post",
|
|
38
38
|
"confidence": "high"
|
|
39
39
|
},
|
|
40
|
-
"note": "Looked up on Hrvatska pošta's own tracker
|
|
40
|
+
"note": "Looked up on Hrvatska pošta's own tracker; the archived capture holds its history. Detection selects the generic postal carrier."
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"number": "rg357685487hr",
|
|
@@ -47,16 +47,16 @@
|
|
|
47
47
|
"url": "https://web.archive.org/web/20230713060921/https://posiljka.posta.hr/hr/tracking/trackingdata?barcode=rg357685487hr",
|
|
48
48
|
"date": "2023-07-13",
|
|
49
49
|
"dateKind": "capture",
|
|
50
|
-
"access": "
|
|
50
|
+
"access": "full_page"
|
|
51
51
|
},
|
|
52
52
|
"context": {
|
|
53
|
-
"assessment": "
|
|
53
|
+
"assessment": "confirmed"
|
|
54
54
|
},
|
|
55
55
|
"expect": {
|
|
56
56
|
"carrier": "intl-post",
|
|
57
57
|
"confidence": "high"
|
|
58
58
|
},
|
|
59
|
-
"note": "Looked up on Hrvatska pošta's own tracker in lower case
|
|
59
|
+
"note": "Looked up on Hrvatska pošta's own tracker in lower case; the archived capture holds its history. Detection selects the generic postal carrier."
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
"number": "LD084931122HR",
|
|
@@ -66,16 +66,16 @@
|
|
|
66
66
|
"url": "https://web.archive.org/web/20231124155826/https://posiljka.posta.hr/hr/tracking/trackingdata?barcode=LD084931122HR",
|
|
67
67
|
"date": "2023-11-24",
|
|
68
68
|
"dateKind": "capture",
|
|
69
|
-
"access": "
|
|
69
|
+
"access": "full_page"
|
|
70
70
|
},
|
|
71
71
|
"context": {
|
|
72
|
-
"assessment": "
|
|
72
|
+
"assessment": "confirmed"
|
|
73
73
|
},
|
|
74
74
|
"expect": {
|
|
75
75
|
"carrier": "intl-post",
|
|
76
76
|
"confidence": "high"
|
|
77
77
|
},
|
|
78
|
-
"note": "Looked up on Hrvatska pošta's own tracker
|
|
78
|
+
"note": "Looked up on Hrvatska pošta's own tracker; the archived capture holds its history. Detection selects the generic postal carrier."
|
|
79
79
|
},
|
|
80
80
|
{
|
|
81
81
|
"number": "LW225825809HR",
|
|
@@ -85,16 +85,16 @@
|
|
|
85
85
|
"url": "https://web.archive.org/web/20240219134105/https://posiljka.posta.hr/hr/tracking/trackingdata?barcode=LW225825809HR",
|
|
86
86
|
"date": "2024-02-19",
|
|
87
87
|
"dateKind": "capture",
|
|
88
|
-
"access": "
|
|
88
|
+
"access": "full_page"
|
|
89
89
|
},
|
|
90
90
|
"context": {
|
|
91
|
-
"assessment": "
|
|
91
|
+
"assessment": "confirmed"
|
|
92
92
|
},
|
|
93
93
|
"expect": {
|
|
94
94
|
"carrier": "intl-post",
|
|
95
95
|
"confidence": "high"
|
|
96
96
|
},
|
|
97
|
-
"note": "Looked up on Hrvatska pošta's own tracker
|
|
97
|
+
"note": "Looked up on Hrvatska pošta's own tracker; the archived capture holds its history. Detection selects the generic postal carrier."
|
|
98
98
|
},
|
|
99
99
|
{
|
|
100
100
|
"number": "EM146631045HR",
|
|
@@ -107,13 +107,13 @@
|
|
|
107
107
|
"access": "indexed_excerpt"
|
|
108
108
|
},
|
|
109
109
|
"context": {
|
|
110
|
-
"assessment": "
|
|
110
|
+
"assessment": "confirmed"
|
|
111
111
|
},
|
|
112
112
|
"expect": {
|
|
113
113
|
"carrier": "intl-post",
|
|
114
114
|
"confidence": "high"
|
|
115
115
|
},
|
|
116
|
-
"note": "Looked up on Hrvatska pošta's own tracker, as an archived address shows; the
|
|
116
|
+
"note": "Looked up on Hrvatska pošta's own tracker, as an archived address shows. The tracker returns its history; detection selects the generic postal carrier."
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
119
|
"number": "LD241044973HR",
|
|
@@ -126,13 +126,13 @@
|
|
|
126
126
|
"access": "indexed_excerpt"
|
|
127
127
|
},
|
|
128
128
|
"context": {
|
|
129
|
-
"assessment": "
|
|
129
|
+
"assessment": "confirmed"
|
|
130
130
|
},
|
|
131
131
|
"expect": {
|
|
132
132
|
"carrier": "intl-post",
|
|
133
133
|
"confidence": "high"
|
|
134
134
|
},
|
|
135
|
-
"note": "Looked up on Hrvatska pošta's own tracker, as an archived address shows; the
|
|
135
|
+
"note": "Looked up on Hrvatska pošta's own tracker, as an archived address shows. The tracker returns its history; detection selects the generic postal carrier."
|
|
136
136
|
},
|
|
137
137
|
{
|
|
138
138
|
"number": "RG523729391HR",
|
|
@@ -145,13 +145,13 @@
|
|
|
145
145
|
"access": "indexed_excerpt"
|
|
146
146
|
},
|
|
147
147
|
"context": {
|
|
148
|
-
"assessment": "
|
|
148
|
+
"assessment": "confirmed"
|
|
149
149
|
},
|
|
150
150
|
"expect": {
|
|
151
151
|
"carrier": "intl-post",
|
|
152
152
|
"confidence": "high"
|
|
153
153
|
},
|
|
154
|
-
"note": "Looked up on Hrvatska pošta's own tracker, as an archived address shows; the
|
|
154
|
+
"note": "Looked up on Hrvatska pošta's own tracker, as an archived address shows. The tracker returns its history; detection selects the generic postal carrier."
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
157
|
"number": "LA008713489HR",
|
|
@@ -164,13 +164,13 @@
|
|
|
164
164
|
"access": "indexed_excerpt"
|
|
165
165
|
},
|
|
166
166
|
"context": {
|
|
167
|
-
"assessment": "
|
|
167
|
+
"assessment": "confirmed"
|
|
168
168
|
},
|
|
169
169
|
"expect": {
|
|
170
170
|
"carrier": "intl-post",
|
|
171
171
|
"confidence": "high"
|
|
172
172
|
},
|
|
173
|
-
"note": "Looked up on Hrvatska pošta's own tracker, as an archived address shows; the
|
|
173
|
+
"note": "Looked up on Hrvatska pošta's own tracker, as an archived address shows. The tracker returns its history; detection selects the generic postal carrier."
|
|
174
174
|
},
|
|
175
175
|
{
|
|
176
176
|
"number": "VV175837515HR",
|
|
@@ -183,13 +183,13 @@
|
|
|
183
183
|
"access": "indexed_excerpt"
|
|
184
184
|
},
|
|
185
185
|
"context": {
|
|
186
|
-
"assessment": "
|
|
186
|
+
"assessment": "confirmed"
|
|
187
187
|
},
|
|
188
188
|
"expect": {
|
|
189
189
|
"carrier": "intl-post",
|
|
190
190
|
"confidence": "high"
|
|
191
191
|
},
|
|
192
|
-
"note": "Looked up on Hrvatska pošta's own tracker, as an archived address shows; the
|
|
192
|
+
"note": "Looked up on Hrvatska pošta's own tracker, as an archived address shows. The tracker returns its history; detection selects the generic postal carrier."
|
|
193
193
|
},
|
|
194
194
|
{
|
|
195
195
|
"number": "RH030544536HR",
|
|
@@ -202,13 +202,13 @@
|
|
|
202
202
|
"access": "indexed_excerpt"
|
|
203
203
|
},
|
|
204
204
|
"context": {
|
|
205
|
-
"assessment": "
|
|
205
|
+
"assessment": "confirmed"
|
|
206
206
|
},
|
|
207
207
|
"expect": {
|
|
208
208
|
"carrier": "intl-post",
|
|
209
209
|
"confidence": "high"
|
|
210
210
|
},
|
|
211
|
-
"note": "Looked up on Hrvatska pošta's own tracker, as an archived address shows; the
|
|
211
|
+
"note": "Looked up on Hrvatska pošta's own tracker, as an archived address shows. The tracker returns its history; detection selects the generic postal carrier."
|
|
212
212
|
},
|
|
213
213
|
{
|
|
214
214
|
"number": "LW260370397HR",
|
|
@@ -221,13 +221,13 @@
|
|
|
221
221
|
"access": "indexed_excerpt"
|
|
222
222
|
},
|
|
223
223
|
"context": {
|
|
224
|
-
"assessment": "
|
|
224
|
+
"assessment": "confirmed"
|
|
225
225
|
},
|
|
226
226
|
"expect": {
|
|
227
227
|
"carrier": "intl-post",
|
|
228
228
|
"confidence": "high"
|
|
229
229
|
},
|
|
230
|
-
"note": "Looked up on Hrvatska pošta's own tracker, as an archived address shows; the
|
|
230
|
+
"note": "Looked up on Hrvatska pošta's own tracker, as an archived address shows. The tracker returns its history; detection selects the generic postal carrier."
|
|
231
231
|
}
|
|
232
232
|
]
|
|
233
233
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CarrierResult } from '../../core/result/index.js';
|
|
2
|
+
export declare const PROVIDER = "Hrvatska po\u0161ta";
|
|
3
|
+
export declare const HRVATSKA_POSTA_MAX_BYTES = 1000000;
|
|
4
|
+
/**
|
|
5
|
+
* The numbers the tracker's own form accepts: an S10 item code, a 3S code, a
|
|
6
|
+
* twelve-digit 2-of-5 barcode, an SSCC, an MH10 code starting J and a BAT
|
|
7
|
+
* code. S10 codes and SSCCs must pass their check digit.
|
|
8
|
+
*/
|
|
9
|
+
export declare function normalizeHrvatskaPostaNumber(raw: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* The tracker's English page for one number: its scans, grouped under the
|
|
12
|
+
* page's milestones, each with its date and time, wording and, mostly, the
|
|
13
|
+
* office. The page names the number it answers for; a number without data
|
|
14
|
+
* comes back with a notice instead of scans.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseHrvatskaPosta(html: string, rawNumber: string): CarrierResult;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { load } from 'cheerio';
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
3
|
+
import { isValidS10TrackingNumber, normalizeTrackingNumber } from '../../core/detection/index.js';
|
|
4
|
+
import { isValidSscc } from '../../core/detection/numericChecksums.js';
|
|
5
|
+
import { ChallengeError, IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
|
|
6
|
+
import { lastMovement } from '../../core/result/pickup.js';
|
|
7
|
+
import { languageStageStatus } from '../../core/status/index.js';
|
|
8
|
+
import { clean } from '../../core/transport/index.js';
|
|
9
|
+
import { hrvatskaPostaStage } from './status.js';
|
|
10
|
+
export const PROVIDER = 'Hrvatska pošta';
|
|
11
|
+
// A page with a long history is a few dozen kilobytes.
|
|
12
|
+
export const HRVATSKA_POSTA_MAX_BYTES = 1_000_000;
|
|
13
|
+
const MAX_ROWS = 500;
|
|
14
|
+
// The page's label for the number it answers for, in English, the language the adapter asks for.
|
|
15
|
+
const NUMBER_LABEL = 'Item number';
|
|
16
|
+
// The English page's notice for a number without data. It reads like a format
|
|
17
|
+
// error, but the Croatian page words the same notice "Nema podataka za uneseni
|
|
18
|
+
// prijamni broj pošiljke", no data for the number entered.
|
|
19
|
+
const NO_DATA = 'Invalid barcode format';
|
|
20
|
+
// The notice for a number the tracker does not accept.
|
|
21
|
+
const BAD_FORMAT = 'Invalid shipment number format';
|
|
22
|
+
// A firewall or bot check's page, which can come back with HTTP 200.
|
|
23
|
+
const BLOCKED = /<title>\s*(?:Request Rejected|Access Denied|Just a moment)|requested URL was rejected|_cf_chl_opt|cf-turnstile/i;
|
|
24
|
+
const STAMP = /^(\d{2}\.\d{2}\.\d{4}) (\d{2}:\d{2}:\d{2})$/;
|
|
25
|
+
// A Croatian post office or parcel locker comes with its five-digit number, which is a postcode.
|
|
26
|
+
const OFFICE_NUMBER = /^\d{5}\s+/;
|
|
27
|
+
const WEIGHT = /^(\d{1,7}) g$/;
|
|
28
|
+
/**
|
|
29
|
+
* The numbers the tracker's own form accepts: an S10 item code, a 3S code, a
|
|
30
|
+
* twelve-digit 2-of-5 barcode, an SSCC, an MH10 code starting J and a BAT
|
|
31
|
+
* code. S10 codes and SSCCs must pass their check digit.
|
|
32
|
+
*/
|
|
33
|
+
export function normalizeHrvatskaPostaNumber(raw) {
|
|
34
|
+
const number = normalizeTrackingNumber(raw);
|
|
35
|
+
if (isValidS10TrackingNumber(number) || /^3S[A-Z0-9]{1,13}$/.test(number) || /^\d{12}$/.test(number)
|
|
36
|
+
|| (/^00\d{18}$/.test(number) && isValidSscc(number)) || /^J[A-Z0-9]{15,34}$/.test(number)
|
|
37
|
+
|| /^BAT\d{1,12}$/.test(number))
|
|
38
|
+
return number;
|
|
39
|
+
throw new InvalidInputError(PROVIDER, 'Hrvatska pošta tracks S10 item codes and the barcodes its tracking form accepts');
|
|
40
|
+
}
|
|
41
|
+
/** The scan's date and time ("30.06.2026 10:15:45") as a wall clock. They carry no offset. */
|
|
42
|
+
function wallClock(stamp) {
|
|
43
|
+
const match = STAMP.exec(stamp);
|
|
44
|
+
if (!match)
|
|
45
|
+
return null;
|
|
46
|
+
// UTC only checks the calendar fields. No offset is assigned.
|
|
47
|
+
const parsed = DateTime.fromFormat(`${match[1]} ${match[2]}`, 'dd.MM.yyyy HH:mm:ss', { zone: 'UTC' });
|
|
48
|
+
return parsed.isValid ? parsed.toFormat("yyyy-MM-dd'T'HH:mm:ss") : null;
|
|
49
|
+
}
|
|
50
|
+
/** The office, locker or foreign facility, without the number of a Croatian one. */
|
|
51
|
+
function place(raw) {
|
|
52
|
+
return clean(raw, 200).replace(OFFICE_NUMBER, '');
|
|
53
|
+
}
|
|
54
|
+
function weightKg(raw) {
|
|
55
|
+
const grams = WEIGHT.exec(raw)?.[1];
|
|
56
|
+
// An item without a measured weight shows 0 g.
|
|
57
|
+
return grams && Number(grams) > 0 ? Number(grams) / 1000 : undefined;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The tracker's English page for one number: its scans, grouped under the
|
|
61
|
+
* page's milestones, each with its date and time, wording and, mostly, the
|
|
62
|
+
* office. The page names the number it answers for; a number without data
|
|
63
|
+
* comes back with a notice instead of scans.
|
|
64
|
+
*/
|
|
65
|
+
export function parseHrvatskaPosta(html, rawNumber) {
|
|
66
|
+
const number = normalizeHrvatskaPostaNumber(rawNumber);
|
|
67
|
+
const $ = load(html);
|
|
68
|
+
const roots = $('div.track__n__trace');
|
|
69
|
+
if (roots.length !== 1) {
|
|
70
|
+
// The site's error page, which it can serve with HTTP 200 for a while for an item it knows.
|
|
71
|
+
if ($('div.not__found').length)
|
|
72
|
+
throw new IndeterminateError(PROVIDER, 'Hrvatska pošta answered with its error page');
|
|
73
|
+
if (BLOCKED.test(html.slice(0, 64_000)))
|
|
74
|
+
throw new ChallengeError(PROVIDER, 'Hrvatska pošta answered with a block page');
|
|
75
|
+
throw new SchemaError(PROVIDER, 'Hrvatska pošta did not return a tracking page');
|
|
76
|
+
}
|
|
77
|
+
const root = roots.first();
|
|
78
|
+
const header = root.find('div.track__n__trace-header');
|
|
79
|
+
const named = header.find('h5');
|
|
80
|
+
const label = clean(named.first().prev('span').text(), 40);
|
|
81
|
+
if (header.length !== 1 || named.length !== 1 || label !== NUMBER_LABEL) {
|
|
82
|
+
throw new SchemaError(PROVIDER, 'Hrvatska pošta did not answer in English');
|
|
83
|
+
}
|
|
84
|
+
if (normalizeTrackingNumber(clean(named.text(), 80)) !== number) {
|
|
85
|
+
throw new SchemaError(PROVIDER, 'Hrvatska pošta answered for a different number');
|
|
86
|
+
}
|
|
87
|
+
const groups = root.find('div.track__n__trace-events-content > div.event__status__item');
|
|
88
|
+
if (!groups.length) {
|
|
89
|
+
const notices = root.find('div.address__change__wrapper h4');
|
|
90
|
+
const notice = notices.length === 1 ? clean(notices.text(), 200) : '';
|
|
91
|
+
// Only the tracker's "no data" notice is a missing item.
|
|
92
|
+
if (notice === NO_DATA)
|
|
93
|
+
throw new NotFoundError(PROVIDER);
|
|
94
|
+
if (notice === BAD_FORMAT)
|
|
95
|
+
throw new InvalidInputError(PROVIDER, 'Hrvatska pošta does not accept the number');
|
|
96
|
+
throw new IndeterminateError(PROVIDER, 'Hrvatska pošta returned no scans for the number');
|
|
97
|
+
}
|
|
98
|
+
// The desktop rows; the page repeats each scan in a mobile row, which is not read.
|
|
99
|
+
const rows = groups.find('div.event__status__item-content > div.row');
|
|
100
|
+
if (!rows.length || rows.length > MAX_ROWS)
|
|
101
|
+
throw new SchemaError(PROVIDER, 'Hrvatska pošta returned an invalid item history');
|
|
102
|
+
const scans = [];
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
rows.each((index, node) => {
|
|
105
|
+
const columns = $(node).children('div');
|
|
106
|
+
if (columns.length !== 2)
|
|
107
|
+
throw new SchemaError(PROVIDER, 'Hrvatska pošta history columns changed');
|
|
108
|
+
const time = wallClock(clean(columns.eq(0).children('span').first().text(), 40));
|
|
109
|
+
const spans = columns.eq(1).children('span');
|
|
110
|
+
const description = clean(spans.eq(0).text(), 300);
|
|
111
|
+
if (!time || !description || spans.length > 2)
|
|
112
|
+
throw new SchemaError(PROVIDER, 'Hrvatska pošta returned a scan without a clock or wording');
|
|
113
|
+
const location = spans.length === 2 ? place(spans.eq(1).text()) : '';
|
|
114
|
+
const event = { local_time: time, description, ...(location ? { location } : {}) };
|
|
115
|
+
// A parcel can repeat a scan with the same clock, place and wording.
|
|
116
|
+
const identity = JSON.stringify(event);
|
|
117
|
+
if (seen.has(identity))
|
|
118
|
+
return;
|
|
119
|
+
seen.add(identity);
|
|
120
|
+
scans.push({ event, index });
|
|
121
|
+
});
|
|
122
|
+
// The page groups scans by milestone, so their clocks can interleave across
|
|
123
|
+
// groups. Newest first; equal clocks keep the page's order.
|
|
124
|
+
scans.sort((left, right) => right.event.local_time > left.event.local_time ? 1
|
|
125
|
+
: right.event.local_time < left.event.local_time ? -1 : left.index - right.index);
|
|
126
|
+
const events = scans.map(({ event }) => event);
|
|
127
|
+
// Oldest first: an acceptance after the item moved on is a facility abroad
|
|
128
|
+
// receiving it, and a delivery once it was sent back reaches its sender.
|
|
129
|
+
let moved = false;
|
|
130
|
+
let returning = false;
|
|
131
|
+
for (const event of [...events].reverse()) {
|
|
132
|
+
const classified = hrvatskaPostaStage(event.description);
|
|
133
|
+
if (!classified)
|
|
134
|
+
continue;
|
|
135
|
+
let stage = classified.stage;
|
|
136
|
+
if (stage === 'accepted' && moved)
|
|
137
|
+
stage = 'in_transit';
|
|
138
|
+
if (stage === 'delivered' && returning)
|
|
139
|
+
stage = 'returned';
|
|
140
|
+
if (stage !== 'registered' && stage !== 'accepted')
|
|
141
|
+
moved = true;
|
|
142
|
+
if (stage === 'returned')
|
|
143
|
+
returning = true;
|
|
144
|
+
Object.assign(event, { stage, stage_source: classified.source });
|
|
145
|
+
}
|
|
146
|
+
const latest = events[0];
|
|
147
|
+
const stage = latest.stage;
|
|
148
|
+
// The office or locker an item waits at is its pickup point. A delivered
|
|
149
|
+
// item was collected there when it waited there last before the delivery.
|
|
150
|
+
const earlier = events.slice(1);
|
|
151
|
+
const waiting = stage === 'ready_for_pickup' ? latest
|
|
152
|
+
: stage === 'delivered' && lastMovement(earlier) === 'ready_for_pickup'
|
|
153
|
+
? earlier.find(event => event.stage === 'ready_for_pickup')
|
|
154
|
+
: undefined;
|
|
155
|
+
const point = waiting?.location ?? '';
|
|
156
|
+
// The header's value next to its "Weight" label.
|
|
157
|
+
const weights = header.find('span.Text-Style-4').filter((_, node) => clean($(node).text(), 40) === 'Weight');
|
|
158
|
+
const weight = weights.length === 1 ? weightKg(clean(weights.next('span').text(), 40)) : undefined;
|
|
159
|
+
return {
|
|
160
|
+
status: stage ? languageStageStatus(stage) : 'unknown',
|
|
161
|
+
...(stage ? { current_stage: stage, current_stage_source: latest.stage_source } : {}),
|
|
162
|
+
last_status_text: latest.description,
|
|
163
|
+
last_update: null,
|
|
164
|
+
last_update_local: latest.local_time,
|
|
165
|
+
expected_delivery: null,
|
|
166
|
+
...(point ? { pickup_point: point } : {}),
|
|
167
|
+
...(weight ? { weight_kg: weight } : {}),
|
|
168
|
+
events,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -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 the map gives Hrvatska pošta's wording, if it knows it. */
|
|
4
|
+
export declare function hrvatskaPostaScanStage(wording: string): Stage | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* A scan's stage from the map, 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 hrvatskaPostaStage(wording: string): ClassifiedWording | null;
|
|
10
|
+
/** The scans carry wording only. */
|
|
11
|
+
export declare const statusMap: CarrierStatusMap;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { classifyWording } from '../../core/status/index.js';
|
|
2
|
+
import { normalizeStatusWording } from '../../core/status/statusMap.js';
|
|
3
|
+
/**
|
|
4
|
+
* The English wording of Hrvatska pošta's tracker, which the adapter asks
|
|
5
|
+
* for. The scans carry no code. An item waiting at a post office or in a
|
|
6
|
+
* parcel locker is ready for pickup, and one handed back to its sender, or
|
|
7
|
+
* on its way back, is returned.
|
|
8
|
+
*/
|
|
9
|
+
const STAGES = new Map([
|
|
10
|
+
// The sender entered the item's data; Hrvatska pošta does not have it yet.
|
|
11
|
+
['item announced', 'registered'],
|
|
12
|
+
['item received', 'accepted'],
|
|
13
|
+
// An outbound item at the exchange office, sent abroad, and received by the post abroad.
|
|
14
|
+
['item received at exchange office', 'in_transit'],
|
|
15
|
+
['item sent', 'in_transit'],
|
|
16
|
+
['item received in destination country', 'in_transit'],
|
|
17
|
+
['delivery was redirected to the parcelstation', 'in_transit'],
|
|
18
|
+
['item is out for delivery', 'out_for_delivery'],
|
|
19
|
+
['delivery attempt', 'failed_attempt'],
|
|
20
|
+
// Left uncollected at the office, before it goes back to its sender.
|
|
21
|
+
['item not delivered', 'failed_attempt'],
|
|
22
|
+
['item can be picked up at the post office', 'ready_for_pickup'],
|
|
23
|
+
['the shipment has been handed over to the parcelstation', 'ready_for_pickup'],
|
|
24
|
+
['item delivered', 'delivered'],
|
|
25
|
+
['item delivered to sender', 'returned'],
|
|
26
|
+
['item returned to sender', 'returned'],
|
|
27
|
+
]);
|
|
28
|
+
/** The stage the map gives Hrvatska pošta's wording, if it knows it. */
|
|
29
|
+
export function hrvatskaPostaScanStage(wording) {
|
|
30
|
+
return STAGES.get(normalizeStatusWording(wording));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A scan's stage from the map, else from the shared wording rules. Wording
|
|
34
|
+
* neither knows stays without a stage, so it remains visible for review.
|
|
35
|
+
*/
|
|
36
|
+
export function hrvatskaPostaStage(wording) {
|
|
37
|
+
const mapped = hrvatskaPostaScanStage(wording);
|
|
38
|
+
if (mapped)
|
|
39
|
+
return { stage: mapped, source: 'carrier_map' };
|
|
40
|
+
const worded = classifyWording(wording, 'in_transit');
|
|
41
|
+
return worded.source === 'none' ? null : worded;
|
|
42
|
+
}
|
|
43
|
+
/** The scans carry wording only. */
|
|
44
|
+
export const statusMap = {
|
|
45
|
+
stage: (code, wording) => (code ? undefined : hrvatskaPostaScanStage(wording)),
|
|
46
|
+
gaps: [],
|
|
47
|
+
};
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"carrier": "hrvatska-posta",
|
|
3
|
-
"entries": [
|
|
3
|
+
"entries": [
|
|
4
|
+
{"wording": "Item announced", "stage": "registered", "language": "en", "confirmedBy": "live", "note": "The sender entered the item's data; the Croatian page reads \"Pošiljatelj je unio podatke, pošiljka još nije predana Hrvatskoj pošti\", not yet handed to Hrvatska pošta."},
|
|
5
|
+
{"wording": "Item received", "stage": "accepted", "language": "en", "confirmedBy": "live", "note": "Accepted at the office the scan names. A facility abroad receiving an item that already moved on uses the same wording; the adapter reads that one as in transit."},
|
|
6
|
+
{"wording": "Item received at exchange office", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "An outbound item at the office of exchange; the Croatian page words it as being prepared for dispatch."},
|
|
7
|
+
{"wording": "Item sent", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "Dispatched from the office of exchange."},
|
|
8
|
+
{"wording": "Item received in destination country", "stage": "in_transit", "language": "en", "confirmedBy": "portal", "note": "Relayed from the post abroad; seen in an archived capture of the tracker."},
|
|
9
|
+
{"wording": "Delivery was redirected to the parcelstation", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "Sent on to a parcel locker instead of the address."},
|
|
10
|
+
{"wording": "Item is out for delivery", "stage": "out_for_delivery", "language": "en", "confirmedBy": "live"},
|
|
11
|
+
{"wording": "Delivery attempt", "stage": "failed_attempt", "language": "en", "confirmedBy": "live"},
|
|
12
|
+
{"wording": "Item not delivered", "stage": "failed_attempt", "language": "en", "confirmedBy": "live", "note": "Seen when an item nobody collected leaves the post office, just before it goes back to its sender."},
|
|
13
|
+
{"wording": "Item can be picked up at the Post Office", "stage": "ready_for_pickup", "language": "en", "confirmedBy": "live", "note": "The office becomes the pickup point. Repeated while the item waits there."},
|
|
14
|
+
{"wording": "The shipment has been handed over to the parcelstation", "stage": "ready_for_pickup", "language": "en", "confirmedBy": "live", "note": "Left in a parcel locker, which becomes the pickup point."},
|
|
15
|
+
{"wording": "Item delivered", "stage": "delivered", "language": "en", "confirmedBy": "live", "note": "The Croatian page has two wordings for it, delivered and delivered to the recipient. Once the item is on its way back, the adapter reads a delivery as returned."},
|
|
16
|
+
{"wording": "Item delivered to sender", "stage": "returned", "language": "en", "confirmedBy": "live"},
|
|
17
|
+
{"wording": "Item returned to sender", "stage": "returned", "language": "en", "confirmedBy": "live", "note": "On its way back to the sender; the Croatian page reads \"Pošiljka se vraća pošiljatelju\"."}
|
|
18
|
+
]
|
|
4
19
|
}
|
|
@@ -40,6 +40,7 @@ import { statusMap as glsIt } from '../../carriers/gls-it/status.js';
|
|
|
40
40
|
import { statusMap as gofoFr } from '../../carriers/gofo-fr/status.js';
|
|
41
41
|
import { statusMap as gofoIt } from '../../carriers/gofo-it/status.js';
|
|
42
42
|
import { statusMap as helthjem } from '../../carriers/helthjem/status.js';
|
|
43
|
+
import { statusMap as hrvatskaPosta } from '../../carriers/hrvatska-posta/status.js';
|
|
43
44
|
import { statusMap as jAndT } from '../../carriers/j-and-t/status.js';
|
|
44
45
|
import { statusMap as jntCargo } from '../../carriers/j-and-t-cargo/status.js';
|
|
45
46
|
import { statusMap as laPoste } from '../../carriers/la-poste/status.js';
|
|
@@ -74,8 +75,8 @@ export const STATUS_MAPS = {
|
|
|
74
75
|
'acs-courier': acsCourier, aliexpress, 'allegro-one': allegroOne, 'an-post': anPost, 'aras-kargo': arasKargo, 'box-now': boxNow, cacesa, cargus,
|
|
75
76
|
'ceska-posta': ceskaPosta, 'china-post': chinaPost, chronopost, 'correos-spain': correosSpain, 'dhl-express': dhlExpress, dpd, 'dpd-de': dpdDe,
|
|
76
77
|
'dpd-fr': dpdFr, 'dpd-ie': dpdIe, 'dpd-pl': dpdPl, econt, elta, emile, 'express-one': expressOne, 'fan-courier': fanCourier, foxpost,
|
|
77
|
-
'geniki-taxydromiki': geniki, 'gls-es': glsEs, 'gls-it': glsIt, 'gofo-fr': gofoFr, 'gofo-it': gofoIt, helthjem,
|
|
78
|
-
'j-and-t-cargo': jntCargo, 'la-poste': laPoste, matkahuolto, 'mng-kargo': mngKargo, 'old-dominion': oldDominion, omgo, omniva,
|
|
78
|
+
'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,
|
|
79
80
|
'orlen-paczka': orlenPaczka, 'posta-slovenije': postaSlovenije, postlogistics, ppl, ptt, sagawa, sameday, speedx, speedy, sto, 'swiss-post': swissPost, 'thailand-post': thailandPost,
|
|
80
81
|
tnt, ups, venipak, 'vinted-go': vintedGo, yunexpress, 'yurtici-kargo': yurticiKargo,
|
|
81
82
|
};
|
package/dist/data/catalog.json
CHANGED
|
@@ -6708,7 +6708,9 @@
|
|
|
6708
6708
|
"timezone": "Europe/Zagreb",
|
|
6709
6709
|
"tracking": {
|
|
6710
6710
|
"mode": "automatic",
|
|
6711
|
-
"adapter": "
|
|
6711
|
+
"adapter": "hrvatska-posta",
|
|
6712
|
+
"recognitionRank": 810,
|
|
6713
|
+
"localClocks": true
|
|
6712
6714
|
},
|
|
6713
6715
|
"canaryUrl": "https://posiljka.posta.hr/",
|
|
6714
6716
|
"trackingUrlTemplate": "https://posiljka.posta.hr/hr/tracking/trackingdata?barcode={trackingNumber}",
|
|
@@ -6724,7 +6726,14 @@
|
|
|
6724
6726
|
]
|
|
6725
6727
|
}
|
|
6726
6728
|
],
|
|
6727
|
-
"detectionRules": [
|
|
6729
|
+
"detectionRules": [
|
|
6730
|
+
{
|
|
6731
|
+
"pattern": "^[A-Z]{2}\\d{9}HR$",
|
|
6732
|
+
"confidence": "low",
|
|
6733
|
+
"checksum": "s10",
|
|
6734
|
+
"preferred": true
|
|
6735
|
+
}
|
|
6736
|
+
]
|
|
6728
6737
|
},
|
|
6729
6738
|
"instabox": {
|
|
6730
6739
|
"displayName": "Instabox",
|
|
@@ -4525,7 +4525,9 @@ export declare const CARRIER_CATALOG: {
|
|
|
4525
4525
|
readonly timezone: "Europe/Zagreb";
|
|
4526
4526
|
readonly tracking: {
|
|
4527
4527
|
readonly mode: "automatic";
|
|
4528
|
-
readonly adapter: "
|
|
4528
|
+
readonly adapter: "hrvatska-posta";
|
|
4529
|
+
readonly recognitionRank: 810;
|
|
4530
|
+
readonly localClocks: true;
|
|
4529
4531
|
};
|
|
4530
4532
|
readonly canaryUrl: "https://posiljka.posta.hr/";
|
|
4531
4533
|
readonly trackingUrlTemplate: "https://posiljka.posta.hr/hr/tracking/trackingdata?barcode={trackingNumber}";
|
|
@@ -4535,7 +4537,12 @@ export declare const CARRIER_CATALOG: {
|
|
|
4535
4537
|
readonly pathPattern: "^/(?:hr/|en/)?tracking/trackingdata/?$";
|
|
4536
4538
|
readonly params: readonly ["barcode"];
|
|
4537
4539
|
}];
|
|
4538
|
-
readonly detectionRules: readonly [
|
|
4540
|
+
readonly detectionRules: readonly [{
|
|
4541
|
+
readonly pattern: "^[A-Z]{2}\\d{9}HR$";
|
|
4542
|
+
readonly confidence: "low";
|
|
4543
|
+
readonly checksum: "s10";
|
|
4544
|
+
readonly preferred: true;
|
|
4545
|
+
}];
|
|
4539
4546
|
};
|
|
4540
4547
|
readonly instabox: {
|
|
4541
4548
|
readonly displayName: "Instabox";
|
|
@@ -6709,7 +6709,9 @@ export const CARRIER_CATALOG = {
|
|
|
6709
6709
|
"timezone": "Europe/Zagreb",
|
|
6710
6710
|
"tracking": {
|
|
6711
6711
|
"mode": "automatic",
|
|
6712
|
-
"adapter": "
|
|
6712
|
+
"adapter": "hrvatska-posta",
|
|
6713
|
+
"recognitionRank": 810,
|
|
6714
|
+
"localClocks": true
|
|
6713
6715
|
},
|
|
6714
6716
|
"canaryUrl": "https://posiljka.posta.hr/",
|
|
6715
6717
|
"trackingUrlTemplate": "https://posiljka.posta.hr/hr/tracking/trackingdata?barcode={trackingNumber}",
|
|
@@ -6725,7 +6727,14 @@ export const CARRIER_CATALOG = {
|
|
|
6725
6727
|
]
|
|
6726
6728
|
}
|
|
6727
6729
|
],
|
|
6728
|
-
"detectionRules": [
|
|
6730
|
+
"detectionRules": [
|
|
6731
|
+
{
|
|
6732
|
+
"pattern": "^[A-Z]{2}\\d{9}HR$",
|
|
6733
|
+
"confidence": "low",
|
|
6734
|
+
"checksum": "s10",
|
|
6735
|
+
"preferred": true
|
|
6736
|
+
}
|
|
6737
|
+
]
|
|
6729
6738
|
},
|
|
6730
6739
|
"instabox": {
|
|
6731
6740
|
"displayName": "Instabox",
|
|
@@ -8894,7 +8903,7 @@ export const DETECTION_RULE_IDS = {
|
|
|
8894
8903
|
"gofo-fr": ["gofo-fr-1", "gofo-fr-2"],
|
|
8895
8904
|
"gofo-it": ["gofo-it-1"],
|
|
8896
8905
|
"helthjem": ["helthjem-1", "helthjem-2"],
|
|
8897
|
-
"hrvatska-posta": [],
|
|
8906
|
+
"hrvatska-posta": ["hrvatska-posta-s10"],
|
|
8898
8907
|
"instabox": [],
|
|
8899
8908
|
"intelcom": ["intelcom-1", "intelcom-2", "intelcom-3"],
|
|
8900
8909
|
"j-and-t-cargo": ["j-and-t-cargo-1", "j-and-t-cargo-2"],
|
|
@@ -77,6 +77,7 @@ import { adapter as heppner } from '../carriers/heppner/adapter.js';
|
|
|
77
77
|
import { adapter as hermes } from '../carriers/hermes/adapter.js';
|
|
78
78
|
import { adapter as hermesDe } from '../carriers/hermes-de/adapter.js';
|
|
79
79
|
import { adapter as hongkongPost } from '../carriers/hongkong-post/adapter.js';
|
|
80
|
+
import { adapter as hrvatskaPosta } from '../carriers/hrvatska-posta/adapter.js';
|
|
80
81
|
import { adapter as indiaPost } from '../carriers/india-post/adapter.js';
|
|
81
82
|
import { adapter as inpost } from '../carriers/inpost/adapter.js';
|
|
82
83
|
import { adapter as intelcom } from '../carriers/intelcom/adapter.js';
|
|
@@ -228,6 +229,7 @@ export const REGISTRY = {
|
|
|
228
229
|
"hermes": hermes,
|
|
229
230
|
"hermes-de": hermesDe,
|
|
230
231
|
"hongkong-post": hongkongPost,
|
|
232
|
+
"hrvatska-posta": hrvatskaPosta,
|
|
231
233
|
"india-post": indiaPost,
|
|
232
234
|
"inpost": inpost,
|
|
233
235
|
"intelcom": intelcom,
|
|
@@ -401,7 +403,7 @@ export const REGISTRY = {
|
|
|
401
403
|
"hermes": "hermes",
|
|
402
404
|
"hermes-de": "hermes-de",
|
|
403
405
|
"hongkong-post": "hongkong-post",
|
|
404
|
-
"hrvatska-posta": "
|
|
406
|
+
"hrvatska-posta": "hrvatska-posta",
|
|
405
407
|
"india-post": "india-post",
|
|
406
408
|
"inpost": "inpost",
|
|
407
409
|
"instabox": "universal",
|
package/dist/server/openapi.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-parcel-scraper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.92.0-main.985",
|
|
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",
|