universal-parcel-scraper 0.74.0-main.925 → 0.75.0-main.929

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 **129 dedicated adapters** and **5 universal fallbacks**
16
+ **3,500+ carriers** through **130 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,613 such statuses, each filed under one stage.
158
+ The carrier folders record 3,675 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
@@ -7462,7 +7462,7 @@
7462
7462
  "timezone": "Europe/Prague",
7463
7463
  "tracking": {
7464
7464
  "mode": "automatic",
7465
- "adapter": "universal"
7465
+ "adapter": "ppl"
7466
7466
  },
7467
7467
  "canaryUrl": "https://www.ppl.cz/vyhledat-zasilku",
7468
7468
  "trackingUrlTemplate": "https://www.ppl.cz/vyhledat-zasilku?shipmentId={trackingNumber}",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "catalog": 207,
3
- "activeAdapters": 129,
3
+ "activeAdapters": 130,
4
4
  "countries": 66,
5
5
  "referenceCarriers": 100,
6
6
  "comparison": {
@@ -5917,7 +5917,7 @@ var ye = {
5917
5917
  timezone: "Europe/Prague",
5918
5918
  tracking: {
5919
5919
  mode: "automatic",
5920
- adapter: "universal"
5920
+ adapter: "ppl"
5921
5921
  },
5922
5922
  canaryUrl: "https://www.ppl.cz/vyhledat-zasilku",
5923
5923
  trackingUrlTemplate: "https://www.ppl.cz/vyhledat-zasilku?shipmentId={trackingNumber}",
@@ -9,6 +9,16 @@ const LABELS = {
9
9
  'DEPOSITADO EN PUNTO MRW': { status: 'in_transit', stage: 'ready_for_pickup' },
10
10
  'DEPOSITADO EN POINTCORNER DESTINO': { status: 'in_transit', stage: 'ready_for_pickup' },
11
11
  'ENVIO ENTREGADO': { status: 'delivered', stage: 'delivered' },
12
+ // www.mrw.pt renders the same history in Portuguese.
13
+ 'PENDENTE DE RECOLHER': { status: 'pending', stage: 'registered' },
14
+ 'ENVIO RECOLHIDO NA ORIGEM': { status: 'in_transit', stage: 'accepted' },
15
+ 'EM TRANSITO': { status: 'in_transit', stage: 'in_transit' },
16
+ 'ENVIO EM DISTRIBUICAO': { status: 'out_for_delivery', stage: 'out_for_delivery' },
17
+ 'DESTINATARIO AUSENTE OU FECHADO': { status: 'exception', stage: 'failed_attempt' },
18
+ 'ACORDADA PROXIMA ENTREGA': { status: 'in_transit', stage: 'in_transit' },
19
+ 'DEPOSITADO EM MRW POINT': { status: 'in_transit', stage: 'ready_for_pickup' },
20
+ 'DEPOSITADO NO POINTCORNER DESTINO': { status: 'in_transit', stage: 'ready_for_pickup' },
21
+ 'ENVIO ENTREGUE': { status: 'delivered', stage: 'delivered' },
12
22
  };
13
23
  export function classifyMrwStatus(value) {
14
24
  const key = value.normalize('NFD').replace(/\p{Diacritic}/gu, '').toUpperCase().replace(/\s+/g, ' ').trim();
@@ -9,6 +9,15 @@
9
9
  { "wording": "Entrega concertada", "language": "es", "stage": "in_transit", "confirmedBy": "live" },
10
10
  { "wording": "Depositado en punto MRW", "language": "es", "stage": "ready_for_pickup", "confirmedBy": "live" },
11
11
  { "wording": "Depositado en PointCorner destino", "language": "es", "stage": "ready_for_pickup", "confirmedBy": "live" },
12
- { "wording": "Envío entregado", "language": "es", "stage": "delivered", "confirmedBy": "live" }
12
+ { "wording": "Envío entregado", "language": "es", "stage": "delivered", "confirmedBy": "live" },
13
+ { "wording": "Pendente de recolher", "language": "pt", "stage": "registered", "confirmedBy": "live" },
14
+ { "wording": "Envio recolhido na origem", "language": "pt", "stage": "accepted", "confirmedBy": "live" },
15
+ { "wording": "Em tránsito", "language": "pt", "stage": "in_transit", "confirmedBy": "live" },
16
+ { "wording": "Envio em distribuição", "language": "pt", "stage": "out_for_delivery", "confirmedBy": "live" },
17
+ { "wording": "Destinatário ausente ou fechado", "language": "pt", "stage": "failed_attempt", "confirmedBy": "live" },
18
+ { "wording": "Acordada próxima entrega", "language": "pt", "stage": "in_transit", "confirmedBy": "live" },
19
+ { "wording": "Depositado em MRW Point", "language": "pt", "stage": "ready_for_pickup", "confirmedBy": "live" },
20
+ { "wording": "Depositado no PointCorner destino", "language": "pt", "stage": "ready_for_pickup", "confirmedBy": "live" },
21
+ { "wording": "Envio entregue", "language": "pt", "stage": "delivered", "confirmedBy": "live" }
13
22
  ]
14
23
  }
@@ -0,0 +1,15 @@
1
+ import type { AdapterFactory, 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 class PplTracker {
5
+ private readonly options;
6
+ constructor(options?: {
7
+ fetcher?: typeof fetch;
8
+ recorder?: StepRecorder;
9
+ userAgent?: string;
10
+ /** The tracking page's key. Omit for the included one; null or blank turns the lookup off. */
11
+ key?: string | null;
12
+ });
13
+ fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
14
+ }
15
+ export declare const adapter: AdapterFactory;
@@ -0,0 +1,49 @@
1
+ import { ChallengeError, SchemaError, TransportError } from '../../core/errors/index.js';
2
+ import { runSteps } from '../../core/runner/index.js';
3
+ import { NOOP_RECORDER } from '../../core/telemetry/index.js';
4
+ import { decodeText, fetchBounded, userAgentOf } from '../../core/transport/index.js';
5
+ import { normalizePplNumber, parsePpl, parsePplRejection, PPL } from './parser.js';
6
+ const API = 'https://api.dhl.com/ecs/ppl/webapi/TrackAndTrace';
7
+ /** The key PPL's tracking page gives every visitor, included with the maintainer's approval. */
8
+ const KEY = '7HH634Q79Zpge4xEGeFAHXAnUMRxv0XQ';
9
+ function json(bytes) {
10
+ const text = decodeText(bytes).replace(/^\uFEFF/, '');
11
+ if (/^\s*</.test(text))
12
+ throw new ChallengeError(PPL, 'PPL answered with a web page');
13
+ try {
14
+ return JSON.parse(text);
15
+ }
16
+ catch (cause) {
17
+ throw new SchemaError(PPL, 'PPL returned invalid tracking JSON', { cause });
18
+ }
19
+ }
20
+ export class PplTracker {
21
+ options;
22
+ constructor(options = {}) {
23
+ this.options = options;
24
+ }
25
+ async fetch(raw, context = {}) {
26
+ const number = normalizePplNumber(raw);
27
+ const key = (this.options.key === undefined ? KEY : this.options.key ?? '').trim();
28
+ if (!key)
29
+ throw new ChallengeError(PPL, 'PPL tracking key is disabled');
30
+ return runSteps({ carrier: 'ppl', budgetMs: context.budgetMs ?? 15_000, signal: context.signal,
31
+ recorder: this.options.recorder ?? NOOP_RECORDER }, [{ id: 'direct', run: async ({ signal, remainingMs }) => {
32
+ // The tracking page's own search: a POST with an empty body. English wording.
33
+ const { response, bytes } = await fetchBounded(`${API}/${number}`, { method: 'POST', signal, headers: {
34
+ Accept: 'application/json', 'Accept-Language': 'en-US', 'Content-Type': 'application/json', 'dhl-api-key': key,
35
+ 'User-Agent': userAgentOf(this.options.userAgent),
36
+ }, body: '{}' }, { provider: PPL, timeoutMs: remainingMs, maxBytes: 1_000_000, fetcher: this.options.fetcher,
37
+ allowHttpStatuses: [400, 404, 410] });
38
+ if (response.status === 404 || response.status === 410)
39
+ throw new TransportError(PPL, 'PPL tracking service is unavailable');
40
+ const payload = json(bytes);
41
+ return response.status === 400 ? parsePplRejection(payload, number) : parsePpl(payload, number);
42
+ } }]);
43
+ }
44
+ }
45
+ export const adapter = environment => {
46
+ const tracker = new PplTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent,
47
+ key: environment.env.PPL_TRACKING_KEY });
48
+ return { id: 'ppl', recordsSteps: true, steps: ['direct'], track: (input, context) => tracker.fetch(input.number, context) };
49
+ };
@@ -16,18 +16,60 @@
16
16
  "timezone": "Europe/Prague",
