universal-parcel-scraper 0.78.0-main.939 → 0.79.0-main.941

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 **135 dedicated adapters** and **5 universal fallbacks**
16
+ **3,500+ carriers** through **136 dedicated adapters** and **5 universal fallbacks**
17
17
 
18
18
  <sub>207 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 3,852 such statuses, each filed under one stage.
158
+ The carrier folders record 3,871 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
@@ -8285,7 +8285,9 @@
8285
8285
  "timezone": "Europe/Istanbul",
8286
8286
  "tracking": {
8287
8287
  "mode": "automatic",
8288
- "adapter": "universal"
8288
+ "adapter": "yurtici-kargo",
8289
+ "recognitionRank": 66,
8290
+ "localClocks": true
8289
8291
  },
8290
8292
  "canaryUrl": "https://www.yurticikargo.com/tr/online-servisler/gonderi-sorgula",
8291
8293
  "trackingUrlTemplate": "https://www.yurticikargo.com/tr/online-servisler/gonderi-sorgula?code={trackingNumber}",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "catalog": 207,
3
- "activeAdapters": 135,
3
+ "activeAdapters": 136,
4
4
  "countries": 66,
5
5
  "referenceCarriers": 100,
6
6
  "comparison": {
@@ -6522,7 +6522,9 @@ var ye = {
6522
6522
  timezone: "Europe/Istanbul",
6523
6523
  tracking: {
6524
6524
  mode: "automatic",
6525
- adapter: "universal"
6525
+ adapter: "yurtici-kargo",
6526
+ recognitionRank: 66,
6527
+ localClocks: !0
6526
6528
  },
6527
6529
  canaryUrl: "https://www.yurticikargo.com/tr/online-servisler/gonderi-sorgula",
6528
6530
  trackingUrlTemplate: "https://www.yurticikargo.com/tr/online-servisler/gonderi-sorgula?code={trackingNumber}",
@@ -11772,7 +11774,8 @@ var _c = {
11772
11774
  tnt: 44,
11773
11775
  ukrposhta: 5,
11774
11776
  uniuni: 33,
11775
- venipak: 63
11777
+ venipak: 63,
11778
+ "yurtici-kargo": 66
11776
11779
  }, vc = 5;
11777
11780
  function yc(e) {
11778
11781
  let t = y(e);
@@ -0,0 +1,18 @@
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 YURTICI_SEARCH_URL = "https://www.yurticikargo.com/service/shipmentstrackingsearch";
5
+ export declare const YURTICI_DETAIL_URL = "https://www.yurticikargo.com/service/shipmentstracking";
6
+ export declare const YURTICI_MOVEMENT_URL = "https://www.yurticikargo.com/service/shipmenttrackingmovement";
7
+ export declare class YurticiKargoTracker {
8
+ private readonly options;
9
+ constructor(options?: {
10
+ fetcher?: typeof fetch;
11
+ recorder?: StepRecorder;
12
+ userAgent?: string;
13
+ });
14
+ fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
15
+ private lookup;
16
+ private get;
17
+ }
18
+ export declare const adapter: AdapterFactory;
@@ -0,0 +1,72 @@
1
+ import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
2
+ import { ChallengeError, SchemaError, TransportError, UpstreamHttpError } from '../../core/errors/index.js';
3
+ import { runSteps } from '../../core/runner/index.js';
4
+ import { NOOP_RECORDER } from '../../core/telemetry/index.js';
5
+ import { decodeText, fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
6
+ import { isRecord } from '../../core/types.js';
7
+ import { normalizeYurticiNumber, parseYurtici, parseYurticiSearch, PROVIDER, YURTICI_MAX_BYTES } from './parser.js';
8
+ // The tracker on www.yurticikargo.com/en/online-servisler/gonderi-sorgula reads these
9
+ // anonymously: a search for the code, then the shipment's details, then its movements.
10
+ const SERVICE = 'https://www.yurticikargo.com/service';
11
+ export const YURTICI_SEARCH_URL = `${SERVICE}/shipmentstrackingsearch`;
12
+ export const YURTICI_DETAIL_URL = `${SERVICE}/shipmentstracking`;
13
+ export const YURTICI_MOVEMENT_URL = `${SERVICE}/shipmenttrackingmovement`;
14
+ /** The firewall in front of www.yurticikargo.com rejects a request with an HTML page. */
15
+ function rejected(bytes) {
16
+ return /Request Rejected|The requested URL was rejected/i.test(decodeText(bytes.subarray(0, 4_000)));
17
+ }
18
+ export class YurticiKargoTracker {
19
+ options;
20
+ constructor(options = {}) {
21
+ this.options = options;
22
+ }
23
+ async fetch(raw, context = {}) {
24
+ const number = normalizeYurticiNumber(raw);
25
+ const budgetMs = context.budgetMs ?? 15_000;
26
+ if (!Number.isFinite(budgetMs) || budgetMs <= 0)
27
+ throw new TypeError('Yurtiçi Kargo budget must be a positive number of milliseconds');
28
+ return runSteps({ carrier: 'yurtici-kargo', budgetMs, signal: context.signal, recorder: this.options.recorder ?? NOOP_RECORDER }, [
29
+ { id: 'direct', run: step => this.lookup(number, step) },
30
+ ]);
31
+ }
32
+ async lookup(number, { signal, remainingMs }) {
33
+ const deadline = performance.now() + remainingMs;
34
+ const get = (url, id) => this.get(url, id, signal, Math.max(1, Math.floor(deadline - performance.now())));
35
+ const search = parseYurticiSearch(await get(YURTICI_SEARCH_URL, number), number);
36
+ const detail = await get(YURTICI_DETAIL_URL, search.id);
37
+ // The tracker lists movements until the parcel is delivered or returned, and only asks then.
38
+ const movements = isOpen(detail) ? await get(YURTICI_MOVEMENT_URL, number) : undefined;
39
+ return parseYurtici(detail, movements, number, search);
40
+ }
41
+ async get(url, id, signal, timeoutMs) {
42
+ signal.throwIfAborted();
43
+ let bytes;
44
+ try {
45
+ ({ bytes } = await fetchBounded(`${url}?${new URLSearchParams({ id, language: 'en' })}`, { signal, headers: { Accept: 'application/json', 'User-Agent': userAgentOf(this.options.userAgent) } }, { provider: PROVIDER, timeoutMs, maxBytes: YURTICI_MAX_BYTES, fetcher: this.options.fetcher, retryTransient: true }));
46
+ }
47
+ catch (error) {
48
+ // A missing route is not a missing shipment.
49
+ if (error instanceof UpstreamHttpError && [404, 410].includes(error.status)) {
50
+ throw new TransportError(PROVIDER, 'Yurtiçi Kargo tracking service is unavailable', { cause: error });
51
+ }
52
+ throw error;
53
+ }
54
+ try {
55
+ return parseJsonBytes(bytes, PROVIDER);
56
+ }
57
+ catch (cause) {
58
+ if (rejected(bytes))
59
+ throw new ChallengeError(PROVIDER, 'Yurtiçi Kargo rejected the tracking request', { cause });
60
+ throw new SchemaError(PROVIDER, 'Yurtiçi Kargo returned invalid tracking JSON', { cause });
61
+ }
62
+ }
63
+ }
64
+ /** Details of a parcel not yet delivered or returned, whose movements the tracker lists. */
65
+ function isOpen(detail) {
66
+ return isRecord(detail) && detail.IsDelivered === false && detail.ErrorMessage === undefined;
67
+ }
68
+ export const adapter = environment => {
69
+ const tracker = new YurticiKargoTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
70
+ return { id: 'yurtici-kargo', recordsSteps: true, steps: ['direct'], track: (input, context) => tracker.fetch(input.number, context),
71
+ recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizeYurticiNumber(number))) };
72
+ };
@@ -17,18 +17,58 @@
17
17
  "timezone": "Europe/Istanbul",
18
18
  "tracking": {
19
19
  "mode": "automatic",
20
- "adapter": "universal",
21
- "steps": []
20
+ "adapter": "yurtici-kargo",
21
+ "steps": [
22
+ "direct"
23
+ ],
24
+ "recognition": {
25
+ "rank": 66
26
+ },
27
+ "localClocks": true
22
28
  },
23
- "capabilities": [],
29
+ "capabilities": [
30
+ "history",
31
+ "location",
32
+ "eta",
33
+ "service_name",
34
+ "pickup_point"
35
+ ],
24
36
  "portal": {
25
37
  "url": "https://www.yurticikargo.com/tr/online-servisler/gonderi-sorgula?code={trackingNumber}",
26
38
  "searchUrl": "https://www.yurticikargo.com/tr/online-servisler/gonderi-sorgula",
27
39
  "canaryUrl": "https://www.yurticikargo.com/tr/online-servisler/gonderi-sorgula",
28
- "shows": [],
29
- "scraped": [],
30
- "discarded": [],
31
- "unavailable": []
40
+ "shows": [
41
+ "status",
42
+ "history",
43
+ "location",
44
+ "eta",
45
+ "service_name",
46
+ "pickup_point",
47
+ "recipient_name",
48
+ "sender_name",
49
+ "signatory",
50
+ "contact_details",
51
+ "payment_details"
52
+ ],
53
+ "scraped": [
54
+ "status",
55
+ "history",
56
+ "location",
57
+ "eta",
58
+ "service_name",
59
+ "pickup_point"
60
+ ],
61
+ "discarded": [
62
+ "recipient_name",
63
+ "sender_name",
64
+ "signatory",
65
+ "contact_details",
66
+ "payment_details"
67
+ ],
68
+ "unavailable": [
69
+ "weight",
70
+ "proof_of_delivery"
71
+ ]
32
72
  },
33
73
  "links": [
34
74
  {
@@ -12,7 +12,7 @@
12
12
  "access": "full_page"
13
13
  },
14
14
  "context": {
15
- "assessment": "candidate"
15
+ "assessment": "confirmed"
16
16
  },
17
17
  "expect": {
18
18
  "carrier": "unknown",
@@ -30,7 +30,7 @@
30
30
  "lbc-express"
31
31
  ]
32
32
  },
33
- "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number."
33
+ "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number; Yurtiçi Kargo's tracking knows the shipment."
34
34
  },
35
35
  {
36
36
  "number": "108886350393",
@@ -43,7 +43,7 @@
43
43
  "access": "full_page"
44
44
  },
45
45
  "context": {
46
- "assessment": "candidate"
46
+ "assessment": "confirmed"
47
47
  },
48
48
  "expect": {
49
49
  "carrier": "unknown",
@@ -61,7 +61,7 @@
61
61
  "lbc-express"
62
62
  ]
63
63
  },
