universal-parcel-scraper 0.769.9-main.4113 → 0.770.0-main.4115

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 **631 dedicated adapters** and **5 universal fallbacks**
16
+ **3,500+ carriers** through **632 dedicated adapters** and **5 universal fallbacks**
17
17
 
18
18
  <sub>807 catalog entries · 135 countries represented</sub>
19
19
  <!-- /GENERATED:summary -->
@@ -161,7 +161,7 @@ the command line's `track --source ParcelsApp` does the same.
161
161
  <!-- GENERATED:stages -->
162
162
  <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">
163
163
 
164
- The carrier folders record 16,928 such statuses, each filed under one stage.
164
+ The carrier folders record 16,929 such statuses, each filed under one stage.
165
165
  <!-- /GENERATED:stages -->
166
166
 
167
167
  Wording nobody recorded yet goes through a classifier that reads seven European languages.
package/data/catalog.json CHANGED
@@ -16959,7 +16959,8 @@
16959
16959
  "timezone": "Europe/Istanbul",
16960
16960
  "tracking": {
16961
16961
  "mode": "automatic",
16962
- "adapter": "universal"
16962
+ "adapter": "horoz",
16963
+ "localClocks": true
16963
16964
  },
16964
16965
  "canaryUrl": "https://app3.horoz.com.tr/wsKurumsal/_genel/frmGonderiTakip.aspx",
16965
16966
  "trackingUrlTemplate": "https://app3.horoz.com.tr/wsKurumsal/_genel/frmAtfDetay.aspx?Ptn={trackingNumber}",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "catalog": 807,
3
- "activeAdapters": 631,
3
+ "activeAdapters": 632,
4
4
  "countries": 135,
5
5
  "referenceCarriers": 100,