17
17
  "tracking": {
18
18
  "mode": "automatic",
19
- "adapter": "universal",
20
- "steps": []
19
+ "adapter": "ppl",
20
+ "steps": [
21
+ "direct"
22
+ ]
21
23
  },
22
- "capabilities": [],
24
+ "capabilities": [
25
+ "history",
26
+ "provider_code",
27
+ "eta",
28
+ "weight",
29
+ "delivered_at",
30
+ "pickup_point"
31
+ ],
23
32
  "portal": {
24
33
  "url": "https://www.ppl.cz/vyhledat-zasilku?shipmentId={trackingNumber}",
25
34
  "searchUrl": "https://www.ppl.cz/vyhledat-zasilku",
26
35
  "canaryUrl": "https://www.ppl.cz/vyhledat-zasilku",
27
- "shows": [],
28
- "scraped": [],
29
- "discarded": [],
30
- "unavailable": []
36
+ "shows": [
37
+ "status",
38
+ "history",
39
+ "eta",
40
+ "eta_window",
41
+ "weight",
42
+ "sender_name",
43
+ "customer_reference",
44
+ "pickup_point",
45
+ "pickup_point_address",
46
+ "delivery_partner",
47
+ "delivery_partner_reference",
48
+ "payment_details",
49
+ "proof_of_delivery"
50
+ ],
51
+ "scraped": [
52
+ "status",
53
+ "history",
54
+ "provider_code",
55
+ "eta",
56
+ "weight",
57
+ "delivered_at",
58
+ "pickup_point"
59
+ ],
60
+ "discarded": [
61
+ "eta_window",
62
+ "sender_name",
63
+ "customer_reference",
64
+ "pickup_point_address",
65
+ "delivery_partner",
66
+ "delivery_partner_reference",
67
+ "payment_details",
68
+ "proof_of_delivery"
69
+ ],
70
+ "unavailable": [
71
+ "location"
72
+ ]
31
73
  },