64
- "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number."
64
+ "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number; Yurtiçi Kargo's tracking knows the shipment."
65
65
  },
66
66
  {
67
67
  "number": "114011114595",
@@ -74,7 +74,7 @@
74
74
  "access": "full_page"
75
75
  },
76
76
  "context": {
77
- "assessment": "candidate"
77
+ "assessment": "confirmed"
78
78
  },
79
79
  "expect": {
80
80
  "carrier": "unknown",
@@ -92,7 +92,7 @@
92
92
  "lbc-express"
93
93
  ]
94
94
  },
95
- "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number."
95
+ "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number; Yurtiçi Kargo's tracking knows the shipment."
96
96
  },
97
97
  {
98
98
  "number": "412078984222",
@@ -105,7 +105,7 @@
105
105
  "access": "full_page"
106
106
  },
107
107
  "context": {
108
- "assessment": "candidate"
108
+ "assessment": "confirmed"
109
109
  },
110
110
  "expect": {
111
111
  "carrier": "unknown",
@@ -122,7 +122,7 @@
122
122
  "lbc-express"
123
123
  ]
124
124
  },
125
- "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number."
125
+ "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number; Yurtiçi Kargo's tracking knows the shipment."
126
126
  },
127
127
  {
128
128
  "number": "191329680164",
@@ -135,7 +135,7 @@
135
135
  "access": "full_page"
136
136
  },