6
6
  "comparison": {
@@ -13098,7 +13098,8 @@ var Ze = {
13098
13098
  timezone: "Europe/Istanbul",
13099
13099
  tracking: {
13100
13100
  mode: "automatic",
13101
- adapter: "universal"
13101
+ adapter: "horoz",
13102
+ localClocks: !0
13102
13103
  },
13103
13104
  canaryUrl: "https://app3.horoz.com.tr/wsKurumsal/_genel/frmGonderiTakip.aspx",
13104
13105
  trackingUrlTemplate: "https://app3.horoz.com.tr/wsKurumsal/_genel/frmAtfDetay.aspx?Ptn={trackingNumber}",
@@ -0,0 +1,17 @@
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
+ /** The delivery-note page, which looks up the number in its address. */
5
+ export declare const HOROZ_PAGE = "https://app3.horoz.com.tr/wsKurumsal/_genel/frmAtfDetay.aspx";
6
+ /** The delivery-note page's address for a number, as Horoz's links build it. */
7
+ export declare function horozLookupUrl(number: string): string;
8
+ export declare class HorozTracker {
9
+ private readonly options;
10
+ constructor(options?: {
11
+ fetcher?: typeof fetch;
12
+ recorder?: StepRecorder;
13
+ userAgent?: string;
14
+ });
15
+ fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
16
+ }
17
+ export declare const adapter: AdapterFactory;
@@ -0,0 +1,52 @@
1
+ import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
2
+ import { ChallengeError, 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 { CARRIER, HOROZ_MAX_BYTES, normalizeHorozNumber, parseHoroz, PROVIDER } from './parser.js';
8
+ /** The delivery-note page, which looks up the number in its address. */
9
+ export const HOROZ_PAGE = 'https://app3.horoz.com.tr/wsKurumsal/_genel/frmAtfDetay.aspx';
10
+ const REDIRECTS = [301, 302, 303, 307, 308];
11
+ // The page answers an unknown number with empty fields: a missing route is not a missing shipment.
12
+ const GONE = [404, 405, 410];
13
+ /** The delivery-note page's address for a number, as Horoz's links build it. */
14
+ export function horozLookupUrl(number) {
15
+ return `${HOROZ_PAGE}?${new URLSearchParams({ Ptn: number })}`;
16
+ }
17
+ export class HorozTracker {
18
+ options;
19
+ constructor(options = {}) {
20
+ this.options = options;
21
+ }
22
+ async fetch(raw, context = {}) {
23
+ const number = normalizeHorozNumber(raw);
24
+ const read = async (signal, timeoutMs) => {
25
+ const { response, bytes } = await fetchBounded(horozLookupUrl(number), { method: 'GET', signal, headers: {
26
+ Accept: 'text/html', 'User-Agent': userAgentOf(this.options.userAgent),
27
+ } }, { provider: PROVIDER, timeoutMs: Math.max(1, Math.floor(timeoutMs)), maxBytes: HOROZ_MAX_BYTES, redirect: 'manual',
28
+ fetcher: this.options.fetcher, allowHttpStatuses: [...REDIRECTS, ...GONE, 403] });
29
+ const status = response.status;
30
+ if (status === 403)
31
+ throw new ChallengeError(PROVIDER, 'Horoz Lojistik refused the lookup', { status });
32
+ if (REDIRECTS.includes(status) || GONE.includes(status))
33
+ throw new TransportError(PROVIDER, 'Horoz Lojistik tracking page is unavailable', { status });
34
+ return parseHoroz(decodeText(bytes), number);
35
+ };
36
+ // A read that fails to reach the site, or hangs, gets one more try: the
37
+ // first gets half of the budget, so the retry fits in the rest.
38
+ return runSteps({ carrier: CARRIER, 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 HorozTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
47
+ return {
48
+ id: CARRIER, recordsSteps: true, steps: ['direct', 'retry'],
49
+ track: (input, context) => tracker.fetch(input.number, context),
50
+ recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizeHorozNumber(number))),
51
+ };
52
+ };
@@ -17,18 +17,64 @@
17
17
  "timezone": "Europe/Istanbul",
18
18
  "tracking": {
19
19
  "mode": "automatic",
20
- "adapter": "universal",
21
- "steps": []
20
+ "adapter": "horoz",
21
+ "steps": [
22
+ "direct",
23
+ "retry"
24
+ ],
25
+ "localClocks": true
22
26
  },
23
- "capabilities": [],
27
+ "capabilities": [
28
+ "history"
29
+ ],
24
30
  "portal": {
25
31
  "url": "https://app3.horoz.com.tr/wsKurumsal/_genel/frmAtfDetay.aspx?Ptn={trackingNumber}",
26
32
  "searchUrl": "https://app3.horoz.com.tr/wsKurumsal/_genel/frmGonderiTakip.aspx",
27
33
  "canaryUrl": "https://app3.horoz.com.tr/wsKurumsal/_genel/frmGonderiTakip.aspx",
28
- "shows": [],
29
- "scraped": [],
30
- "discarded": [],
31
- "unavailable": []
34
+ "shows": [
35
+ "status",
36
+ "history",
37
+ "location",
38
+ "eta",
39
+ "delivered_at",
40
+ "service_name",
41
+ "waybill",
42
+ "sender_name",
43
+ "recipient_name",
44
+ "origin",
45
+ "destination",
46
+ "recipient_address",
47
+ "parcel_count",
48
+ "volume",
49
+ "order_contents",
50
+ "vehicle"
51
+ ],
52
+ "scraped": [
53
+ "status",
54
+ "history",
55
+ "location",
56
+ "eta"
57
+ ],
58
+ "discarded": [
59
+ "delivered_at",
60
+ "service_name",
61
+ "waybill",
62
+ "sender_name",
63
+ "recipient_name",
64
+ "origin",
65
+ "destination",
66
+ "recipient_address",
67
+ "parcel_count",
68
+ "volume",
69
+ "order_contents",
70
+ "vehicle"
71
+ ],
72
+ "unavailable": [
73
+ "provider_code",
74
+ "offset_timestamps",
75
+ "pickup_point",
76
+ "proof_of_delivery"
77
+ ]
32
78
  },
33
79
  "links": [
34
80
  {
@@ -0,0 +1,21 @@
1
+ import type { CarrierResult } from '../../core/result/index.js';
2
+ export declare const PROVIDER = "Horoz Lojistik";
3
+ export declare const CARRIER = "horoz";
4
+ export declare const HOROZ_MAX_BYTES: number;
5
+ /** A number for Horoz Lojistik's tracker; anything else is not one it issues. */
6
+ export declare function normalizeHorozNumber(raw: string): string;
7
+ /** A firewall or bot-check page in place of the tracker's own. */
8
+ export declare function isBlockPage(html: string): boolean;
9
+ /**
10
+ * The delivery note (ATF) Horoz Lojistik's tracker shows at
11
+ * `frmAtfDetay.aspx?Ptn={number}`: a headline status, the note's details,
12
+ * the masked sender and recipient, and a table of movements, oldest first.
13
+ * Each movement is a departure from or an arrival at a centre, with its
14
+ * clock; a delivered parcel's last row is the delivery. A number the tracker
15
+ * does not know gets the same page with every field empty and no table.
16
+ *
17
+ * The page's form must name the whole number asked. The masked names, the
18
+ * provinces and addresses, the waybill, the vehicle plates, the destination
19
+ * centres and the record clocks are not read.
20
+ */
21
+ export declare function parseHoroz(html: string, raw: string): CarrierResult;
@@ -0,0 +1,144 @@
1
+ import { load } from 'cheerio';
2
+ import { DateTime } from 'luxon';
3
+ import { normalizeTrackingNumber } from '../../core/detection/index.js';
4
+ import { ChallengeError, IndeterminateError, InvalidInputError, NoHistoryError, NotFoundError, SchemaError } from '../../core/errors/index.js';
5
+ import { languageStageStatus } from '../../core/status/index.js';
6
+ import { clean } from '../../core/transport/index.js';
7
+ import { horozStage } from './status.js';
8
+ export const PROVIDER = 'Horoz Lojistik';
9
+ export const CARRIER = 'horoz';
10
+ // A delivery note with a long movement list is under thirty kilobytes.
11
+ export const HOROZ_MAX_BYTES = 512 * 1024;
12
+ const NUMBER = /^\d{14}$/;
13
+ const HEADINGS = ['İşlem', 'Giriş/Çıkış Merkezi', 'Tarihi', 'Plaka', 'Varış Merkezi', 'İşlem Tarihi'];
14
+ // "14.03.2026 16:27:05": the movement's clock, with seconds.
15
+ const CLOCK = /^(\d{2})\.(\d{2})\.(\d{4}) (\d{2}):(\d{2}):(\d{2})$/;
16
+ const DAY = /^(\d{2})\.(\d{2})\.(\d{4})$/;
17
+ // The page posts back to itself, so its form names the number it was opened with.
18
+ const FORM_ACTION = /^\.\/frmAtfDetay\.aspx\?Ptn=([^&#]*)$/;
19
+ // Firewall and bot-check pages that could stand in for the tracker's own.
20
+ const BLOCK = /<title>\s*(?:Just a moment|Attention Required|Access Denied|Request Rejected)|The requested URL was rejected|_cf_chl_opt|cf-turnstile|challenge-platform|captcha/i;
21
+ const MAX_ROWS = 500;
22
+ const FINAL = new Set(['delivered', 'returned']);
23
+ /** A number for Horoz Lojistik's tracker; anything else is not one it issues. */
24
+ export function normalizeHorozNumber(raw) {
25
+ const number = typeof raw === 'string' && raw.length <= 64 ? normalizeTrackingNumber(raw) : '';
26
+ if (!NUMBER.test(number))
27
+ throw new InvalidInputError(PROVIDER, 'Horoz Lojistik numbers are fourteen digits');
28
+ return number;
29
+ }
30
+ /** A firewall or bot-check page in place of the tracker's own. */
31
+ export function isBlockPage(html) {
32
+ return BLOCK.test(html.slice(0, 64_000));
33
+ }
34
+ /** A movement's date and second as one wall time. The page gives no zone. */
35
+ function wallClock(text) {
36
+ const match = CLOCK.exec(text);
37
+ // UTC only checks the calendar fields; no offset is assigned.
38
+ const parsed = match ? DateTime.fromObject({ year: Number(match[3]), month: Number(match[2]), day: Number(match[1]),
39
+ hour: Number(match[4]), minute: Number(match[5]), second: Number(match[6]) }, { zone: 'UTC' }) : null;
40
+ // Luxon rolls 24:00 over to the next day; only a clock that reads back unchanged is kept.
41
+ if (!match || !parsed?.isValid || parsed.year < 2000 || parsed.toFormat('dd.MM.yyyy HH:mm:ss') !== text) {
42
+ throw new SchemaError(PROVIDER, 'Horoz Lojistik returned an invalid movement time');
43
+ }
44
+ return parsed.toFormat("yyyy-MM-dd'T'HH:mm:ss");
45
+ }
46
+ /** The delivery date the note expects, as a calendar day, when it gives a real one. */
47
+ function expectedDay(text) {
48
+ const match = DAY.exec(text);
49
+ const parsed = match ? DateTime.fromObject({ year: Number(match[3]), month: Number(match[2]), day: Number(match[1]) }, { zone: 'UTC' }) : null;
50
+ return parsed?.isValid && parsed.year >= 2000 && parsed.toFormat('dd.MM.yyyy') === text ? parsed.toISODate() : null;
51
+ }
52
+ /**
53
+ * The delivery note (ATF) Horoz Lojistik's tracker shows at
54
+ * `frmAtfDetay.aspx?Ptn={number}`: a headline status, the note's details,
55
+ * the masked sender and recipient, and a table of movements, oldest first.
56
+ * Each movement is a departure from or an arrival at a centre, with its
57
+ * clock; a delivered parcel's last row is the delivery. A number the tracker
58
+ * does not know gets the same page with every field empty and no table.
59
+ *
60
+ * The page's form must name the whole number asked. The masked names, the
61
+ * provinces and addresses, the waybill, the vehicle plates, the destination
62
+ * centres and the record clocks are not read.
63
+ */
64
+ export function parseHoroz(html, raw) {
65
+ const number = normalizeHorozNumber(raw);
66
+ if (isBlockPage(html))
67
+ throw new ChallengeError(PROVIDER, 'Horoz Lojistik answered with a browser check');
68
+ const $ = load(html);
69
+ const forms = $('form#form1');
70
+ const action = forms.length === 1 ? FORM_ACTION.exec(forms.attr('action') ?? '') : null;
71
+ const note = $('span#lblatfno');
72
+ if (!action || note.length !== 1 || $('span#LblBaslik').length !== 1)
73
+ throw new SchemaError(PROVIDER, 'Horoz Lojistik returned an unexpected page');
74
+ let asked;
75
+ try {
76
+ asked = decodeURIComponent(action[1]);
77
+ }
78
+ catch {
79
+ throw new SchemaError(PROVIDER, 'Horoz Lojistik returned an unexpected page');
80
+ }
81
+ if (asked !== number)
82
+ throw new IndeterminateError(PROVIDER, 'Horoz Lojistik answered for another shipment');
83
+ const tables = $('table#grdAtfHareket');
84
+ if (!clean(note.text(), 40)) {
85
+ // Empty fields and no movements are the answer for an unknown number.
86
+ if (!tables.length)
87
+ throw new NotFoundError(PROVIDER);
88
+ throw new SchemaError(PROVIDER, 'Horoz Lojistik returned movements without a delivery note');
89
+ }
90
+ if (!tables.length)
91
+ throw new NoHistoryError(PROVIDER);
92
+ const rows = tables.length === 1 ? tables.find('tr') : null;
93
+ const headings = rows?.first().children('td').map((_, element) => clean($(element).text(), 40)).get() ?? [];
94
+ if (!rows || headings.join('|') !== HEADINGS.join('|'))
95
+ throw new SchemaError(PROVIDER, 'Horoz Lojistik history layout changed');
96
+ if (rows.length - 1 > MAX_ROWS)
97
+ throw new SchemaError(PROVIDER, 'Horoz Lojistik returned an oversized history');
98
+ const seen = new Set();
99
+ const movements = [];
100
+ rows.slice(1).each((index, element) => {
101
+ const cells = $(element).children('td');
102
+ if (cells.length !== HEADINGS.length)
103
+ throw new SchemaError(PROVIDER, 'Horoz Lojistik returned an invalid movement');
104
+ const description = clean(cells.eq(0).text(), 200);
105
+ if (!description)
106
+ throw new SchemaError(PROVIDER, 'Horoz Lojistik returned a movement without wording');
107
+ const centre = clean(cells.eq(1).text(), 200);
108
+ const local = wallClock(clean(cells.eq(2).text(), 60));
109
+ // A row the page lists twice is kept once.
110
+ const identity = JSON.stringify([local, description, centre]);
111
+ if (seen.has(identity))
112
+ return;
113
+ seen.add(identity);
114
+ const staged = horozStage(description);
115
+ movements.push({ local, index, event: {
116
+ local_time: local,
117
+ description,
118
+ ...(centre ? { location: centre } : {}),
119
+ ...(staged ? { stage: staged.stage, stage_source: staged.source } : {}),
120
+ } });
121
+ });
122
+ if (!movements.length)
123
+ throw new NoHistoryError(PROVIDER);
124
+ // Newest first; of equal clocks, the row further down the page is the newer.
125
+ movements.sort((left, right) => right.local.localeCompare(left.local) || right.index - left.index);
126
+ const events = movements.map(({ event }) => event);
127
+ const newest = events[0];
128
+ // The headline sums the note up; without a stage of its own, the newest staged movement stands.
129
+ const headline = clean($('span#LblBaslik').text(), 200);
130
+ const summary = headline ? horozStage(headline) : null;
131
+ const latest = events.find((event) => event.stage);
132
+ const current = summary ?? (latest ? { stage: latest.stage, source: latest.stage_source } : null);
133
+ const stage = current?.stage;
134
+ const expected = stage && FINAL.has(stage) ? null : expectedDay(clean($('span#lblolasiteslimtarihi').text(), 40));
135
+ return {
136
+ status: stage ? languageStageStatus(stage) : 'unknown',
137
+ ...(stage ? { current_stage: stage, current_stage_source: current.source } : {}),
138
+ last_status_text: headline || newest.description,
139
+ last_update: null,
140
+ last_update_local: newest.local_time,
141
+ expected_delivery: expected,
142
+ events,
143
+ };
144
+ }
@@ -0,0 +1,8 @@
1
+ import { type ClassifiedWording, type Stage } from '../../core/status/index.js';
2
+ import { type CarrierStatusMap } from '../../core/status/statusMap.js';
3
+ /** The stage Horoz Lojistik's map gives a wording in `normalizeStatusWording` form. */
4
+ export declare function horozMappedStage(wording: string): Stage | undefined;
5
+ /** A wording's stage: the map, else the shared wording rules, else nothing. */
6
+ export declare function horozStage(wording: string): ClassifiedWording | null;
7
+ /** Every wording seen is mapped. */
8
+ export declare const statusMap: CarrierStatusMap;
@@ -0,0 +1,39 @@
1
+ import { classifyWording } from '../../core/status/index.js';
2
+ import { normalizeStatusWording } from '../../core/status/statusMap.js';
3
+ /**
4
+ * The wordings of Horoz Lojistik's delivery note, which carry no code: the
5
+ * movements of its table, its headline, and the states complaints quote from
6
+ * its tracking screen. Another wording falls to the shared wording rules.
7
+ */
8
+ const WORDINGS = new Map([
9
+ // A departure from, or an arrival at, a centre: the sender's depot or one of Horoz's.
10
+ ['Çıkış', 'in_transit'],
11
+ ['Giriş', 'in_transit'],
12
+ ['dağıtım şubesinde', 'in_transit'],
13
+ ['dağıtımda', 'out_for_delivery'],
14
+ ['dağıtıma çıktı', 'out_for_delivery'],
15
+ // Not delivered: the recipient was not found, or the address was not enough.
16
+ ['adreste bulunamadı', 'failed_attempt'],
17
+ ['siparişiniz teslim edilememiştir', 'failed_attempt'],
18
+ ['adres yetersiz', 'failed_attempt'],
19
+ ['Teslim', 'delivered'],
20
+ ['teslim edildi', 'delivered'],
21
+ ['GÖNDERİNİZ TESLİM EDİLMİŞTİR', 'delivered'],
22
+ ].map(([wording, stage]) => [normalizeStatusWording(wording), stage]));
23
+ /** The stage Horoz Lojistik's map gives a wording in `normalizeStatusWording` form. */
24
+ export function horozMappedStage(wording) {
25
+ return WORDINGS.get(wording);
26
+ }
27
+ /** A wording's stage: the map, else the shared wording rules, else nothing. */
28
+ export function horozStage(wording) {
29
+ const mapped = horozMappedStage(normalizeStatusWording(wording));
30
+ if (mapped)
31
+ return { stage: mapped, source: 'carrier_map' };
32
+ const worded = classifyWording(wording, 'in_transit');
33
+ return worded.source === 'none' ? null : worded;
34
+ }
35
+ /** Every wording seen is mapped. */
36
+ export const statusMap = {
37
+ stage: (_code, wording) => horozMappedStage(wording),
38
+ gaps: [],
39
+ };
@@ -4,6 +4,7 @@
4
4
  {"wording": "Çıkış", "stage": "in_transit", "language": "tr", "confirmedBy": "portal", "note": "Departure from a centre, in the delivery note's movement list."},
5
5
  {"wording": "Giriş", "stage": "in_transit", "language": "tr", "confirmedBy": "portal", "note": "Arrival at a centre, in the delivery note's movement list."},
6
6
  {"wording": "Teslim", "stage": "delivered", "language": "tr", "confirmedBy": "portal", "note": "Last movement of a delivered parcel."},
7
+ {"wording": "GÖNDERİNİZ TESLİM EDİLMİŞTİR", "stage": "delivered", "language": "tr", "confirmedBy": "portal", "note": "The delivery note's headline once the parcel is delivered."},
7
8
  {"wording": "dağıtım şubesinde", "stage": "in_transit", "language": "tr", "confirmedBy": "source", "note": "Quoted in Şikayetvar complaints as the tracking state; at the delivery branch."},
8
9
  {"wording": "dağıtımda", "stage": "out_for_delivery", "language": "tr", "confirmedBy": "source", "note": "Quoted in Şikayetvar complaints as the tracking state."},
9
10
  {"wording": "dağıtıma çıktı", "stage": "out_for_delivery", "language": "tr", "confirmedBy": "source", "note": "Quoted in a Şikayetvar complaint as the tracking state."},
@@ -285,6 +285,7 @@ import { statusMap as hermesDe } from '../../carriers/hermes-de/status.js';
285
285
  import { statusMap as hispapost } from '../../carriers/hispapost/status.js';
286
286
  import { statusMap as homexpress } from '../../carriers/homexpress/status.js';
287
287
  import { statusMap as hongkongPost } from '../../carriers/hongkong-post/status.js';
288
+ import { statusMap as horoz } from '../../carriers/horoz/status.js';
288
289
  import { statusMap as houngAhLoun } from '../../carriers/houng-ah-loun/status.js';
289
290
  import { statusMap as hovership } from '../../carriers/hovership/status.js';
290
291
  import { statusMap as hrParcel } from '../../carriers/hr-parcel/status.js';
@@ -680,7 +681,7 @@ export const STATUS_MAPS = {
680
681
  globalpost, 'gms-worldwide': gmsWorldwide, 'go-express': goExpress, go4, gobolt, gofo, 'gofo-ca': gofoCa, 'gofo-fr': gofoFr, 'gofo-it': gofoIt, 'gofo-nl': gofoNl, 'gogo-xpress': gogoXpress,
681
682
  'grand-slam-express': grandSlamExpress, 'gs-postbox': gsPostbox, 'gsi-express': gsiExpress, 'gta-airskip': gtaAirskip, 'gtl-goto': gtlGoto, guatex, gzyl,
682
683
  hanjin, 'happy-post': happyPost, haypost, 'hct-logistics': hctLogistics, helthjem, heppner, hepsijet, hermes, 'hermes-at': hermesAt, 'hermes-de': hermesDe, hispapost, homexpress, 'hongkong-post': hongkongPost,
683
- 'houng-ah-loun': houngAhLoun, hovership, 'hr-parcel': hrParcel, 'hrvatska-posta': hrvatskaPosta, huahan, 'hunter-express': hunterExpress, hywl,
684
+ horoz, 'houng-ah-loun': houngAhLoun, hovership, 'hr-parcel': hrParcel, 'hrvatska-posta': hrvatskaPosta, huahan, 'hunter-express': hunterExpress, hywl,
684
685
  'icc-worldwide': iccWorldwide, 'ics-courier': icsCourier, 'ids-logistik': idsLogistik, 'ilyang-logis': ilyangLogis, imile,
685
686
  'indah-logistik': indahLogistik, 'india-post': indiaPost, inpost, instabox, 'integral-pack': integralPack, 'intel-valley': intelValley, intelcom, 'internet-express': internetExpress, interrapidisimo,
686
687
  'intex-paketdienst': intexPaketdienst, 'israel-post': israelPost, 'j-and-t': jAndT, 'j-and-t-au': jAndTAu, 'j-and-t-de': jAndTDe, 'j-and-t-cargo': jntCargo,
@@ -16959,7 +16959,8 @@
16959
16959
  "timezone": "Europe/Istanbul",
16960
16960
  "tracking": {
16961
16961
  "mode": "automatic",
16962
- "adapter": "universal"
16962
+ "adapter": "horoz",
16963
+ "localClocks": true
16963
16964
  },
16964
16965
  "canaryUrl": "https://app3.horoz.com.tr/wsKurumsal/_genel/frmGonderiTakip.aspx",
16965
16966
  "trackingUrlTemplate": "https://app3.horoz.com.tr/wsKurumsal/_genel/frmAtfDetay.aspx?Ptn={trackingNumber}",
@@ -10990,7 +10990,8 @@ export declare const CARRIER_CATALOG: {
10990
10990
  readonly timezone: "Europe/Istanbul";
10991
10991
  readonly tracking: {
10992
10992
  readonly mode: "automatic";
10993
- readonly adapter: "universal";
10993
+ readonly adapter: "horoz";
10994
+ readonly localClocks: true;
10994
10995
  };
10995
10996
  readonly canaryUrl: "https://app3.horoz.com.tr/wsKurumsal/_genel/frmGonderiTakip.aspx";
10996
10997
  readonly trackingUrlTemplate: "https://app3.horoz.com.tr/wsKurumsal/_genel/frmAtfDetay.aspx?Ptn={trackingNumber}";
@@ -16960,7 +16960,8 @@ export const CARRIER_CATALOG = {
16960
16960
  "timezone": "Europe/Istanbul",
16961
16961
  "tracking": {
16962
16962
  "mode": "automatic",
16963
- "adapter": "universal"
16963
+ "adapter": "horoz",
16964
+ "localClocks": true
16964
16965
  },
16965
16966
  "canaryUrl": "https://app3.horoz.com.tr/wsKurumsal/_genel/frmGonderiTakip.aspx",
16966
16967
  "trackingUrlTemplate": "https://app3.horoz.com.tr/wsKurumsal/_genel/frmAtfDetay.aspx?Ptn={trackingNumber}",
@@ -274,6 +274,7 @@ import { adapter as hermesDe } from '../carriers/hermes-de/adapter.js';
274
274
  import { adapter as hispapost } from '../carriers/hispapost/adapter.js';
275
275
  import { adapter as homexpress } from '../carriers/homexpress/adapter.js';
276
276
  import { adapter as hongkongPost } from '../carriers/hongkong-post/adapter.js';
277
+ import { adapter as horoz } from '../carriers/horoz/adapter.js';
277
278
  import { adapter as houngAhLoun } from '../carriers/houng-ah-loun/adapter.js';
278
279
  import { adapter as hovership } from '../carriers/hovership/adapter.js';
279
280
  import { adapter as hrParcel } from '../carriers/hr-parcel/adapter.js';
@@ -907,6 +908,7 @@ export const REGISTRY = {
907
908
  "hispapost": hispapost,
908
909
  "homexpress": homexpress,
909
910
  "hongkong-post": hongkongPost,
911
+ "horoz": horoz,
910
912
  "houng-ah-loun": houngAhLoun,
911
913
  "hovership": hovership,
912
914
  "hr-parcel": hrParcel,
@@ -1603,7 +1605,7 @@ export const REGISTRY = {
1603
1605
  "hispapost": "hispapost",
1604
1606
  "homexpress": "homexpress",
1605
1607
  "hongkong-post": "hongkong-post",
1606
- "horoz": "universal",
1608
+ "horoz": "horoz",
1607
1609
  "hound-express": "universal",
1608
1610
  "houng-ah-loun": "houng-ah-loun",
1609
1611
  "hovership": "hovership",
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.769.9-main.4113"
5
+ "version": "0.770.0-main.4115"
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.769.9-main.4113",
3
+ "version": "0.770.0-main.4115",
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",