32
74
  "links": [
33
75
  {
@@ -0,0 +1,10 @@
1
+ import type { CarrierResult } from '../../core/result/index.js';
2
+ export declare const PPL = "PPL";
3
+ export declare function normalizePplNumber(raw: string): string;
4
+ /**
5
+ * The tracking service's HTTP 400 problem reply. Only its shipment-not-found
6
+ * detail, for the requested number when it names one, means PPL does not know
7
+ * the parcel; any other rejection proves nothing.
8
+ */
9
+ export declare function parsePplRejection(payload: unknown, rawNumber: string): never;
10
+ export declare function parsePpl(payload: unknown, rawNumber: string): CarrierResult;
@@ -0,0 +1,109 @@
1
+ import { normalizeTrackingNumber } from '../../core/detection/normalize.js';
2
+ import { IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
3
+ import { languageStageStatus } from '../../core/status/index.js';
4
+ import { explicitOffsetTime } from '../../core/time/index.js';
5
+ import { clean } from '../../core/transport/text.js';
6
+ import { isRecord } from '../../core/types.js';
7
+ import { pplStage } from './status.js';
8
+ export const PPL = 'PPL';
9
+ const NOT_FOUND = 'service.TrackAndTrace.ShipmentNotFound';
10
+ const MAX_SCANS = 500;
11
+ const SETTLED = new Set(['delivered', 'returned', 'ready_for_pickup']);
12
+ export function normalizePplNumber(raw) {
13
+ const number = normalizeTrackingNumber(raw);
14
+ if (!/^\d{11}$/.test(number))
15
+ throw new InvalidInputError(PPL, 'PPL requires an eleven-digit parcel number');
16
+ return number;
17
+ }
18
+ /**
19
+ * The tracking service's HTTP 400 problem reply. Only its shipment-not-found
20
+ * detail, for the requested number when it names one, means PPL does not know
21
+ * the parcel; any other rejection proves nothing.
22
+ */
23
+ export function parsePplRejection(payload, rawNumber) {
24
+ const number = normalizePplNumber(rawNumber);
25
+ if (!isRecord(payload))
26
+ throw new SchemaError(PPL, 'PPL returned an invalid error reply');
27
+ if (payload.instance != null && payload.instance !== `/TrackAndTrace/${number}`) {
28
+ throw new SchemaError(PPL, 'PPL answered for another parcel');
29
+ }
30
+ if (payload.detail === NOT_FOUND)
31
+ throw new NotFoundError(PPL);
32
+ throw new IndeterminateError(PPL, 'PPL rejected the tracking request');
33
+ }
34
+ function scan(row) {
35
+ if (!isRecord(row))
36
+ throw new SchemaError(PPL, 'PPL returned an invalid scan');
37
+ const code = clean(row.code, 100);
38
+ const description = clean(row.eventText, 500);
39
+ if (!code || !description)
40
+ throw new SchemaError(PPL, 'PPL returned a scan without its code or wording');
41
+ // Scan clocks carry an explicit offset; one without stays unresolved text.
42
+ const time = explicitOffsetTime(row.eventDate);
43
+ const text = time ? '' : clean(row.eventDate, 64);
44
+ const stage = pplStage(code);
45
+ return { timestamp: time?.timestamp ?? null, ...(stage ? { stage } : {}), event: {
46
+ description, provider_code: code, ...(time ? { time: time.iso } : text ? { provider_time_text: text } : {}),
47
+ ...(stage ? { stage, stage_source: 'carrier_map' } : {}),
48
+ } };
49
+ }
50
+ /** The expected delivery day, only while the portal itself shows one and the parcel is still on its way. */
51
+ function estimatedDay(payload, stage, latest) {
52
+ if (payload.showDeliveryDate !== true || !stage || SETTLED.has(stage))
53
+ return null;
54
+ const estimate = explicitOffsetTime(payload.expectedDeliveryDate);
55
+ if (!estimate)
56
+ return null;
57
+ const day = estimate.iso.slice(0, 10);
58
+ return typeof latest.time === 'string' && day < latest.time.slice(0, 10) ? null : day;
59
+ }
60
+ export function parsePpl(payload, rawNumber) {
61
+ const number = normalizePplNumber(rawNumber);
62
+ if (!isRecord(payload))
63
+ throw new SchemaError(PPL);
64
+ if (typeof payload.shipmentId !== 'string' || payload.shipmentId.trim() !== number) {
65
+ throw new SchemaError(PPL, 'PPL returned a different parcel');
66
+ }
67
+ const rows = payload.events;
68
+ if (!Array.isArray(rows) || rows.length > MAX_SCANS)
69
+ throw new SchemaError(PPL, 'PPL returned an invalid scan list');
70
+ if (!rows.length)
71
+ throw new IndeterminateError(PPL, 'PPL returned no parcel history');
72
+ // The service lists scans oldest first. Newest first, by instant when every
73
+ // scan has one; otherwise in the service's order.
74
+ const parsed = rows.map(scan).reverse();
75
+ if (parsed.every(item => item.timestamp !== null))
76
+ parsed.sort((left, right) => right.timestamp - left.timestamp);
77
+ const seen = new Set();
78
+ const scans = parsed.filter(item => {
79
+ const key = JSON.stringify(item.event);
80
+ return seen.has(key) ? false : (seen.add(key), true);
81
+ });
82
+ const events = scans.map(item => item.event);
83
+ const latest = events[0];
84
+ // Payment steps and codes the map does not know leave the last milestone in place.
85
+ const staged = scans.filter(item => item.stage);
86
+ const newest = staged[0]?.stage;
87
+ // From a return scan on, the parcel travels back and cannot be delivered
88
+ // again: a later delivery hands it to its sender.
89
+ const stage = newest && staged.some(item => item.stage === 'returned') ? 'returned' : newest;
90
+ const delivery = stage === 'delivered' ? staged.findIndex(item => item.stage === 'delivered') : -1;
91
+ // A ParcelShop or ParcelBox holds the parcel, or held it until the delivery
92
+ // that followed: the recipient collected it there. Its name only.
93
+ const held = stage === 'ready_for_pickup' || (delivery >= 0 && staged[delivery + 1]?.stage === 'ready_for_pickup');
94
+ const point = held && isRecord(payload.accessPoint) ? clean(payload.accessPoint.name, 160) : '';
95
+ const deliveredAt = delivery >= 0 ? staged[delivery].event.time : undefined;
96
+ const weight = payload.weight;
97
+ return {
98
+ status: stage ? languageStageStatus(stage) : 'unknown',
99
+ ...(stage ? { current_stage: stage, current_stage_source: 'carrier_map' } : {}),
100
+ last_status_text: latest.description ?? null,
101
+ last_update: latest.time ?? null,
102
+ expected_delivery: estimatedDay(payload, stage, latest),
103
+ ...(typeof deliveredAt === 'string' ? { delivered_at: deliveredAt } : {}),
104
+ ...(typeof weight === 'number' && Number.isFinite(weight) && weight > 0 && weight < 10_000 ? { weight_kg: weight } : {}),
105
+ ...(point ? { pickup_point: point } : {}),
106
+ ...(events.length > 100 ? { history_truncated: true } : {}),
107
+ events: events.slice(0, 100),
108
+ };
109
+ }
@@ -0,0 +1,5 @@
1
+ import type { Stage } from '../../core/status/index.js';
2
+ import type { CarrierStatusMap } from '../../core/status/statusMap.js';
3
+ /** The stage of one PPL event code, or undefined for a payment step or a code the map does not know. */
4
+ export declare function pplStage(code: string): Stage | undefined;
5
+ export declare const statusMap: CarrierStatusMap;
@@ -0,0 +1,14 @@
1
+ import statuses from './statuses.json' with { type: 'json' };
2
+ // Every scan of PPL's tracking service carries an event code, and the code
3
+ // alone decides the stage. The wording follows the requested language and
4
+ // several codes share one wording, so it is never read. Entries without a
5
+ // stage are cash-on-delivery payment steps: they move nothing.
6
+ const CODES = new Map(statuses.entries.flatMap(entry => entry.stage ? [[entry.code, entry.stage]] : []));
7
+ /** The stage of one PPL event code, or undefined for a payment step or a code the map does not know. */
8
+ export function pplStage(code) {
9
+ return CODES.get(code);
10
+ }
11
+ export const statusMap = {
12
+ stage: code => code ? pplStage(code) : undefined,
13
+ gaps: statuses.entries.flatMap(entry => entry.stage ? [] : [{ code: entry.code, note: entry.note }]),
14
+ };
@@ -1,4 +1,58 @@
1
1
  {
2
2
  "carrier": "ppl",
3
- "entries": []
3
+ "entries": [
4
+ {"code": "WaitingForShipment", "wording": "We are waiting for the shipment to be accepted for transport", "language": "en", "stage": "registered", "confirmedBy": "live", "firstSeen": "2026-10-11", "note": "The sender has created the label; PPL does not have the parcel yet."},
5
+ {"code": "WaitingForShipment.Foreign", "wording": "We are waiting for the shipment to be accepted for transport", "language": "en", "stage": "registered", "confirmedBy": "bundle", "note": "The portal's longer text adds that the parcel may be on its way from abroad."},
6
+ {"code": "WaitingForShipment.ExpiringLabel", "stage": "registered", "confirmedBy": "bundle", "note": "The portal shows the date the unused label expires with it. Its wording was not seen."},
7
+ {"code": "ShipmentInTransport.TakeOverFromSender", "wording": "We have received the shipment from the sender", "language": "en", "stage": "accepted", "confirmedBy": "live", "firstSeen": "2026-10-11"},
8
+ {"code": "ShipmentInTransport.TakeOverFromSender.WithException", "wording": "We have received the shipment from the sender", "language": "en", "stage": "accepted", "confirmedBy": "bundle", "note": "An acceptance with a remark; the portal words it as a plain acceptance."},
9
+ {"code": "PhysicalScan", "wording": "We have received the shipment from the sender", "language": "en", "stage": "accepted", "confirmedBy": "bundle", "note": "The portal words it as the acceptance from the sender."},
10
+ {"code": "ShipmentInTransport.TakeOverFromForeignPartner", "wording": "We have received the shipment from the foreign partner", "language": "en", "stage": "in_transit", "confirmedBy": "bundle", "note": "PPL takes the parcel over from a partner abroad; the sender handed it over earlier."},
11
+ {"code": "ShipmentInForeignCountry", "wording": "The shipment is being transported to the destination", "language": "en", "stage": "in_transit", "confirmedBy": "bundle"},
12
+ {"code": "ShipmentInTransport", "wording": "The shipment is in transit", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-10-11"},
13
+ {"code": "PreparingForDelivery", "wording": "The shipment is in transit", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-10-11", "note": "Worded like ShipmentInTransport; the portal's longer text says the parcel is being prepared for delivery."},
14
+ {"code": "OtherDepot", "wording": "The shipment is in transit", "language": "en", "stage": "in_transit", "confirmedBy": "bundle"},
15
+ {"code": "HandedOverToForeignPartner", "wording": "The shipment has been handed over to a foreign partner for delivery", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-10-11", "note": "A partner abroad delivers the parcel, and PPL relays the partner's milestones after it."},
16
+ {"code": "HandedOverToDHLFreight", "wording": "The shipment was handed over to DHL Freight for delivery", "language": "en", "stage": "in_transit", "confirmedBy": "bundle", "note": "The parcel exceeds parcel limits and DHL Freight delivers it."},
17
+ {"code": "InputDepot.Foreign", "wording": "The shipment is at the entry sorting hub abroad", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-10-11"},
18
+ {"code": "DeliveryToDepot.Foreign", "wording": "The shipment is on its way to the delivery depot at the destination", "language": "en", "stage": "in_transit", "confirmedBy": "bundle"},
19
+ {"code": "DeliveryDepot.Foreign", "wording": "The shipment is at the delivery depot at the destination", "language": "en", "stage": "in_transit", "confirmedBy": "bundle"},
20
+ {"code": "ExitDepot.Foreign", "wording": "The shipment is at the delivery depot at the destination", "language": "en", "stage": "in_transit", "confirmedBy": "bundle"},
21
+ {"code": "Undelivered.DidNotManage", "wording": "The shipment was not delivered due to lack of time", "language": "en", "stage": "in_transit", "confirmedBy": "bundle", "note": "A delay on PPL's side, such as the driver running out of time: nobody tried the recipient, and a later round delivers the parcel."},
22
+ {"code": "Undelivered.WasNotReached", "wording": "The shipment was not delivered due to lack of time", "language": "en", "stage": "in_transit", "confirmedBy": "bundle", "note": "A delay on PPL's side, such as the driver running out of time: nobody tried the recipient, and a later round delivers the parcel. Despite its code, the portal words it as a lack of time."},
23
+ {"code": "Undelivered.LackOfTime", "wording": "The shipment was not delivered due to the late arrival of shipments at the delivery depot", "language": "en", "stage": "in_transit", "confirmedBy": "bundle", "note": "A delay on PPL's side, such as the driver running out of time: nobody tried the recipient, and a later round delivers the parcel."},
24
+ {"code": "Undelivered.CarFault", "wording": "The shipment was not delivered due to a car breakdown", "language": "en", "stage": "in_transit", "confirmedBy": "bundle", "note": "A delay on PPL's side, such as the driver running out of time: nobody tried the recipient, and a later round delivers the parcel."},
25
+ {"code": "Undelivered.BadWeather", "wording": "The shipment was not delivered due to an adverse route situation beyond our control", "language": "en", "stage": "in_transit", "confirmedBy": "bundle", "note": "A delay on PPL's side, such as the driver running out of time: nobody tried the recipient, and a later round delivers the parcel."},
26
+ {"code": "LoadingForDelivery", "wording": "The shipment will be delivered today", "language": "en", "stage": "out_for_delivery", "confirmedBy": "live", "firstSeen": "2026-10-11", "note": "The parcel is loaded for the day's round. The wording reads as a forecast, but the code is the loading scan."},
27
+ {"code": "LoadingForDelivery.TimeWindow", "wording": "The shipment will be delivered today", "language": "en", "stage": "out_for_delivery", "confirmedBy": "bundle", "note": "The portal adds the expected time window."},
28
+ {"code": "Delivery", "wording": "The shipment is being delivered to the recipient at the destination", "language": "en", "stage": "out_for_delivery", "confirmedBy": "live", "firstSeen": "2026-10-11", "note": "A partner abroad is delivering the parcel."},
29
+ {"code": "NotDelivered.WasNotReached.Announced", "wording": "The shipment was not delivered due to failure to reach the recipient", "language": "en", "stage": "failed_attempt", "confirmedBy": "bundle"},
30
+ {"code": "NotDelivered.WasNotReached.Unannounced", "wording": "The shipment was not delivered due to failure to reach the recipient", "language": "en", "stage": "failed_attempt", "confirmedBy": "bundle"},
31
+ {"code": "NotDelivered.Holiday", "wording": "The shipment was not delivered due to failure to reach the recipient", "language": "en", "stage": "failed_attempt", "confirmedBy": "bundle", "note": "The portal words it as the recipient not being reached."},
32
+ {"code": "NotDelivered.Close", "wording": "The shipment was not delivered due to failure to reach the recipient", "language": "en", "stage": "failed_attempt", "confirmedBy": "bundle", "note": "The portal words it as the recipient not being reached."},
33
+ {"code": "NotDelivered.OtherDate", "wording": "We will deliver the shipment on another date", "language": "en", "stage": "failed_attempt", "confirmedBy": "bundle", "note": "Delivery moves to another day after an attempt."},
34
+ {"code": "NotDelivered.NewAddress", "wording": "We will deliver the shipment to a new address", "language": "en", "stage": "failed_attempt", "confirmedBy": "bundle", "note": "Delivery moves to a new address after an attempt."},
35
+ {"code": "NotDelivered.UnpreparedCash", "wording": "The shipment was not delivered due to unprepared cash", "language": "en", "stage": "failed_attempt", "confirmedBy": "bundle"},
36
+ {"code": "NotDelivered.Documents", "wording": "The shipment was not delivered due to unprepared or incorrect documents", "language": "en", "stage": "failed_attempt", "confirmedBy": "bundle"},
37
+ {"code": "NotDelivered.AgeLimit", "wording": "The shipment was not delivered due to the recipient’s age", "language": "en", "stage": "failed_attempt", "confirmedBy": "bundle"},
38
+ {"code": "NotDelivered.IncompleteShipment", "wording": "The shipment was not delivered due to incompleteness", "language": "en", "stage": "exception", "confirmedBy": "bundle"},
39
+ {"code": "NotDelivered.IncompleteData", "wording": "The shipment was not delivered due to incomplete or incorrect delivery details", "language": "en", "stage": "exception", "confirmedBy": "bundle", "note": "The portal asks for the delivery details to be completed."},
40
+ {"code": "NotDelivered.Damage", "wording": "The shipment was not delivered due to damage", "language": "en", "stage": "exception", "confirmedBy": "bundle", "note": "The portal says the sender is told about the damage."},
41
+ {"code": "NotDelivered.BackToSender", "wording": "The recipient refused to accept the shipment due to the fact that the goods were not ordered", "language": "en", "stage": "returned", "confirmedBy": "bundle", "note": "A refusal: the portal says the parcel goes back to the sender."},
42
+ {"code": "BackToSender.Refused", "wording": "The recipient refused to accept the shipment", "language": "en", "stage": "returned", "confirmedBy": "bundle", "note": "A refusal: the portal says the parcel goes back to the sender."},
43
+ {"code": "BackToSender.Refused.NoReason", "wording": "The recipient refused to accept the shipment without giving a reason", "language": "en", "stage": "returned", "confirmedBy": "bundle", "note": "A refusal: the portal says the parcel goes back to the sender."},
44
+ {"code": "BackToSender", "wording": "The shipment will be returned to the sender", "language": "en", "stage": "returned", "confirmedBy": "bundle", "note": "The portal says the parcel is on its way back and cannot be delivered again."},
45
+ {"code": "BackToSender.NotPickedUp", "wording": "The shipment has not been collected by the recipient and will be returned to the sender", "language": "en", "stage": "returned", "confirmedBy": "bundle", "note": "Nobody collected the parcel from the pickup point in time."},
46
+ {"code": "BackToSender.StorageTimeExceeded", "wording": "The shipment has not been collected by the recipient and will be returned to the sender", "language": "en", "stage": "returned", "confirmedBy": "bundle", "note": "Nobody collected the parcel from the pickup point in time."},
47
+ {"code": "Delivered.BackToSender", "wording": "We have returned the shipment to the sender", "language": "en", "stage": "returned", "confirmedBy": "bundle", "note": "The sender has the parcel back."},
48
+ {"code": "Delivered.Parcelshop", "wording": "We have delivered the shipment to the pick-up point", "language": "en", "stage": "ready_for_pickup", "confirmedBy": "live", "firstSeen": "2026-10-11", "note": "A PPL ParcelShop or ParcelBox holds the parcel for the recipient; a later Delivered is the collection."},
49
+ {"code": "Delivered.ServicePoint", "wording": "We have delivered the shipment to the pick-up point abroad", "language": "en", "stage": "ready_for_pickup", "confirmedBy": "bundle", "note": "A pickup point abroad holds the parcel."},
50
+ {"code": "Delivered", "wording": "We have delivered the shipment to the recipient", "language": "en", "stage": "delivered", "confirmedBy": "live", "firstSeen": "2026-10-11"},
51
+ {"code": "Delivered.Uncomplete", "wording": "We have delivered the shipment to the recipient", "language": "en", "stage": "delivered", "confirmedBy": "bundle", "note": "Delivered incomplete; the portal words it as a delivery."},
52
+ {"code": "Delivered.Exception", "wording": "We have delivered the shipment to the recipient", "language": "en", "stage": "delivered", "confirmedBy": "bundle", "note": "Delivered with a remark; the portal words it as a delivery."},
53
+ {"code": "Delivered.Damaged", "wording": "We have delivered the shipment to the recipient", "language": "en", "stage": "delivered", "confirmedBy": "bundle", "note": "Delivered damaged; the portal words it as a delivery."},
54
+ {"code": "Cod.AdviceSent", "wording": "We have sent a payment order with a cash on delivery amount", "language": "en", "confirmedBy": "bundle", "note": "A cash-on-delivery payment step, not a movement: the parcel keeps the stage it had."},
55
+ {"code": "Cod.Completed", "wording": "The cash on delivery amount has been credited to the sender’s account", "language": "en", "confirmedBy": "bundle", "note": "A cash-on-delivery payment step, not a movement: the parcel keeps the stage it had."},
56
+ {"code": "Deleted", "wording": "The shipment has been deleted from the PPL system. Please contact the PPL customer service for more information", "language": "en", "stage": "exception", "confirmedBy": "bundle", "note": "Removed from PPL's system; the portal refers the recipient to customer service."}
57
+ ]
4
58
  }
@@ -36,6 +36,7 @@ import { statusMap as oldDominion } from '../../carriers/old-dominion/status.js'
36
36
  import { statusMap as omgo } from '../../carriers/omgo/status.js';
37
37
  import { statusMap as orlenPaczka } from '../../carriers/orlen-paczka/status.js';
38
38
  import { statusMap as postlogistics } from '../../carriers/postlogistics/status.js';
39
+ import { statusMap as ppl } from '../../carriers/ppl/status.js';
39
40
  import { statusMap as sagawa } from '../../carriers/sagawa/status.js';
40
41
  import { statusMap as sameday } from '../../carriers/sameday/status.js';
41
42
  import { statusMap as speedx } from '../../carriers/speedx/status.js';
@@ -54,8 +55,8 @@ export const STATUS_MAPS = {
54
55
  'acs-courier': acsCourier, aliexpress, 'an-post': anPost, 'box-now': boxNow, 'china-post': chinaPost, chronopost, 'correos-spain': correosSpain,
55
56
  'dhl-express': dhlExpress, dpd, 'dpd-de': dpdDe, 'dpd-fr': dpdFr, 'dpd-pl': dpdPl, econt, elta, emile, 'fan-courier': fanCourier, foxpost,
56
57
  'gofo-fr': gofoFr, 'gofo-it': gofoIt, 'j-and-t': jAndT, 'j-and-t-cargo': jntCargo, 'la-poste': laPoste, 'old-dominion': oldDominion, omgo,
57
- 'orlen-paczka': orlenPaczka, postlogistics, sagawa, sameday, speedx, speedy, sto, 'swiss-post': swissPost, 'thailand-post': thailandPost, tnt, ups,
58
- yunexpress,
58
+ 'orlen-paczka': orlenPaczka, postlogistics, ppl, sagawa, sameday, speedx, speedy, sto, 'swiss-post': swissPost, 'thailand-post': thailandPost, tnt,
59
+ ups, yunexpress,
59
60
  };
60
61
  /** The carrier the app files the universal providers' scans under. */
61
62
  const UNIVERSAL_CARRIER = 'unknown';
@@ -7462,7 +7462,7 @@
7462
7462
  "timezone": "Europe/Prague",
7463
7463
  "tracking": {
7464
7464
  "mode": "automatic",
7465
- "adapter": "universal"
7465
+ "adapter": "ppl"
7466
7466
  },
7467
7467
  "canaryUrl": "https://www.ppl.cz/vyhledat-zasilku",
7468
7468
  "trackingUrlTemplate": "https://www.ppl.cz/vyhledat-zasilku?shipmentId={trackingNumber}",
@@ -5011,7 +5011,7 @@ export declare const CARRIER_CATALOG: {
5011
5011
  readonly timezone: "Europe/Prague";
5012
5012
  readonly tracking: {
5013
5013
  readonly mode: "automatic";
5014
- readonly adapter: "universal";
5014
+ readonly adapter: "ppl";
5015
5015
  };
5016
5016
  readonly canaryUrl: "https://www.ppl.cz/vyhledat-zasilku";
5017
5017
  readonly trackingUrlTemplate: "https://www.ppl.cz/vyhledat-zasilku?shipmentId={trackingNumber}";
@@ -7463,7 +7463,7 @@ export const CARRIER_CATALOG = {
7463
7463
  "timezone": "Europe/Prague",
7464
7464
  "tracking": {
7465
7465
  "mode": "automatic",
7466
- "adapter": "universal"
7466
+ "adapter": "ppl"
7467
7467
  },
7468
7468
  "canaryUrl": "https://www.ppl.cz/vyhledat-zasilku",
7469
7469
  "trackingUrlTemplate": "https://www.ppl.cz/vyhledat-zasilku?shipmentId={trackingNumber}",
@@ -96,6 +96,7 @@ import { adapter as posteItaliane } from '../carriers/poste-italiane/adapter.js'
96
96
  import { adapter as posti } from '../carriers/posti/adapter.js';
97
97
  import { adapter as postlogistics } from '../carriers/postlogistics/adapter.js';
98
98
  import { adapter as postnord } from '../carriers/postnord/adapter.js';
99
+ import { adapter as ppl } from '../carriers/ppl/adapter.js';
99
100
  import { adapter as purolator } from '../carriers/purolator/adapter.js';
100
101
  import { adapter as relaisColis } from '../carriers/relais-colis/adapter.js';
101
102
  import { adapter as royalMail } from '../carriers/royal-mail/adapter.js';
@@ -227,6 +228,7 @@ export const REGISTRY = {
227
228
  "posti": posti,
228
229
  "postlogistics": postlogistics,
229
230
  "postnord": postnord,
231
+ "ppl": ppl,
230
232
  "purolator": purolator,
231
233
  "relais-colis": relaisColis,
232
234
  "royal-mail": royalMail,
@@ -413,7 +415,7 @@ export const REGISTRY = {
413
415
  "posti": "posti",
414
416
  "postlogistics": "postlogistics",
415
417
  "postnord": "postnord",
416
- "ppl": "universal",
418
+ "ppl": "ppl",
417
419
  "ptt": "universal",
418
420
  "purolator": "purolator",
419
421
  "quickpac": "planzer",
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.74.0"
5
+ "version": "0.75.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.74.0-main.925",
3
+ "version": "0.75.0-main.929",
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",