137
137
  "context": {
138
- "assessment": "candidate"
138
+ "assessment": "confirmed"
139
139
  },
140
140
  "expect": {
141
141
  "carrier": "unknown",
@@ -153,7 +153,7 @@
153
153
  "lbc-express"
154
154
  ]
155
155
  },
156
- "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number."
156
+ "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number; Yurtiçi Kargo's tracking knows the shipment."
157
157
  },
158
158
  {
159
159
  "number": "118977181376",
@@ -166,7 +166,7 @@
166
166
  "access": "full_page"
167
167
  },
168
168
  "context": {
169
- "assessment": "candidate"
169
+ "assessment": "confirmed"
170
170
  },
171
171
  "expect": {
172
172
  "carrier": "unknown",
@@ -187,7 +187,7 @@
187
187
  "lbc-express"
188
188
  ]
189
189
  },
190
- "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number."
190
+ "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number; Yurtiçi Kargo's tracking knows the shipment."
191
191
  },
192
192
  {
193
193
  "number": "173962210543",
@@ -200,7 +200,7 @@
200
200
  "access": "full_page"
201
201
  },
202
202
  "context": {
203
- "assessment": "candidate"
203
+ "assessment": "confirmed"
204
204
  },
205
205
  "expect": {
206
206
  "carrier": "unknown",
@@ -219,7 +219,7 @@
219
219
  "lbc-express"
220
220
  ]
221
221
  },
222
- "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number."
222
+ "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number; Yurtiçi Kargo's tracking knows the shipment."
223
223
  },
224
224
  {
225
225
  "number": "174361470154",
@@ -232,7 +232,7 @@
232
232
  "access": "full_page"
233
233
  },
234
234
  "context": {
235
- "assessment": "candidate"
235
+ "assessment": "confirmed"
236
236
  },
237
237
  "expect": {
238
238
  "carrier": "unknown",
@@ -250,7 +250,7 @@
250
250
  "lbc-express"
251
251
  ]
252
252
  },
253
- "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number."
253
+ "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number; Yurtiçi Kargo's tracking knows the shipment."
254
254
  },
255
255
  {
256
256
  "number": "308377408751",
@@ -263,7 +263,7 @@
263
263
  "access": "full_page"
264
264
  },
265
265
  "context": {
266
- "assessment": "candidate"
266
+ "assessment": "confirmed"
267
267
  },
268
268
  "expect": {
269
269
  "carrier": "unknown",
@@ -280,7 +280,7 @@
280
280
  "lbc-express"
281
281
  ]
282
282
  },
283
- "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number."
283
+ "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number; Yurtiçi Kargo's tracking knows the shipment."
284
284
  },
285
285
  {
286
286
  "number": "104985633362",
@@ -293,7 +293,7 @@
293
293
  "access": "full_page"
294
294
  },
295
295
  "context": {
296
- "assessment": "candidate"
296
+ "assessment": "confirmed"
297
297
  },
298
298
  "expect": {
299
299
  "carrier": "unknown",
@@ -314,7 +314,7 @@
314
314
  "sagawa"
315
315
  ]
316
316
  },
317
- "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number."
317
+ "note": "A complaint on Şikayetvar's Yurtiçi Kargo page calls it the parcel's tracking number; Yurtiçi Kargo's tracking knows the shipment."
318
318
  }
319
319
  ]
320
320
  }
