universal-parcel-scraper 0.3.2-main.314 → 0.3.4-main.317

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
@@ -15,7 +15,7 @@ The engine behind [Peek](https://peektracker.com) ([GitHub](https://github.com/p
15
15
  <!-- GENERATED:summary -->
16
16
  **3,500+ carriers** through **85 dedicated adapters** and **5 universal fallbacks**
17
17
 
18
- <sub>105 carriers in the catalog · 58 countries represented</sub>
18
+ <sub>105 catalog entries · 58 countries represented</sub>
19
19
  <!-- /GENERATED:summary -->
20
20
 
21
21
  [Try it](#try-it) · [Ways to run it](#ways-to-run-it) · [Benchmark](#benchmark) · [Carriers](carriers/) · [Add a carrier](CONTRIBUTING.md)
@@ -33,9 +33,9 @@ its own as a command, a Node library or an HTTP server.
33
33
  ## Benchmark
34
34
 
35
35
  <!-- GENERATED:coverage -->
36
- Benchmarked against 100 popular carriers, it returns tracking history for **70**. The best single aggregator returns 50.
36
+ In a curated benchmark of 100 carriers, dedicated adapters return tracking history for **62**, rising to **75** with all fallbacks enabled. The best single aggregator returns 52.
37
37
 
38
- <img src="docs/assets/coverage.svg" alt="Carriers with tracking history: This project, all fallbacks enabled 70, This project, dedicated adapters alone 51, ParcelsApp 50, Postal Ninja 41, 17TRACK 40, Ship24 38, UPU 11." width="760">
38
+ <img src="docs/assets/coverage.svg" alt="Carriers with tracking history: This project, all fallbacks enabled 75, This project, dedicated adapters alone 62, ParcelsApp 52, Postal Ninja 43, 17TRACK 40, Ship24 41, UPU 12." width="760">
39
39
  <!-- /GENERATED:coverage -->
40
40
 
41
41
  [Method and results per carrier](providers/COVERAGE.md)
@@ -149,7 +149,7 @@ createTracker({ providers: ['ParcelsApp', 'Ship24', '17TRACK', 'Postal Ninja', '
149
149
  <!-- GENERATED:stages -->
150
150
  <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">
151
151
 
152
- The carrier folders record 1,672 such statuses, each filed under one stage.
152
+ The carrier folders record 1,673 such statuses, each filed under one stage.
153
153
  <!-- /GENERATED:stages -->
154
154
 
155
155
  Wording nobody recorded yet goes through a classifier that reads seven European languages.
@@ -4,14 +4,14 @@
4
4
  "countries": 58,
5
5
  "referenceCarriers": 100,
6
6
  "comparison": {
7
- "combined": 70,
8
- "direct": 51,
7
+ "combined": 75,
8
+ "direct": 62,
9
9
  "providers": {
10
- "ParcelsApp": 50,
11
- "Postal Ninja": 41,
10
+ "ParcelsApp": 52,
11
+ "Postal Ninja": 43,
12
12
  "17TRACK": 40,
13
- "Ship24": 38,
14
- "UPU": 11
13
+ "Ship24": 41,
14
+ "UPU": 12
15
15
  }
16
16
  }
17
17
  }
@@ -8111,20 +8111,36 @@ var fs = { carriers: [
8111
8111
  {
8112
8112
  carrier: "bpost",
8113
8113
  direct: "Yes",
8114
- sample: "No history; alternate ✓ 11",
8115
- references: [{ results: {
8116
- Ship24: "no_history",
8117
- ParcelsApp: "postcode_prompt",
8118
- "17TRACK": "no_history",
8119
- "Postal Ninja": "no_history",
8120
- UPU: "n/a"
8121
- } }, {
8122
- note: "An S10 item through Landmark Global",
8123
- results: {
8124
- ParcelsApp: 20,
8125
- "17TRACK": 14
8114
+ sample: "✓ 11",
8115
+ references: [
8116
+ {
8117
+ note: "A domestic parcel",
8118
+ results: {
8119
+ Ship24: "no_history",
8120
+ ParcelsApp: "postcode_prompt",
8121
+ "17TRACK": "no_history",
8122
+ "Postal Ninja": "no_history",
8123
+ UPU: "n/a"
8124
+ }
8125
+ },
8126
+ {
8127
+ results: {
8128
+ Ship24: "no_history",
8129
+ ParcelsApp: "postcode_prompt",
8130
+ "17TRACK": "no_history",
8131
+ "Postal Ninja": "no_history",
8132
+ UPU: "n/a"
8133
+ },
8134
+ note: "Another comparison reference"
8135
+ },
8136
+ {
8137
+ note: "An S10 item through Landmark Global",
8138
+ results: {
8139
+ ParcelsApp: 20,
8140
+ "17TRACK": 14
8141
+ }
8126
8142
  }
8127
- }]
8143
+ ]
8128
8144
  },
8129
8145
  {
8130
8146
  carrier: "austrian-post",
@@ -8171,25 +8187,41 @@ var fs = { carriers: [
8171
8187
  {
8172
8188
  carrier: "aramex",
8173
8189
  direct: "Yes",
8174
- sample: "No history; alternate ✓ 26",
8175
- references: [{ results: {
8176
- Ship24: "no_history",
8177
- ParcelsApp: "no_history",
8178
- "17TRACK": "no_history",
8179
- "Postal Ninja": {
8180
- rows: 16,
8181
- partial: "undated rows"
8190
+ sample: "✓ 16",
8191
+ references: [
8192
+ {
8193
+ note: "A domestic shipment",
8194
+ results: {
8195
+ Ship24: "no_history",
8196
+ ParcelsApp: "no_history",
8197
+ "17TRACK": "no_history",
8198
+ "Postal Ninja": 16,
8199
+ UPU: "n/a"
8200
+ }
8182
8201
  },
8183
- UPU: "n/a"
8184
- } }, {
8185
- note: "An international shipment",
8186
- results: {
8187
- Ship24: 26,
8188
- ParcelsApp: "no_history",
8189
- "17TRACK": 26,
8190
- "Postal Ninja": 26
8202
+ {
8203
+ results: {
8204
+ Ship24: "no_history",
8205
+ ParcelsApp: "no_history",
8206
+ "17TRACK": "no_history",
8207
+ "Postal Ninja": {
8208
+ rows: 16,
8209
+ partial: "undated rows"
8210
+ },
8211
+ UPU: "n/a"
8212
+ },
8213
+ note: "Another comparison reference"
8214
+ },
8215
+ {
8216
+ note: "An international shipment",
8217
+ results: {
8218
+ Ship24: 26,
8219
+ ParcelsApp: "no_history",
8220
+ "17TRACK": 26,
8221
+ "Postal Ninja": 26
8222
+ }
8191
8223
  }
8192
- }]
8224
+ ]
8193
8225
  },
8194
8226
  {
8195
8227
  carrier: "yunexpress",
@@ -8317,14 +8349,26 @@ var fs = { carriers: [
8317
8349
  {
8318
8350
  carrier: "correios-br",
8319
8351
  direct: "Yes (local OCR)",
8320
- sample: "No history; alternate ✓ 14",
8321
- references: [{ results: {
8322
- Ship24: 8,
8323
- ParcelsApp: "no_history",
8324
- "17TRACK": 8,
8325
- "Postal Ninja": 8,
8326
- UPU: "no_history"
8327
- } }]
8352
+ sample: "✓ 8",
8353
+ references: [{
8354
+ note: "A Brazilian postal item",
8355
+ results: {
8356
+ Ship24: 8,
8357
+ ParcelsApp: "no_history",
8358
+ "17TRACK": 8,
8359
+ "Postal Ninja": "error",
8360
+ UPU: "no_history"
8361
+ }
8362
+ }, {
8363
+ results: {
8364
+ Ship24: 8,
8365
+ ParcelsApp: "no_history",
8366
+ "17TRACK": 8,
8367
+ "Postal Ninja": 8,
8368
+ UPU: "no_history"
8369
+ },
8370
+ note: "Another comparison reference"
8371
+ }]
8328
8372
  },
8329
8373
  {
8330
8374
  carrier: "singapore-post",
@@ -8359,23 +8403,39 @@ var fs = { carriers: [
8359
8403
  {
8360
8404
  carrier: "korea-post",
8361
8405
  direct: "Yes (international)",
8362
- sample: "Domestic unsupported; alternate ✓ 20",
8363
- references: [{ results: {
8364
- Ship24: "no_history",
8365
- ParcelsApp: "no_history",
8366
- "17TRACK": "no_history",
8367
- "Postal Ninja": "no_history",
8368
- UPU: "n/a"
8369
- } }, {
8370
- note: "An EMS item",
8371
- results: {
8372
- Ship24: "history",
8373
- ParcelsApp: "history",
8374
- "17TRACK": "history",
8375
- "Postal Ninja": "no_history",
8376
- UPU: "history"
8406
+ sample: "✓ 20",
8407
+ references: [
8408
+ {
8409
+ note: "An EMS item",
8410
+ results: {
8411
+ Ship24: 36,
8412
+ ParcelsApp: 41,
8413
+ "17TRACK": 28,
8414
+ "Postal Ninja": 27,
8415
+ UPU: 1
8416
+ }
8417
+ },
8418
+ {
8419
+ results: {
8420
+ Ship24: "no_history",
8421
+ ParcelsApp: "no_history",
8422
+ "17TRACK": "no_history",
8423
+ "Postal Ninja": "no_history",
8424
+ UPU: "n/a"
8425
+ },
8426
+ note: "Another comparison reference"
8427
+ },
8428
+ {
8429
+ note: "An EMS item",
8430
+ results: {
8431
+ Ship24: "history",
8432
+ ParcelsApp: "history",
8433
+ "17TRACK": "history",
8434
+ "Postal Ninja": "no_history",
8435
+ UPU: "history"
8436
+ }
8377
8437
  }
8378
- }]
8438
+ ]
8379
8439
  },
8380
8440
  {
8381
8441
  carrier: "planzer",
@@ -8461,14 +8521,26 @@ var fs = { carriers: [
8461
8521
  {
8462
8522
  carrier: "delhivery",
8463
8523
  direct: "Yes",
8464
- sample: "No history; alternate summary + 1 undated scan",
8465
- references: [{ results: {
8466
- Ship24: "no_history",
8467
- ParcelsApp: "no_history",
8468
- "17TRACK": "no_history",
8469
- "Postal Ninja": "no_history",
8470
- UPU: "n/a"
8471
- } }]
8524
+ sample: "✓ 2, partial (one undated scan and a status snapshot)",
8525
+ references: [{
8526
+ note: "A domestic parcel",
8527
+ results: {
8528
+ Ship24: "no_history",
8529
+ ParcelsApp: "no_history",
8530
+ "17TRACK": "no_history",
8531
+ "Postal Ninja": "no_history",
8532
+ UPU: "n/a"
8533
+ }
8534
+ }, {
8535
+ results: {
8536
+ Ship24: "no_history",
8537
+ ParcelsApp: "no_history",
8538
+ "17TRACK": "no_history",
8539
+ "Postal Ninja": "no_history",
8540
+ UPU: "n/a"
8541
+ },
8542
+ note: "Another comparison reference"
8543
+ }]
8472
8544
  },
8473
8545
  {
8474
8546
  carrier: "blue-dart",
@@ -8485,14 +8557,26 @@ var fs = { carriers: [
8485
8557
  {
8486
8558
  carrier: "dtdc",
8487
8559
  direct: "Yes",
8488
- sample: "No history; alternate ✓ 9",
8489
- references: [{ results: {
8490
- Ship24: "no_history",
8491
- ParcelsApp: 35,
8492
- "17TRACK": "no_history",
8493
- "Postal Ninja": "refused",
8494
- UPU: "n/a"
8495
- } }]
8560
+ sample: "✓ 9",
8561
+ references: [{
8562
+ note: "A domestic parcel",
8563
+ results: {
8564
+ Ship24: "no_history",
8565
+ ParcelsApp: 37,
8566
+ "17TRACK": "no_history",
8567
+ "Postal Ninja": "no_history",
8568
+ UPU: "n/a"
8569
+ }
8570
+ }, {
8571
+ results: {
8572
+ Ship24: "no_history",
8573
+ ParcelsApp: 35,
8574
+ "17TRACK": "no_history",
8575
+ "Postal Ninja": "refused",
8576
+ UPU: "n/a"
8577
+ },
8578
+ note: "Another comparison reference"
8579
+ }]
8496
8580
  },
8497
8581
  {
8498
8582
  carrier: "ninja-van",
@@ -8509,8 +8593,17 @@ var fs = { carriers: [
8509
8593
  {
8510
8594
  carrier: "packeta",
8511
8595
  direct: "Yes",
8512
- sample: "No history",
8596
+ sample: "✓ 6",
8513
8597
  references: [{
8598
+ note: "A parcel handed to a delivery partner",
8599
+ results: {
8600
+ Ship24: 11,
8601
+ ParcelsApp: "no_history",
8602
+ "17TRACK": "no_history",
8603
+ "Postal Ninja": 12,
8604
+ UPU: "n/a"
8605
+ }
8606
+ }, {
8514
8607
  stale: !0,
8515
8608
  results: {
8516
8609
  Ship24: "no_history",
@@ -8518,7 +8611,8 @@ var fs = { carriers: [
8518
8611
  "17TRACK": "no_history",
8519
8612
  "Postal Ninja": "no_history",
8520
8613
  UPU: "n/a"
8521
- }
8614
+ },
8615
+ note: "Another comparison reference"
8522
8616
  }]
8523
8617
  },
8524
8618
  {
@@ -8542,14 +8636,26 @@ var fs = { carriers: [
8542
8636
  {
8543
8637
  carrier: "bring-posten",
8544
8638
  direct: "Yes",
8545
- sample: "Not tested; alternate ✓ 3",
8546
- references: [{ results: {
8547
- Ship24: 12,
8548
- ParcelsApp: 15,
8549
- "17TRACK": 19,
8550
- "Postal Ninja": 16,
8551
- UPU: 3
8552
- } }]
8639
+ sample: "✓ 12",
8640
+ references: [{
8641
+ note: "A Norwegian postal item",
8642
+ results: {
8643
+ Ship24: 12,
8644
+ ParcelsApp: 15,
8645
+ "17TRACK": 19,
8646
+ "Postal Ninja": 16,
8647
+ UPU: 3
8648
+ }
8649
+ }, {
8650
+ results: {
8651
+ Ship24: 12,
8652
+ ParcelsApp: 15,
8653
+ "17TRACK": 19,
8654
+ "Postal Ninja": 16,
8655
+ UPU: 3
8656
+ },
8657
+ note: "Another comparison reference"
8658
+ }]
8553
8659
  },
8554
8660
  {
8555
8661
  carrier: "posti",
@@ -8602,14 +8708,26 @@ var fs = { carriers: [
8602
8708
  {
8603
8709
  carrier: "brt",
8604
8710
  direct: "Yes",
8605
- sample: "Not tested; alternate ✓ 7",
8606
- references: [{ results: {
8607
- Ship24: "no_history",
8608
- ParcelsApp: 23,
8609
- "17TRACK": "no_history",
8610
- "Postal Ninja": 5,
8611
- UPU: "n/a"
8612
- } }]
8711
+ sample: "✓ 9",
8712
+ references: [{
8713
+ note: "An international parcel",
8714
+ results: {
8715
+ Ship24: "no_history",
8716
+ ParcelsApp: 23,
8717
+ "17TRACK": "no_history",
8718
+ "Postal Ninja": 5,
8719
+ UPU: "n/a"
8720
+ }
8721
+ }, {
8722
+ results: {
8723
+ Ship24: "no_history",
8724
+ ParcelsApp: 23,
8725
+ "17TRACK": "no_history",
8726
+ "Postal Ninja": 5,
8727
+ UPU: "n/a"
8728
+ },
8729
+ note: "Another comparison reference"
8730
+ }]
8613
8731
  },
8614
8732
  {
8615
8733
  carrier: "seur",
@@ -8638,38 +8756,64 @@ var fs = { carriers: [
8638
8756
  {
8639
8757
  carrier: "mrw",
8640
8758
  direct: "Yes",
8641
- sample: "Summary only; alternate ✓ 14",
8759
+ sample: "✓ 14",
8760
+ references: [
8761
+ {
8762
+ note: "A delivered parcel",
8763
+ results: {
8764
+ Ship24: 14,
8765
+ ParcelsApp: 14,
8766
+ "17TRACK": "no_history",
8767
+ "Postal Ninja": 14,
8768
+ UPU: "n/a"
8769
+ }
8770
+ },
8771
+ {
8772
+ stale: !0,
8773
+ results: {
8774
+ Ship24: "no_history",
8775
+ ParcelsApp: "no_history",
8776
+ "17TRACK": 1,
8777
+ "Postal Ninja": "no_history",
8778
+ UPU: "n/a"
8779
+ },
8780
+ note: "Another comparison reference"
8781
+ },
8782
+ {
8783
+ note: "A September parcel",
8784
+ results: {
8785
+ Ship24: 14,
8786
+ ParcelsApp: 14,
8787
+ "17TRACK": "no_history",
8788
+ "Postal Ninja": 14
8789
+ }
8790
+ }
8791
+ ]
8792
+ },
8793
+ {
8794
+ carrier: "nacex",
8795
+ direct: "Yes",
8796
+ sample: "✓ 13",
8642
8797
  references: [{
8643
- stale: !0,
8798
+ note: "An agency/shipment reference",
8644
8799
  results: {
8645
- Ship24: "no_history",
8646
- ParcelsApp: "no_history",
8647
- "17TRACK": 1,
8648
- "Postal Ninja": "no_history",
8800
+ Ship24: "refused",
8801
+ ParcelsApp: "refused",
8802
+ "17TRACK": "refused",
8803
+ "Postal Ninja": "refused",
8649
8804
  UPU: "n/a"
8650
8805
  }
8651
8806
  }, {
8652
- note: "A September parcel",
8653
8807
  results: {
8654
- Ship24: 14,
8655
- ParcelsApp: 14,
8656
- "17TRACK": "no_history",
8657
- "Postal Ninja": 14
8658
- }
8808
+ Ship24: "error",
8809
+ ParcelsApp: "error",
8810
+ "17TRACK": "error",
8811
+ "Postal Ninja": "error",
8812
+ UPU: "n/a"
8813
+ },
8814
+ note: "Another comparison reference"
8659
8815
  }]
8660
8816
  },
8661
- {
8662
- carrier: "nacex",
8663
- direct: "Yes",
8664
- sample: "Not tested; alternate ✓ 14",
8665
- references: [{ results: {
8666
- Ship24: "error",
8667
- ParcelsApp: "error",
8668
- "17TRACK": "error",
8669
- "Postal Ninja": "error",
8670
- UPU: "n/a"
8671
- } }]
8672
- },
8673
8817
  {
8674
8818
  carrier: "colis-prive",
8675
8819
  direct: "Yes (postcode)",
@@ -22,7 +22,8 @@ export function parseDelhivery(payload, number) {
22
22
  throw new SchemaError('Delhivery');
23
23
  if (!payload.data.length && payload.message === 'invalid AWB or very old package')
24
24
  throw new NotFoundError('Delhivery');
25
- const matches = payload.data.filter(isRecord).filter(item => clean(item.awb, 64) === requested);
25
+ const matches = payload.data.filter(isRecord).filter(item => (typeof item.awb === 'string' ? item.awb.trim()
26
+ : typeof item.awb === 'number' && Number.isSafeInteger(item.awb) ? String(item.awb) : '') === requested);
26
27
  if (matches.length !== 1)
27
28
  throw new SchemaError('Delhivery', 'Delhivery did not return one matching shipment');
28
29
  const item = matches[0];
@@ -31,7 +32,14 @@ export function parseDelhivery(payload, number) {
31
32
  const description = clean(item.status.status, 200);
32
33
  if (!description)
33
34
  throw new IndeterminateError('Delhivery', 'Delhivery returned no current status');
34
- const mapped = classifyDelhiveryStatus(description);
35
+ const returning = item.status.statusType === 'RT' || item.currentFlow === 'Returned' || item.currentFlow === 'Reverse';
36
+ // RETURNED names a flow, including parcels still travelling to the sender.
37
+ // The separate shipment status establishes movement or completed delivery.
38
+ const snapshotDescription = returning && description.toUpperCase() === 'RETURNED'
39
+ ? clean(item.hqStatus, 200) : description;
40
+ const current = classifyDelhiveryStatus(snapshotDescription);
41
+ const mapped = returning && current?.stage === 'delivered'
42
+ ? classifyDelhiveryStatus('RTO DELIVERED') : current;
35
43
  const updated = scanTime(item.status.statusDateTime);
36
44
  const currentTimeText = clean(item.status.statusDateTime, 64);
37
45
  const events = [];
@@ -56,21 +64,25 @@ export function parseDelhivery(payload, number) {
56
64
  const time = scanTime(scan.scanDateTime);
57
65
  const timeText = clean(scan.scanDateTime, 64);
58
66
  const location = clean(scan.cityLocation, 160);
59
- const key = `${time?.timestamp ?? timeText}\u0000${label}\u0000${location}`;
67
+ const returnScan = scan.scanType === 'RT' || item.currentFlow === 'Reverse';
68
+ const key = `${time?.timestamp ?? timeText}\u0000${label}\u0000${location}\u0000${returnScan ? 'return' : ''}`;
60
69
  if (seen.has(key))
61
70
  continue;
62
71
  seen.add(key);
63
- const classified = classifyDelhiveryStatus(label);
72
+ const scanStatus = classifyDelhiveryStatus(label);
73
+ const classified = returnScan && scanStatus?.stage === 'delivered'
74
+ ? classifyDelhiveryStatus('RTO DELIVERED') : scanStatus;
64
75
  events.push({ description: label, ...(time ? { time: time.iso } : timeText ? { provider_time_text: timeText } : {}), ...(location ? { location } : {}),
65
- ...(classified ? { stage: classified.stage } : {}) });
76
+ ...(classified ? { stage: classified.stage } : {}), ...(returnScan ? { provider_leg: 'return' } : {}) });
66
77
  }
67
78
  }
68
79
  // Repeated scan labels cannot bind a summary timestamp to a particular
69
80
  // historical location. Keep the dated status snapshot separate.
70
- if (!updated || !events.some(event => event.time && Date.parse(event.time) === updated.timestamp
71
- && event.description?.toUpperCase() === description.toUpperCase())) {
72
- events.unshift({ description, ...(updated ? { time: updated.iso } : currentTimeText ? { provider_time_text: currentTimeText } : {}),
73
- ...(mapped ? { stage: mapped.stage } : {}), summary_snapshot: true });
81
+ if (snapshotDescription && (!updated || !events.some(event => event.time && Date.parse(event.time) === updated.timestamp
82
+ && event.description?.toUpperCase() === snapshotDescription.toUpperCase() && event.stage === mapped?.stage
83
+ && (!returning || event.provider_leg === 'return')))) {
84
+ events.unshift({ description: snapshotDescription, ...(updated ? { time: updated.iso } : currentTimeText ? { provider_time_text: currentTimeText } : {}),
85
+ ...(mapped ? { stage: mapped.stage } : {}), ...(returning ? { provider_leg: 'return' } : {}), summary_snapshot: true });
74
86
  }
75
87
  const dated = events.filter(event => event.time).sort((a, b) => Date.parse(b.time) - Date.parse(a.time));
76
88
  const undated = events.filter(event => !event.time);
@@ -2,9 +2,11 @@ const statuses = {
2
2
  DELIVERED: { status: 'delivered', stage: 'delivered' },
3
3
  'OUT FOR DELIVERY': { status: 'out_for_delivery', stage: 'out_for_delivery' },
4
4
  'IN TRANSIT': { status: 'in_transit', stage: 'in_transit' },
5
+ 'IN TRANSIT FOR RETURN': { status: 'in_transit', stage: 'in_transit' },
5
6
  'PICKED UP': { status: 'in_transit', stage: 'accepted' },
6
7
  MANIFESTED: { status: 'pending', stage: 'registered' },
7
8
  'RTO DELIVERED': { status: 'exception', stage: 'returned' },
9
+ DELIVERED_SELLER: { status: 'exception', stage: 'returned' },
8
10
  };
9
11
  export function classifyDelhiveryStatus(value) {
10
12
  return statuses[value.toUpperCase()];
@@ -6,6 +6,12 @@
6
6
  "language": "en",
7
7
  "stage": "delivered",
8
8
  "confirmedBy": "live"
9
+ },
10
+ {
11
+ "wording": "In Transit",
12
+ "language": "en",
13
+ "stage": "in_transit",
14
+ "confirmedBy": "live"
9
15
  }
10
16
  ]
11
17
  }
@@ -1,5 +1,5 @@
1
1
  import { lookupBudget } from '../../core/adapter/index.js';
2
- import { InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
2
+ import { IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
3
3
  import { zonedTime } from '../../core/time/index.js';
4
4
  import { clean, fetchBounded, parseJsonBytes, UpstreamHttpError, userAgentOf } from '../../core/transport/index.js';
5
5
  import { isRecord } from '../../core/types.js';
@@ -52,18 +52,22 @@ export function parsePacketaTrackingResponse(payload, trackingNumber) {
52
52
  if (!isRecord(payload))
53
53
  throw new SchemaError('Packeta', 'Packeta returned an invalid tracking response');
54
54
  const item = payload.item;
55
- // A 200 carrying an error instead of an item is Packeta's second unknown-code
56
- // signal (mirrors the HTTP 404 contract); it is a domain outcome, not a crash.
55
+ // Only the named notFound error establishes absence; other API errors say
56
+ // nothing about the requested parcel.
57
57
  if (!isRecord(item)) {
58
- if (typeof payload.error === 'string')
58
+ if (payload.error === 'notFound')
59
59
  throw new NotFoundError('Packeta');
60
+ if (typeof payload.error === 'string')
61
+ throw new IndeterminateError('Packeta', 'Packeta returned a tracking error');
60
62
  throw new SchemaError('Packeta', 'Packeta returned an invalid tracking response');
61
63
  }
62
- const returned = clean(item.barcode, 64).toLocaleUpperCase('en-US').replace(/[\s.-]/g, '');
64
+ const returned = typeof item.barcode === 'string'
65
+ ? item.barcode.toLocaleUpperCase('en-US').replace(/[\s.-]/g, '') : '';
63
66
  if (!returned)
64
67
  throw new SchemaError('Packeta', 'Packeta did not return a shipment identifier');
65
- if (returned !== requested)
68
+ if (returned !== requested && returned !== requested.slice(1)) {
66
69
  throw new SchemaError('Packeta', 'Packeta returned a different shipment');
70
+ }
67
71
  const code = clean(item.packetStatusId, 16);
68
72
  const classified = classifyPacketaStatus(code);
69
73
  const rawDetails = item.trackingDetails;
@@ -157,8 +161,18 @@ export class PacketaTracker {
157
161
  allowHttpError: true,
158
162
  fetcher: this.fetcher,
159
163
  });
160
- if (response.status === 404)
161
- throw new NotFoundError('Packeta');
164
+ if (response.status === 404) {
165
+ let payload;
166
+ try {
167
+ payload = parseJsonBytes(bytes, 'Packeta tracking');
168
+ }
169
+ catch {
170
+ throw new UpstreamHttpError('Packeta tracking', response.status);
171
+ }
172
+ if (isRecord(payload) && payload.error === 'notFound')
173
+ throw new NotFoundError('Packeta');
174
+ throw new UpstreamHttpError('Packeta tracking', response.status);
175
+ }
162
176
  if (!response.ok)
163
177
  throw new UpstreamHttpError('Packeta tracking', response.status);
164
178
  return parsePacketaTrackingResponse(parseJsonBytes(bytes, 'Packeta tracking'), trackingNumber);
@@ -531,9 +531,10 @@
531
531
  {
532
532
  "carrier": "bpost",
533
533
  "direct": "Yes",
534
- "sample": "No history; alternate ✓ 11",
534
+ "sample": "✓ 11",
535
535
  "references": [
536
536
  {
537
+ "note": "A domestic parcel",
537
538
  "results": {
538
539
  "Ship24": "no_history",
539
540
  "ParcelsApp": "postcode_prompt",
@@ -542,6 +543,16 @@
542
543
  "UPU": "n/a"
543
544
  }
544
545
  },
546
+ {
547
+ "results": {
548
+ "Ship24": "no_history",
549
+ "ParcelsApp": "postcode_prompt",
550
+ "17TRACK": "no_history",
551
+ "Postal Ninja": "no_history",
552
+ "UPU": "n/a"
553
+ },
554
+ "note": "Another comparison reference"
555
+ },
545
556
  {
546
557
  "note": "An S10 item through Landmark Global",
547
558
  "results": {
@@ -608,8 +619,18 @@
608
619
  {
609
620
  "carrier": "aramex",
610
621
  "direct": "Yes",
611
- "sample": "No history; alternate ✓ 26",
622
+ "sample": "✓ 16",
612
623
  "references": [
624
+ {
625
+ "note": "A domestic shipment",
626
+ "results": {
627
+ "Ship24": "no_history",
628
+ "ParcelsApp": "no_history",
629
+ "17TRACK": "no_history",
630
+ "Postal Ninja": 16,
631
+ "UPU": "n/a"
632
+ }
633
+ },
613
634
  {
614
635
  "results": {
615
636
  "Ship24": "no_history",
@@ -620,7 +641,8 @@
620
641
  "partial": "undated rows"
621
642
  },
622
643
  "UPU": "n/a"
623
- }
644
+ },
645
+ "note": "Another comparison reference"
624
646
  },
625
647
  {
626
648
  "note": "An international shipment",
@@ -799,16 +821,27 @@
799
821
  {
800
822
  "carrier": "correios-br",
801
823
  "direct": "Yes (local OCR)",
802
- "sample": "No history; alternate ✓ 14",
824
+ "sample": "✓ 8",
803
825
  "references": [
804
826
  {
827
+ "note": "A Brazilian postal item",
805
828
  "results": {
806
829
  "Ship24": 8,
807
830
  "ParcelsApp": "no_history",
808
831
  "17TRACK": 8,
809
- "Postal Ninja": 8,
832
+ "Postal Ninja": "error",
810
833
  "UPU": "no_history"
811
834
  }
835
+ },
836
+ {
837
+ "results": {
838
+ "Ship24": 8,
839
+ "ParcelsApp": "no_history",
840
+ "17TRACK": 8,
841
+ "Postal Ninja": 8,
842
+ "UPU": "no_history"
843
+ },
844
+ "note": "Another comparison reference"
812
845
  }
813
846
  ]
814
847
  },
@@ -853,8 +886,18 @@
853
886
  {
854
887
  "carrier": "korea-post",
855
888
  "direct": "Yes (international)",
856
- "sample": "Domestic unsupported; alternate ✓ 20",
889
+ "sample": "✓ 20",
857
890
  "references": [
891
+ {
892
+ "note": "An EMS item",
893
+ "results": {
894
+ "Ship24": 36,
895
+ "ParcelsApp": 41,
896
+ "17TRACK": 28,
897
+ "Postal Ninja": 27,
898
+ "UPU": 1
899
+ }
900
+ },
858
901
  {
859
902
  "results": {
860
903
  "Ship24": "no_history",
@@ -862,7 +905,8 @@
862
905
  "17TRACK": "no_history",
863
906
  "Postal Ninja": "no_history",
864
907
  "UPU": "n/a"
865
- }
908
+ },
909
+ "note": "Another comparison reference"
866
910
  },
867
911
  {
868
912
  "note": "An EMS item",
@@ -982,9 +1026,10 @@
982
1026
  {
983
1027
  "carrier": "delhivery",
984
1028
  "direct": "Yes",
985
- "sample": "No history; alternate summary + 1 undated scan",
1029
+ "sample": "✓ 2, partial (one undated scan and a status snapshot)",
986
1030
  "references": [
987
1031
  {
1032
+ "note": "A domestic parcel",
988
1033
  "results": {
989
1034
  "Ship24": "no_history",
990
1035
  "ParcelsApp": "no_history",
@@ -992,6 +1037,16 @@
992
1037
  "Postal Ninja": "no_history",
993
1038
  "UPU": "n/a"
994
1039
  }
1040
+ },
1041
+ {
1042
+ "results": {
1043
+ "Ship24": "no_history",
1044
+ "ParcelsApp": "no_history",
1045
+ "17TRACK": "no_history",
1046
+ "Postal Ninja": "no_history",
1047
+ "UPU": "n/a"
1048
+ },
1049
+ "note": "Another comparison reference"
995
1050
  }
996
1051
  ]
997
1052
  },
@@ -1014,8 +1069,18 @@
1014
1069
  {
1015
1070
  "carrier": "dtdc",
1016
1071
  "direct": "Yes",
1017
- "sample": "No history; alternate ✓ 9",
1072
+ "sample": "✓ 9",
1018
1073
  "references": [
1074
+ {
1075
+ "note": "A domestic parcel",
1076
+ "results": {
1077
+ "Ship24": "no_history",
1078
+ "ParcelsApp": 37,
1079
+ "17TRACK": "no_history",
1080
+ "Postal Ninja": "no_history",
1081
+ "UPU": "n/a"
1082
+ }
1083
+ },
1019
1084
  {
1020
1085
  "results": {
1021
1086
  "Ship24": "no_history",
@@ -1023,7 +1088,8 @@
1023
1088
  "17TRACK": "no_history",
1024
1089
  "Postal Ninja": "refused",
1025
1090
  "UPU": "n/a"
1026
- }
1091
+ },
1092
+ "note": "Another comparison reference"
1027
1093
  }
1028
1094
  ]
1029
1095
  },
@@ -1046,8 +1112,18 @@
1046
1112
  {
1047
1113
  "carrier": "packeta",
1048
1114
  "direct": "Yes",
1049
- "sample": "No history",
1115
+ "sample": "✓ 6",
1050
1116
  "references": [
1117
+ {
1118
+ "note": "A parcel handed to a delivery partner",
1119
+ "results": {
1120
+ "Ship24": 11,
1121
+ "ParcelsApp": "no_history",
1122
+ "17TRACK": "no_history",
1123
+ "Postal Ninja": 12,
1124
+ "UPU": "n/a"
1125
+ }
1126
+ },
1051
1127
  {
1052
1128
  "stale": true,
1053
1129
  "results": {
@@ -1056,7 +1132,8 @@
1056
1132
  "17TRACK": "no_history",
1057
1133
  "Postal Ninja": "no_history",
1058
1134
  "UPU": "n/a"
1059
- }
1135
+ },
1136
+ "note": "Another comparison reference"
1060
1137
  }
1061
1138
  ]
1062
1139
  },
@@ -1086,9 +1163,10 @@
1086
1163
  {
1087
1164
  "carrier": "bring-posten",
1088
1165
  "direct": "Yes",
1089
- "sample": "Not tested; alternate ✓ 3",
1166
+ "sample": "✓ 12",
1090
1167
  "references": [
1091
1168
  {
1169
+ "note": "A Norwegian postal item",
1092
1170
  "results": {
1093
1171
  "Ship24": 12,
1094
1172
  "ParcelsApp": 15,
@@ -1096,6 +1174,16 @@
1096
1174
  "Postal Ninja": 16,
1097
1175
  "UPU": 3
1098
1176
  }
1177
+ },
1178
+ {
1179
+ "results": {
1180
+ "Ship24": 12,
1181
+ "ParcelsApp": 15,
1182
+ "17TRACK": 19,
1183
+ "Postal Ninja": 16,
1184
+ "UPU": 3
1185
+ },
1186
+ "note": "Another comparison reference"
1099
1187
  }
1100
1188
  ]
1101
1189
  },
@@ -1166,9 +1254,10 @@
1166
1254
  {
1167
1255
  "carrier": "brt",
1168
1256
  "direct": "Yes",
1169
- "sample": "Not tested; alternate ✓ 7",
1257
+ "sample": "✓ 9",
1170
1258
  "references": [
1171
1259
  {
1260
+ "note": "An international parcel",
1172
1261
  "results": {
1173
1262
  "Ship24": "no_history",
1174
1263
  "ParcelsApp": 23,
@@ -1176,6 +1265,16 @@
1176
1265
  "Postal Ninja": 5,
1177
1266
  "UPU": "n/a"
1178
1267
  }
1268
+ },
1269
+ {
1270
+ "results": {
1271
+ "Ship24": "no_history",
1272
+ "ParcelsApp": 23,
1273
+ "17TRACK": "no_history",
1274
+ "Postal Ninja": 5,
1275
+ "UPU": "n/a"
1276
+ },
1277
+ "note": "Another comparison reference"
1179
1278
  }
1180
1279
  ]
1181
1280
  },
@@ -1214,8 +1313,18 @@
1214
1313
  {
1215
1314
  "carrier": "mrw",
1216
1315
  "direct": "Yes",
1217
- "sample": "Summary only; alternate ✓ 14",
1316
+ "sample": "✓ 14",
1218
1317
  "references": [
1318
+ {
1319
+ "note": "A delivered parcel",
1320
+ "results": {
1321
+ "Ship24": 14,
1322
+ "ParcelsApp": 14,
1323
+ "17TRACK": "no_history",
1324
+ "Postal Ninja": 14,
1325
+ "UPU": "n/a"
1326
+ }
1327
+ },
1219
1328
  {
1220
1329
  "stale": true,
1221
1330
  "results": {
@@ -1224,7 +1333,8 @@
1224
1333
  "17TRACK": 1,
1225
1334
  "Postal Ninja": "no_history",
1226
1335
  "UPU": "n/a"
1227
- }
1336
+ },
1337
+ "note": "Another comparison reference"
1228
1338
  },
1229
1339
  {
1230
1340
  "note": "A September parcel",
@@ -1240,8 +1350,18 @@
1240
1350
  {
1241
1351
  "carrier": "nacex",
1242
1352
  "direct": "Yes",
1243
- "sample": "Not tested; alternate ✓ 14",
1353
+ "sample": "✓ 13",
1244
1354
  "references": [
1355
+ {
1356
+ "note": "An agency/shipment reference",
1357
+ "results": {
1358
+ "Ship24": "refused",
1359
+ "ParcelsApp": "refused",
1360
+ "17TRACK": "refused",
1361
+ "Postal Ninja": "refused",
1362
+ "UPU": "n/a"
1363
+ }
1364
+ },
1245
1365
  {
1246
1366
  "results": {
1247
1367
  "Ship24": "error",
@@ -1249,7 +1369,8 @@
1249
1369
  "17TRACK": "error",
1250
1370
  "Postal Ninja": "error",
1251
1371
  "UPU": "n/a"
1252
- }
1372
+ },
1373
+ "note": "Another comparison reference"
1253
1374
  }
1254
1375
  ]
1255
1376
  },
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.3.2"
5
+ "version": "0.3.4"
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.3.2-main.314",
3
+ "version": "0.3.4-main.317",
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",