@@ -0,0 +1,26 @@
1
+ import type { CarrierResult } from '../../core/result/index.js';
2
+ export declare const PROVIDER = "Yurti\u00E7i Kargo";
3
+ export declare const YURTICI_MAX_BYTES = 256000;
4
+ /**
5
+ * What the tracker's field takes: a twelve-digit shipment code or a
6
+ * sixteen-digit international AWB code, digits only.
7
+ */
8
+ export declare function normalizeYurticiNumber(raw: string): string;
9
+ export interface YurticiSearch {
10
+ /** The id the tracker asks for the shipment's details. */
11
+ readonly id: string;
12
+ }
13
+ /**
14
+ * The search the tracker runs first: the shipment's id, or the tracker's
15
+ * notice for a code it does not know. A domestic shipment's id is its code.
16
+ * An international shipment is read from its AWB record, which the adapter
17
+ * does not read.
18
+ */
19
+ export declare function parseYurticiSearch(payload: unknown, raw: string): YurticiSearch;
20
+ /**
21
+ * The shipment's details, with its movements when the tracker lists them.
22
+ * The tracker stops listing movements once a parcel is delivered or returned;
23
+ * the details then give the shipment date and the final status, and the
24
+ * answer is a summary.
25
+ */
26
+ export declare function parseYurtici(detail: unknown, movements: unknown, raw: string, search: YurticiSearch): CarrierResult;
@@ -0,0 +1,250 @@
1
+ import { normalizeTrackingNumber } from '../../core/detection/index.js';
2
+ import { IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
3
+ import { languageStageStatus } from '../../core/status/index.js';
4
+ import { calendarDay } from '../../core/time/index.js';
5
+ import { clean, cleanScalar } from '../../core/transport/index.js';
6
+ import { isRecord } from '../../core/types.js';
7
+ import { yurticiScanStage, yurticiSummaryStage } from './status.js';
8
+ export const PROVIDER = 'Yurtiçi Kargo';
9
+ export const YURTICI_MAX_BYTES = 256_000;
10
+ const MAX_ROWS = 300;
11
+ const MAX_EVENTS = 100;
12
+ /**
13
+ * What the tracker's field takes: a twelve-digit shipment code or a
14
+ * sixteen-digit international AWB code, digits only.
15
+ */
16
+ export function normalizeYurticiNumber(raw) {
17
+ const number = normalizeTrackingNumber(raw);
18
+ if (!/^(?:\d{12}|\d{16})$/.test(number)) {
19
+ throw new InvalidInputError(PROVIDER, 'Yurtiçi Kargo shipment codes are twelve digits and AWB codes sixteen');
20
+ }
21
+ return number;
22
+ }
23
+ /** The tracker's notice for a code it does not know, in English and in Turkish. */
24
+ const NOT_FOUND = new Set([
25
+ 'No record found. Please try with a different tracking code.',
26
+ 'Sorguladığınız gönderi koduyla bir kargo bulunamadı.Lütfen gönderi kodunu kontrol ederek tekrar deneyiniz.',
27
+ ]);
28
+ /** The carrier's notice, or undefined when the reply carries none. */
29
+ function notice(payload) {
30
+ if (payload.ErrorMessage === undefined)
31
+ return undefined;
32
+ return clean(payload.ErrorMessage, 300);
33
+ }
34
+ /** The shipment id the search answers with, as a string of digits. */
35
+ function shipmentId(value) {
36
+ const text = typeof value === 'number' ? (Number.isSafeInteger(value) && value > 0 ? String(value) : '') : cleanScalar(value, 20);
37
+ return /^\d{1,16}$/.test(text) ? text : '';
38
+ }
39
+ /** A shipment id as the twelve-digit code it is printed as. */
40
+ const printed = (id) => id.padStart(12, '0');
41
+ /**
42
+ * The search the tracker runs first: the shipment's id, or the tracker's
43
+ * notice for a code it does not know. A domestic shipment's id is its code.
44
+ * An international shipment is read from its AWB record, which the adapter
45
+ * does not read.
46
+ */
47
+ export function parseYurticiSearch(payload, raw) {
48
+ const number = normalizeYurticiNumber(raw);
49
+ if (!isRecord(payload))
50
+ throw new SchemaError(PROVIDER);
51
+ const message = notice(payload);
52
+ if (message !== undefined) {
53
+ if (NOT_FOUND.has(message))
54
+ throw new NotFoundError(PROVIDER);
55
+ throw new IndeterminateError(PROVIDER, 'Yurtiçi Kargo could not complete the search');
56
+ }
57
+ if (payload.IsAbroad === true) {
58
+ throw new IndeterminateError(PROVIDER, 'Yurtiçi Kargo tracks this shipment through its international record', { reason: 'international_shipment' });
59
+ }
60
+ const id = shipmentId(payload.DocId);
61
+ if (!id || payload.IsAbroad !== false)
62
+ throw new SchemaError(PROVIDER, 'Yurtiçi Kargo returned a search result without a shipment');
63
+ if (number.length === 12 && printed(id) !== number)
64
+ throw new SchemaError(PROVIDER, 'Yurtiçi Kargo did not identify the requested shipment');
65
+ return { id };
66
+ }
67
+ const MONTHS = new Map([
68
+ ...['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'],
69
+ ...['ocak', 'şubat', 'mart', 'nisan', 'mayıs', 'haziran', 'temmuz', 'ağustos', 'eylül', 'ekim', 'kasım', 'aralık'],
70
+ ].map((name, index) => [name, (index % 12) + 1]));
71
+ const month = (name) => MONTHS.get(name.toLocaleLowerCase('tr-TR')) ?? MONTHS.get(name.toLowerCase());
72
+ // "Monday, October 5, 2026 10:30 AM" in English; "5 Ekim 2026 Pazartesi 10:30" in Turkish.
73
+ const ENGLISH_DATE = /^(?:[A-Za-z]+,\s*)?([A-Za-z]+)\s+(\d{1,2}),\s*(\d{4})(?:\s+(\d{1,2}):(\d{2})\s*([AP]M))?$/i;
74
+ const TURKISH_DATE = /^(\d{1,2})\s+(\p{L}+)\s+(\d{4})(?:\s+\p{L}+)?(?:\s+(\d{1,2}):(\d{2}))?$/u;
75
+ /** A day, or a day and a wall clock, as the tracker writes them in either language. */
76
+ function writtenDate(value) {
77
+ const text = clean(value, 64);
78
+ let year, monthNumber, dayNumber, hour, minute;
79
+ const english = ENGLISH_DATE.exec(text);
80
+ const turkish = english ? null : TURKISH_DATE.exec(text);
81
+ if (english) {
82
+ [year, monthNumber, dayNumber] = [Number(english[3]), month(english[1]), Number(english[2])];
83
+ if (english[4] !== undefined) {
84
+ const twelve = Number(english[4]);
85
+ if (twelve < 1 || twelve > 12)
86
+ return null;
87
+ hour = String((twelve % 12) + (english[6].toUpperCase() === 'PM' ? 12 : 0)).padStart(2, '0');
88
+ minute = english[5];
89
+ }
90
+ }
91
+ else if (turkish) {
92
+ [year, monthNumber, dayNumber] = [Number(turkish[3]), month(turkish[2]), Number(turkish[1])];
93
+ hour = turkish[4]?.padStart(2, '0');
94
+ minute = turkish[5];
95
+ }
96
+ else
97
+ return null;
98
+ const day = monthNumber ? calendarDay(year, monthNumber, dayNumber) : null;
99
+ if (!day)
100
+ return null;
101
+ if (hour === undefined)
102
+ return { day };
103
+ if (Number(hour) > 23 || Number(minute) > 59)
104
+ return null;
105
+ return { day, clock: `${day}T${hour}:${minute}:00` };
106
+ }
107
+ /** A `yyyyMMddHHmmss` stamp of the summary as a wall clock. */
108
+ function stamp(value) {
109
+ const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(clean(value, 14));
110
+ if (!match)
111
+ return null;
112
+ const day = calendarDay(Number(match[1]), Number(match[2]), Number(match[3]));
113
+ if (!day || Number(match[4]) > 23 || Number(match[5]) > 59 || Number(match[6]) > 59)
114
+ return null;
115
+ return `${day}T${match[4]}:${match[5]}:${match[6]}`;
116
+ }
117
+ /** A unit's "City - County"; a transfer between units has an empty one. */
118
+ function place(value) {
119
+ const text = clean(value, 120);
120
+ return /^[\s-]*$/.test(text) ? '' : text;
121
+ }
122
+ /** The first movement's wording, which the summary's shipment date stands for. */
123
+ const ISSUED = 'The transport document was issued.';
124
+ /**
125
+ * The movements the tracker lists for a parcel on its way, oldest first by
126
+ * their order number. Each names its unit's city and county, and the unit's
127
+ * own name, which the adapter does not read. A reason, such as why a
128
+ * delivery failed, is appended in brackets; a delivery's reason is never
129
+ * read, as it could name who took the parcel.
130
+ */
131
+ function movementsOf(payload) {
132
+ if (isRecord(payload)) {
133
+ const message = notice(payload);
134
+ if (message !== undefined)
135
+ throw new IndeterminateError(PROVIDER, 'Yurtiçi Kargo could not list the movements');
136
+ }
137
+ if (!Array.isArray(payload) || payload.length > MAX_ROWS)
138
+ throw new SchemaError(PROVIDER, 'Yurtiçi Kargo returned an invalid movement list');
139
+ const rows = payload.map((row, index) => {
140
+ if (!isRecord(row))
141
+ throw new SchemaError(PROVIDER, 'Yurtiçi Kargo returned an invalid movement');
142
+ const order = Number.isSafeInteger(row.OrderNo) ? row.OrderNo : null;
143
+ return { row, order, index };
144
+ });
145
+ // The list comes in its order numbers; without them it keeps the order it came in.
146
+ if (rows.every(({ order }) => order !== null))
147
+ rows.sort((left, right) => left.order - right.order || left.index - right.index);
148
+ const scans = [];
149
+ const seen = new Set();
150
+ for (const { row } of rows) {
151
+ const wording = clean(row.Description, 200);
152
+ if (!wording)
153
+ throw new SchemaError(PROVIDER, 'Yurtiçi Kargo returned a movement without wording');
154
+ const stage = yurticiScanStage(wording);
155
+ const reason = stage === 'delivered' ? '' : clean(row.DetailDescription, 160).replace(/[()]/g, '');
156
+ const description = reason && reason !== wording ? `${wording} (${reason})` : wording;
157
+ const written = writtenDate(row.EventDate);
158
+ const text = clean(row.EventDate, 64);
159
+ if (!written?.clock && !text)
160
+ throw new SchemaError(PROVIDER, 'Yurtiçi Kargo returned a movement without a date');
161
+ const location = place(row.Title);
162
+ const clock = written?.clock ? { local_time: written.clock } : { provider_time_text: text };
163
+ const event = { ...clock, description, ...(location ? { location } : {}),
164
+ ...(stage ? { stage, stage_source: 'carrier_map' } : {}) };
165
+ const key = JSON.stringify(event);
166
+ if (seen.has(key))
167
+ continue;
168
+ seen.add(key);
169
+ scans.push({ event, local: written?.clock ?? null });
170
+ }
171
+ return scans.reverse();
172
+ }
173
+ const cityCounty = (city, county) => [clean(city, 60), clean(county, 60)].filter(Boolean).join(' - ');
174
+ /** Stages before the parcel reaches its recipient, while an estimate still means something. */
175
+ const MOVING = new Set(['registered', 'accepted', 'in_transit', 'out_for_delivery']);
176
+ /**
177
+ * The shipment's details, with its movements when the tracker lists them.
178
+ * The tracker stops listing movements once a parcel is delivered or returned;
179
+ * the details then give the shipment date and the final status, and the
180
+ * answer is a summary.
181
+ */
182
+ export function parseYurtici(detail, movements, raw, search) {
183
+ const number = normalizeYurticiNumber(raw);
184
+ if (!isRecord(detail))
185
+ throw new SchemaError(PROVIDER);
186
+ const message = notice(detail);
187
+ if (message !== undefined) {
188
+ // The search knows the code, but the tracker no longer holds its record.
189
+ if (NOT_FOUND.has(message)) {
190
+ throw new IndeterminateError(PROVIDER, 'Yurtiçi Kargo no longer holds this shipment', { reason: 'history_expired' });
191
+ }
192
+ throw new IndeterminateError(PROVIDER, 'Yurtiçi Kargo could not read the shipment');
193
+ }
194
+ if (shipmentId(detail.Id) !== search.id)
195
+ throw new SchemaError(PROVIDER, 'Yurtiçi Kargo answered for a different shipment');
196
+ const summary = clean(detail.ShipmentStatus, 80);
197
+ if (!summary || typeof detail.IsDelivered !== 'boolean')
198
+ throw new SchemaError(PROVIDER, 'Yurtiçi Kargo returned a shipment without a status');
199
+ const delivered = detail.IsDelivered;
200
+ const listed = movements === undefined ? [] : movementsOf(movements);
201
+ const scans = [...listed];
202
+ if (!scans.length) {
203
+ const issued = stamp(detail.Title);
204
+ const departure = cityCounty(detail.DepartureCityName, detail.DepartureCountyName);
205
+ if (issued)
206
+ scans.push({ local: issued, event: { local_time: issued, description: ISSUED,
207
+ ...(departure ? { location: departure } : {}), stage: 'accepted', stage_source: 'carrier_map' } });
208
+ }
209
+ // A delivered or returned parcel: the summary says which, else whether it was turned back.
210
+ const summaryStage = yurticiSummaryStage(summary);
211
+ const final = delivered
212
+ ? (summaryStage === 'delivered' || summaryStage === 'returned' ? summaryStage
213
+ : detail.RejectStatus === '0' ? 'delivered' : detail.RejectStatus === '2' ? 'returned' : undefined)
214
+ : undefined;
215
+ if (delivered && !listed.length) {
216
+ const closed = stamp(detail.LastProcessDate);
217
+ const destination = cityCounty(detail.DeliveryCityName, detail.DeliveryCountyName);
218
+ if (closed)
219
+ scans.unshift({ local: closed, event: { local_time: closed, description: summary,
220
+ ...(destination ? { location: destination } : {}), ...(final ? { stage: final, stage_source: 'carrier_map' } : {}) } });
221
+ }
222
+ const events = scans.map(({ event }) => event);
223
+ // The summary settles a delivery, a return and a return that has not reached the sender; otherwise the newest movement does.
224
+ // A finished parcel whose summary settles neither keeps no stage rather than an earlier one.
225
+ const settled = final ?? (!delivered && summaryStage === 'exception' ? summaryStage : undefined);
226
+ const stage = settled ?? (delivered ? undefined : events.find(event => event.stage)?.stage);
227
+ if (!events.length && !stage)
228
+ throw new IndeterminateError(PROVIDER, 'Yurtiçi Kargo returned a shipment without dates or a known status');
229
+ const newest = scans.find(({ local }) => local)?.local ?? null;
230
+ const estimate = writtenDate(detail.EstimatedDeliveryDate)?.day;
231
+ const expected = stage && MOVING.has(stage) && estimate && (!newest || estimate >= newest.slice(0, 10)) ? estimate : null;
232
+ // A parcel the recipient collects from the delivery branch.
233
+ const branch = stage === 'delivered' && detail.IsAddressDelivery === false ? clean(detail.DeliveryUnitName, 80) : '';
234
+ const service = clean(detail.ProductName, 80);
235
+ const canonical = number.length === 16 ? printed(search.id) : '';
236
+ return {
237
+ status: stage ? languageStageStatus(stage) : 'unknown',
238
+ ...(stage ? { current_stage: stage, current_stage_source: 'carrier_map' } : {}),
239
+ last_status_text: settled ? summary : events[0]?.description ?? summary,
240
+ ...(listed.length ? {} : { summary_only: true }),
241
+ last_update: null,
242
+ last_update_local: newest,
243
+ expected_delivery: expected,
244
+ ...(service ? { service_name: service } : {}),
245
+ ...(branch ? { pickup_point: branch } : {}),
246
+ ...(canonical && canonical !== number ? { canonical_tracking_number: canonical } : {}),
247
+ events: events.slice(0, MAX_EVENTS),
248
+ ...(events.length > MAX_EVENTS ? { history_truncated: true } : {}),
249
+ };
250
+ }
@@ -0,0 +1,11 @@
1
+ import type { Stage } from '../../core/status/index.js';
2
+ import { type CarrierStatusMap } from '../../core/status/statusMap.js';
3
+ /** The stage a movement's wording gives it, its appended reason aside. */
4
+ export declare function yurticiScanStage(wording: string): Stage | undefined;
5
+ /** The stage a summary status gives the shipment, when it settles one. */
6
+ export declare function yurticiSummaryStage(wording: string): Stage | undefined;
7
+ /**
8
+ * Movements and summary statuses carry wording only. A summary status words
9
+ * the newest event of a parcel whose movements the tracker no longer lists.
10
+ */
11
+ export declare const statusMap: CarrierStatusMap;
@@ -0,0 +1,58 @@
1
+ import { normalizeStatusWording } from '../../core/status/statusMap.js';
2
+ /**
3
+ * The wording of the movements Yurtiçi's tracker lists for a parcel on its
4
+ * way, in English and in Turkish. Movements carry no code. The transport
5
+ * document is the waybill the departure branch issues when it takes the
6
+ * parcel; "Distribution exited" is the parcel out on a courier's round. A
7
+ * parcel turned back to its sender is sent to its departure branch for the
8
+ * return, which is not yet a return.
9
+ */
10
+ const SCANS = [
11
+ ['The transport document was issued.', 'accepted'],
12
+ ['Taşıma belgesi düzenlendi.', 'accepted'],
13
+ ['In the transfer process.', 'in_transit'],
14
+ ['Transfer Sürecinde', 'in_transit'],
15
+ ['Reached the delivery unit.', 'in_transit'],
16
+ ['Teslim birimine ulaştı.', 'in_transit'],
17
+ ['Redirected to New Delivery Unit.', 'in_transit'],
18
+ ['Yeni Teslim Birimine Yönlendirildi', 'in_transit'],
19
+ ['Distribution exited.', 'out_for_delivery'],
20
+ ['Dağıtıma çıkarıldı.', 'out_for_delivery'],
21
+ ['NOT DELIVERED / WAITING AT BRANCH', 'failed_attempt'],
22
+ ['Teslim edilemedi.', 'failed_attempt'],
23
+ ['Redirected to Exit Unit for Return Process.', 'exception'],
24
+ ['İade Süreci için Çıkış Birimine Yönlendirildi', 'exception'],
25
+ ];
26
+ /**
27
+ * The shipment's summary status. The English tracker translates some and
28
+ * keeps others in Turkish. A return that has not reached its sender is a
29
+ * problem, not yet a return.
30
+ */
31
+ const SUMMARIES = [
32
+ ['DELIVERED', 'delivered'],
33
+ ['TESLİM EDİLDİ', 'delivered'],
34
+ ['İADE EDİLDİ', 'returned'],
35
+ ['NOT RETURNED', 'exception'],
36
+ ['İADE EDİLEMEDİ', 'exception'],
37
+ ];
38
+ const keyed = (entries) => new Map(entries.map(([wording, stage]) => [normalizeStatusWording(wording), stage]));
39
+ const SCAN_KEYS = keyed(SCANS);
40
+ const SUMMARY_KEYS = keyed(SUMMARIES);
41
+ /** A movement's reason, which the adapter appends in brackets. */
42
+ const REASON = /\s*\([^()]*\)$/;
43
+ /** The stage a movement's wording gives it, its appended reason aside. */
44
+ export function yurticiScanStage(wording) {
45
+ return SCAN_KEYS.get(normalizeStatusWording(wording.replace(REASON, '')));
46
+ }
47
+ /** The stage a summary status gives the shipment, when it settles one. */
48
+ export function yurticiSummaryStage(wording) {
49
+ return SUMMARY_KEYS.get(normalizeStatusWording(wording));
50
+ }
51
+ /**
52
+ * Movements and summary statuses carry wording only. A summary status words
53
+ * the newest event of a parcel whose movements the tracker no longer lists.
54
+ */
55
+ export const statusMap = {
56
+ stage: (code, wording) => code === null ? yurticiScanStage(wording) ?? yurticiSummaryStage(wording) : undefined,
57
+ gaps: [],
58
+ };
@@ -1,4 +1,24 @@
1
1
  {
2
2
  "carrier": "yurtici-kargo",
3
- "entries": []
3
+ "entries": [
4
+ { "wording": "The transport document was issued.", "language": "en", "stage": "accepted", "confirmedBy": "live", "note": "Movement: the departure branch issues the waybill when it takes the parcel." },
5
+ { "wording": "Taşıma belgesi düzenlendi.", "language": "tr", "stage": "accepted", "confirmedBy": "live", "note": "Movement: the departure branch issues the waybill when it takes the parcel." },
6
+ { "wording": "In the transfer process.", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "Movement between units; it names no place." },
7
+ { "wording": "Transfer Sürecinde", "language": "tr", "stage": "in_transit", "confirmedBy": "live", "note": "Movement between units; it names no place." },
8
+ { "wording": "Reached the delivery unit.", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "Movement: also the departure branch when a return reaches it." },
9
+ { "wording": "Teslim birimine ulaştı.", "language": "tr", "stage": "in_transit", "confirmedBy": "live", "note": "Movement: also the departure branch when a return reaches it." },
10
+ { "wording": "Redirected to New Delivery Unit.", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "Movement after a failed delivery." },
11
+ { "wording": "Yeni Teslim Birimine Yönlendirildi", "language": "tr", "stage": "in_transit", "confirmedBy": "live", "note": "Movement after a failed delivery." },
12
+ { "wording": "Distribution exited.", "language": "en", "stage": "out_for_delivery", "confirmedBy": "live", "note": "Movement: out on a courier's round." },
13
+ { "wording": "Dağıtıma çıkarıldı.", "language": "tr", "stage": "out_for_delivery", "confirmedBy": "live", "note": "Movement: out on a courier's round." },
14
+ { "wording": "NOT DELIVERED / WAITING AT BRANCH", "language": "en", "stage": "failed_attempt", "confirmedBy": "live", "note": "Movement: its reason is appended in brackets." },
15
+ { "wording": "Teslim edilemedi.", "language": "tr", "stage": "failed_attempt", "confirmedBy": "live", "note": "Movement: its reason is appended in brackets." },
16
+ { "wording": "Redirected to Exit Unit for Return Process.", "language": "en", "stage": "exception", "confirmedBy": "live", "note": "Movement: the parcel heads back to its sender, not yet returned." },
17
+ { "wording": "İade Süreci için Çıkış Birimine Yönlendirildi", "language": "tr", "stage": "exception", "confirmedBy": "live", "note": "Movement: the parcel heads back to its sender, not yet returned." },
18
+ { "wording": "DELIVERED", "language": "en", "stage": "delivered", "confirmedBy": "live", "note": "Summary status of a delivered parcel, whose movements the tracker no longer lists." },
19
+ { "wording": "TESLİM EDİLDİ", "language": "tr", "stage": "delivered", "confirmedBy": "live", "note": "Summary status of a delivered parcel, whose movements the tracker no longer lists." },
20
+ { "wording": "İADE EDİLDİ", "language": "tr", "stage": "returned", "confirmedBy": "live", "note": "Summary status of a parcel returned to its sender; the English tracker keeps it in Turkish." },
21
+ { "wording": "NOT RETURNED", "language": "en", "stage": "exception", "confirmedBy": "live", "note": "Summary status of a parcel on its way back that has not reached its sender." },
22
+ { "wording": "İADE EDİLEMEDİ", "language": "tr", "stage": "exception", "confirmedBy": "live", "note": "Summary status of a parcel on its way back that has not reached its sender." }
23
+ ]
4
24
  }
@@ -52,6 +52,7 @@ import { statusMap as tnt } from '../../carriers/tnt/status.js';
52
52
  import { statusMap as ups } from '../../carriers/ups/status.js';
53
53
  import { statusMap as venipak } from '../../carriers/venipak/status.js';
54
54
  import { statusMap as yunexpress } from '../../carriers/yunexpress/status.js';
55
+ import { statusMap as yurticiKargo } from '../../carriers/yurtici-kargo/status.js';
55
56
  import { statusMap as universal } from '../../providers/status.js';
56
57
  import { normalizeStatusWording } from '../status/statusMap.js';
57
58
  import { CARRIER_DEFINITIONS } from './definitions.js';
@@ -61,7 +62,7 @@ export const STATUS_MAPS = {
61
62
  'dhl-express': dhlExpress, dpd, 'dpd-de': dpdDe, 'dpd-fr': dpdFr, 'dpd-ie': dpdIe, 'dpd-pl': dpdPl, econt, elta, emile, 'fan-courier': fanCourier,
62
63
  foxpost, 'geniki-taxydromiki': geniki, 'gls-it': glsIt, 'gofo-fr': gofoFr, 'gofo-it': gofoIt, 'j-and-t': jAndT, 'j-and-t-cargo': jntCargo,
63
64
  'la-poste': laPoste, 'old-dominion': oldDominion, omgo, omniva, 'orlen-paczka': orlenPaczka, postlogistics, ppl, sagawa, sameday, speedx, speedy,
64
- sto, 'swiss-post': swissPost, 'thailand-post': thailandPost, tnt, ups, venipak, yunexpress,
65
+ sto, 'swiss-post': swissPost, 'thailand-post': thailandPost, tnt, ups, venipak, yunexpress, 'yurtici-kargo': yurticiKargo,
65
66
  };
66
67
  /** The carrier the app files the universal providers' scans under. */
67
68
  const UNIVERSAL_CARRIER = 'unknown';
@@ -8285,7 +8285,9 @@
8285
8285
  "timezone": "Europe/Istanbul",
8286
8286
  "tracking": {
8287
8287
  "mode": "automatic",
8288
- "adapter": "universal"
8288
+ "adapter": "yurtici-kargo",
8289
+ "recognitionRank": 66,
8290
+ "localClocks": true
8289
8291
  },
8290
8292
  "canaryUrl": "https://www.yurticikargo.com/tr/online-servisler/gonderi-sorgula",
8291
8293
  "trackingUrlTemplate": "https://www.yurticikargo.com/tr/online-servisler/gonderi-sorgula?code={trackingNumber}",
@@ -5550,7 +5550,9 @@ export declare const CARRIER_CATALOG: {
5550
5550
  readonly timezone: "Europe/Istanbul";
5551
5551
  readonly tracking: {
5552
5552
  readonly mode: "automatic";
5553
- readonly adapter: "universal";
5553
+ readonly adapter: "yurtici-kargo";
5554
+ readonly recognitionRank: 66;
5555
+ readonly localClocks: true;
5554
5556
  };
5555
5557
  readonly canaryUrl: "https://www.yurticikargo.com/tr/online-servisler/gonderi-sorgula";
5556
5558
  readonly trackingUrlTemplate: "https://www.yurticikargo.com/tr/online-servisler/gonderi-sorgula?code={trackingNumber}";
@@ -8286,7 +8286,9 @@ export const CARRIER_CATALOG = {
8286
8286
  "timezone": "Europe/Istanbul",
8287
8287
  "tracking": {
8288
8288
  "mode": "automatic",
8289
- "adapter": "universal"
8289
+ "adapter": "yurtici-kargo",
8290
+ "recognitionRank": 66,
8291
+ "localClocks": true
8290
8292
  },
8291
8293
  "canaryUrl": "https://www.yurticikargo.com/tr/online-servisler/gonderi-sorgula",
8292
8294
  "trackingUrlTemplate": "https://www.yurticikargo.com/tr/online-servisler/gonderi-sorgula?code={trackingNumber}",
@@ -69,4 +69,5 @@ export const CARRIER_RECOGNITION_RANKS = {
69
69
  "ukrposhta": 5,
70
70
  "uniuni": 33,
71
71
  "venipak": 63,
72
+ "yurtici-kargo": 66,
72
73
  };
@@ -134,6 +134,7 @@ import { adapter as yanwen } from '../carriers/yanwen/adapter.js';
134
134
  import { adapter as yto } from '../carriers/yto/adapter.js';
135
135
  import { adapter as yunda } from '../carriers/yunda/adapter.js';
136
136
  import { adapter as yunexpress } from '../carriers/yunexpress/adapter.js';
137
+ import { adapter as yurticiKargo } from '../carriers/yurtici-kargo/adapter.js';
137
138
  export const REGISTRY = {
138
139
  factories: {
139
140
  "acs-courier": acsCourier,
@@ -271,6 +272,7 @@ export const REGISTRY = {
271
272
  "yto": yto,
272
273
  "yunda": yunda,
273
274
  "yunexpress": yunexpress,
275
+ "yurtici-kargo": yurticiKargo,
274
276
  },
275
277
  carriers: {
276
278
  "99minutos": "universal",
@@ -477,7 +479,7 @@ export const REGISTRY = {
477
479
  "yto": "yto",
478
480
  "yunda": "yunda",
479
481
  "yunexpress": "yunexpress",
480
- "yurtici-kargo": "universal",
482
+ "yurtici-kargo": "yurtici-kargo",
481
483
  "zeleris": "universal",
482
484
  "zto": "universal",
483
485
  },
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.78.0"
5
+ "version": "0.79.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.78.0-main.939",
3
+ "version": "0.79.0-main.941",
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",