universal-parcel-scraper 0.36.0 → 0.37.0-main.389

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/data/catalog.json CHANGED
@@ -4431,7 +4431,8 @@
4431
4431
  "help": "Optional. DPD uses it to unlock verified scans and the delivery window.",
4432
4432
  "validator": "germanyPostcode"
4433
4433
  }
4434
- ]
4434
+ ],
4435
+ "recognitionRank": 65
4435
4436
  },
4436
4437
  "canaryUrl": "https://tracking.dpd.de/status/en_US/parcel/",
4437
4438
  "trackingUrlTemplate": "https://tracking.dpd.de/status/en_US/parcel/{trackingNumber}",
@@ -248,10 +248,10 @@
248
248
  ],
249
249
  "asked": [
250
250
  "dpd",
251
+ "dpd-de",
251
252
  "seur",
252
253
  "brt",
253
- "hermes-de",
254
- "relais-colis"
254
+ "hermes-de"
255
255
  ]
256
256
  },
257
257
  {
@@ -375,10 +375,10 @@
375
375
  ],
376
376
  "asked": [
377
377
  "dpd",
378
+ "dpd-de",
378
379
  "seur",
379
380
  "brt",
380
- "hermes-de",
381
- "relais-colis"
381
+ "hermes-de"
382
382
  ]
383
383
  },
384
384
  {
@@ -451,10 +451,10 @@
451
451
  ],
452
452
  "asked": [
453
453
  "dpd",
454
+ "dpd-de",
454
455
  "seur",
455
456
  "brt",
456
- "hermes-de",
457
- "relais-colis"
457
+ "hermes-de"
458
458
  ]
459
459
  },
460
460
  {
@@ -469,6 +469,7 @@
469
469
  ],
470
470
  "asked": [
471
471
  "dpd",
472
+ "dpd-de",
472
473
  "chronopost"
473
474
  ]
474
475
  },
@@ -490,10 +491,10 @@
490
491
  ],
491
492
  "asked": [
492
493
  "dpd",
494
+ "dpd-de",
493
495
  "seur",
494
496
  "brt",
495
- "relais-colis",
496
- "ciblex"
497
+ "relais-colis"
497
498
  ]
498
499
  },
499
500
  {
@@ -514,10 +515,10 @@
514
515
  ],
515
516
  "asked": [
516
517
  "dpd",
518
+ "dpd-de",
517
519
  "seur",
518
520
  "brt",
519
- "relais-colis",
520
- "ciblex"
521
+ "relais-colis"
521
522
  ]
522
523
  },
523
524
  {
@@ -538,10 +539,10 @@
538
539
  ],
539
540
  "asked": [
540
541
  "dpd",
542
+ "dpd-de",
541
543
  "seur",
542
544
  "brt",
543
- "relais-colis",
544
- "ciblex"
545
+ "relais-colis"
545
546
  ]
546
547
  },
547
548
  {
@@ -562,10 +563,10 @@
562
563
  ],
563
564
  "asked": [
564
565
  "dpd",
566
+ "dpd-de",
565
567
  "seur",
566
568
  "brt",
567
- "relais-colis",
568
- "ciblex"
569
+ "relais-colis"
569
570
  ]
570
571
  },
571
572
  {
@@ -586,10 +587,10 @@
586
587
  ],
587
588
  "asked": [
588
589
  "dpd",
590
+ "dpd-de",
589
591
  "seur",
590
592
  "brt",
591
- "relais-colis",
592
- "ciblex"
593
+ "relais-colis"
593
594
  ]
594
595
  },
595
596
  {
@@ -619,10 +620,10 @@
619
620
  ],
620
621
  "asked": [
621
622
  "dpd",
623
+ "dpd-de",
622
624
  "seur",
623
625
  "brt",
624
- "hermes-de",
625
- "relais-colis"
626
+ "hermes-de"
626
627
  ]
627
628
  },
628
629
  {
@@ -644,10 +645,10 @@
644
645
  ],
645
646
  "asked": [
646
647
  "dpd",
648
+ "dpd-de",
647
649
  "seur",
648
650
  "brt",
649
- "hermes-de",
650
- "relais-colis"
651
+ "hermes-de"
651
652
  ]
652
653
  },
653
654
  {
@@ -669,10 +670,10 @@
669
670
  ],
670
671
  "asked": [
671
672
  "dpd",
673
+ "dpd-de",
672
674
  "seur",
673
675
  "brt",
674
- "hermes-de",
675
- "relais-colis"
676
+ "hermes-de"
676
677
  ]
677
678
  },
678
679
  {
@@ -694,10 +695,10 @@
694
695
  ],
695
696
  "asked": [
696
697
  "dpd",
698
+ "dpd-de",
697
699
  "seur",
698
700
  "brt",
699
- "hermes-de",
700
- "relais-colis"
701
+ "hermes-de"
701
702
  ]
702
703
  },
703
704
  {
@@ -899,10 +900,10 @@
899
900
  ],
900
901
  "asked": [
901
902
  "dpd",
903
+ "dpd-de",
902
904
  "seur",
903
905
  "brt",
904
- "relais-colis",
905
- "ciblex"
906
+ "relais-colis"
906
907
  ]
907
908
  },
908
909
  {
@@ -1008,10 +1009,10 @@
1008
1009
  ],
1009
1010
  "asked": [
1010
1011
  "dpd",
1012
+ "dpd-de",
1011
1013
  "seur",
1012
1014
  "brt",
1013
- "relais-colis",
1014
- "ciblex"
1015
+ "relais-colis"
1015
1016
  ]
1016
1017
  },
1017
1018
  {
@@ -1035,10 +1036,10 @@
1035
1036
  ],
1036
1037
  "asked": [
1037
1038
  "dpd",
1039
+ "dpd-de",
1038
1040
  "seur",
1039
1041
  "brt",
1040
- "relais-colis",
1041
- "ciblex"
1042
+ "relais-colis"
1042
1043
  ]
1043
1044
  },
1044
1045
  {
@@ -1067,10 +1068,10 @@
1067
1068
  ],
1068
1069
  "asked": [
1069
1070
  "dpd",
1071
+ "dpd-de",
1070
1072
  "seur",
1071
1073
  "brt",
1072
- "relais-colis",
1073
- "ciblex"
1074
+ "relais-colis"
1074
1075
  ]
1075
1076
  },
1076
1077
  {
@@ -1175,10 +1176,10 @@
1175
1176
  ],
1176
1177
  "asked": [
1177
1178
  "dpd",
1179
+ "dpd-de",
1178
1180
  "seur",
1179
1181
  "brt",
1180
- "relais-colis",
1181
- "ciblex"
1182
+ "relais-colis"
1182
1183
  ]
1183
1184
  },
1184
1185
  {
@@ -1211,10 +1212,10 @@
1211
1212
  ],
1212
1213
  "asked": [
1213
1214
  "dpd",
1215
+ "dpd-de",
1214
1216
  "seur",
1215
1217
  "brt",
1216
- "relais-colis",
1217
- "ciblex"
1218
+ "relais-colis"
1218
1219
  ]
1219
1220
  },
1220
1221
  {
@@ -1516,10 +1517,10 @@
1516
1517
  ],
1517
1518
  "asked": [
1518
1519
  "dpd",
1520
+ "dpd-de",
1519
1521
  "seur",
1520
1522
  "brt",
1521
- "hermes-de",
1522
- "relais-colis"
1523
+ "hermes-de"
1523
1524
  ]
1524
1525
  },
1525
1526
  {
@@ -1792,10 +1793,10 @@
1792
1793
  ],
1793
1794
  "asked": [
1794
1795
  "dpd",
1796
+ "dpd-de",
1795
1797
  "seur",
1796
1798
  "brt",
1797
- "relais-colis",
1798
- "ciblex"
1799
+ "relais-colis"
1799
1800
  ]
1800
1801
  },
1801
1802
  {
@@ -1974,10 +1975,10 @@
1974
1975
  ],
1975
1976
  "asked": [
1976
1977
  "dpd",
1978
+ "dpd-de",
1977
1979
  "seur",
1978
1980
  "brt",
1979
- "relais-colis",
1980
- "ciblex"
1981
+ "relais-colis"
1981
1982
  ]
1982
1983
  },
1983
1984
  {
@@ -1999,10 +2000,10 @@
1999
2000
  ],
2000
2001
  "asked": [
2001
2002
  "dpd",
2003
+ "dpd-de",
2002
2004
  "seur",
2003
2005
  "brt",
2004
- "relais-colis",
2005
- "ciblex"
2006
+ "relais-colis"
2006
2007
  ]
2007
2008
  },
2008
2009
  {
@@ -2024,10 +2025,10 @@
2024
2025
  ],
2025
2026
  "asked": [
2026
2027
  "dpd",
2028
+ "dpd-de",
2027
2029
  "seur",
2028
2030
  "brt",
2029
- "relais-colis",
2030
- "ciblex"
2031
+ "relais-colis"
2031
2032
  ]
2032
2033
  },
2033
2034
  {
@@ -3115,10 +3116,10 @@
3115
3116
  ],
3116
3117
  "asked": [
3117
3118
  "dpd",
3119
+ "dpd-de",
3118
3120
  "seur",
3119
3121
  "brt",
3120
- "relais-colis",
3121
- "ciblex"
3122
+ "relais-colis"
3122
3123
  ]
3123
3124
  },
3124
3125
  {
@@ -3140,10 +3141,10 @@
3140
3141
  ],
3141
3142
  "asked": [
3142
3143
  "dpd",
3144
+ "dpd-de",
3143
3145
  "seur",
3144
3146
  "brt",
3145
- "hermes-de",
3146
- "relais-colis"
3147
+ "hermes-de"
3147
3148
  ]
3148
3149
  },
3149
3150
  {
@@ -3165,10 +3166,10 @@
3165
3166
  ],
3166
3167
  "asked": [
3167
3168
  "dpd",
3169
+ "dpd-de",
3168
3170
  "seur",
3169
3171
  "brt",
3170
- "hermes-de",
3171
- "relais-colis"
3172
+ "hermes-de"
3172
3173
  ]
3173
3174
  },
3174
3175
  {
@@ -3221,10 +3222,10 @@
3221
3222
  ],
3222
3223
  "asked": [
3223
3224
  "dpd",
3225
+ "dpd-de",
3224
3226
  "seur",
3225
3227
  "brt",
3226
- "relais-colis",
3227
- "ciblex"
3228
+ "relais-colis"
3228
3229
  ]
3229
3230
  },
3230
3231
  {
@@ -3562,10 +3563,10 @@
3562
3563
  ],
3563
3564
  "asked": [
3564
3565
  "dpd",
3566
+ "dpd-de",
3565
3567
  "seur",
3566
3568
  "brt",
3567
- "hermes-de",
3568
- "relais-colis"
3569
+ "hermes-de"
3569
3570
  ]
3570
3571
  },
3571
3572
  {
@@ -3651,10 +3652,10 @@
3651
3652
  ],
3652
3653
  "asked": [
3653
3654
  "dpd",
3655
+ "dpd-de",
3654
3656
  "seur",
3655
3657
  "brt",
3656
- "relais-colis",
3657
- "ciblex"
3658
+ "relais-colis"
3658
3659
  ]
3659
3660
  },
3660
3661
  {
@@ -3674,10 +3675,10 @@
3674
3675
  ],
3675
3676
  "asked": [
3676
3677
  "dpd",
3678
+ "dpd-de",
3677
3679
  "seur",
3678
3680
  "brt",
3679
- "relais-colis",
3680
- "ciblex"
3681
+ "relais-colis"
3681
3682
  ]
3682
3683
  },
3683
3684
  {
@@ -3737,10 +3738,10 @@
3737
3738
  ],
3738
3739
  "asked": [
3739
3740
  "dpd",
3741
+ "dpd-de",
3740
3742
  "seur",
3741
3743
  "brt",
3742
- "relais-colis",
3743
- "ciblex"
3744
+ "relais-colis"
3744
3745
  ]
3745
3746
  },
3746
3747
  {
@@ -4154,10 +4155,10 @@
4154
4155
  ],
4155
4156
  "asked": [
4156
4157
  "dpd",
4158
+ "dpd-de",
4157
4159
  "seur",
4158
4160
  "brt",
4159
- "relais-colis",
4160
- "ciblex"
4161
+ "relais-colis"
4161
4162
  ]
4162
4163
  },
4163
4164
  {
@@ -4177,10 +4178,10 @@
4177
4178
  ],
4178
4179
  "asked": [
4179
4180
  "dpd",
4181
+ "dpd-de",
4180
4182
  "seur",
4181
4183
  "brt",
4182
- "relais-colis",
4183
- "ciblex"
4184
+ "relais-colis"
4184
4185
  ]
4185
4186
  },
4186
4187
  {
@@ -4326,10 +4327,10 @@
4326
4327
  ],
4327
4328
  "asked": [
4328
4329
  "dpd",
4330
+ "dpd-de",
4329
4331
  "seur",
4330
4332
  "brt",
4331
- "hermes-de",
4332
- "relais-colis"
4333
+ "hermes-de"
4333
4334
  ]
4334
4335
  },
4335
4336
  {
@@ -4349,10 +4350,10 @@
4349
4350
  ],
4350
4351
  "asked": [
4351
4352
  "dpd",
4353
+ "dpd-de",
4352
4354
  "seur",
4353
4355
  "brt",
4354
- "relais-colis",
4355
- "ciblex"
4356
+ "relais-colis"
4356
4357
  ]
4357
4358
  },
4358
4359
  {
@@ -4564,10 +4565,10 @@
4564
4565
  ],
4565
4566
  "asked": [
4566
4567
  "dpd",
4568
+ "dpd-de",
4567
4569
  "seur",
4568
4570
  "brt",
4569
- "hermes-de",
4570
- "relais-colis"
4571
+ "hermes-de"
4571
4572
  ]
4572
4573
  },
4573
4574
  {
@@ -5002,10 +5003,10 @@
5002
5003
  ],
5003
5004
  "asked": [
5004
5005
  "dpd",
5006
+ "dpd-de",
5005
5007
  "seur",
5006
5008
  "brt",
5007
- "hermes-de",
5008
- "relais-colis"
5009
+ "hermes-de"
5009
5010
  ]
5010
5011
  },
5011
5012
  {
@@ -5266,10 +5267,10 @@
5266
5267
  ],
5267
5268
  "asked": [
5268
5269
  "dpd",
5270
+ "dpd-de",
5269
5271
  "seur",
5270
5272
  "brt",
5271
- "relais-colis",
5272
- "ciblex"
5273
+ "relais-colis"
5273
5274
  ]
5274
5275
  },
5275
5276
  {
@@ -5289,10 +5290,10 @@
5289
5290
  ],
5290
5291
  "asked": [
5291
5292
  "dpd",
5293
+ "dpd-de",
5292
5294
  "seur",
5293
5295
  "brt",
5294
- "relais-colis",
5295
- "ciblex"
5296
+ "relais-colis"
5296
5297
  ]
5297
5298
  },
5298
5299
  {
@@ -5346,10 +5347,10 @@
5346
5347
  ],
5347
5348
  "asked": [
5348
5349
  "dpd",
5350
+ "dpd-de",
5349
5351
  "seur",
5350
5352
  "brt",
5351
- "relais-colis",
5352
- "ciblex"
5353
+ "relais-colis"
5353
5354
  ]
5354
5355
  },
5355
5356
  {
@@ -5429,10 +5430,10 @@
5429
5430
  ],
5430
5431
  "asked": [
5431
5432
  "dpd",
5433
+ "dpd-de",
5432
5434
  "seur",
5433
5435
  "brt",
5434
- "relais-colis",
5435
- "ciblex"
5436
+ "relais-colis"
5436
5437
  ]
5437
5438
  },
5438
5439
  {
@@ -5453,10 +5454,10 @@
5453
5454
  ],
5454
5455
  "asked": [
5455
5456
  "dpd",
5457
+ "dpd-de",
5456
5458
  "seur",
5457
5459
  "brt",
5458
- "relais-colis",
5459
- "ciblex"
5460
+ "relais-colis"
5460
5461
  ]
5461
5462
  },
5462
5463
  {
@@ -5507,10 +5508,10 @@
5507
5508
  ],
5508
5509
  "asked": [
5509
5510
  "dpd",
5511
+ "dpd-de",
5510
5512
  "seur",
5511
5513
  "brt",
5512
- "relais-colis",
5513
- "ciblex"
5514
+ "relais-colis"
5514
5515
  ]
5515
5516
  },
5516
5517
  {
@@ -5530,10 +5531,10 @@
5530
5531
  ],
5531
5532
  "asked": [
5532
5533
  "dpd",
5534
+ "dpd-de",
5533
5535
  "seur",
5534
5536
  "brt",
5535
- "relais-colis",
5536
- "ciblex"
5537
+ "relais-colis"
5537
5538
  ]
5538
5539
  },
5539
5540
  {
@@ -5553,10 +5554,10 @@
5553
5554
  ],
5554
5555
  "asked": [
5555
5556
  "dpd",
5557
+ "dpd-de",
5556
5558
  "seur",
5557
5559
  "brt",
5558
- "relais-colis",
5559
- "ciblex"
5560
+ "relais-colis"
5560
5561
  ]
5561
5562
  },
5562
5563
  {
@@ -5697,10 +5698,10 @@
5697
5698
  ],
5698
5699
  "asked": [
5699
5700
  "dpd",
5701
+ "dpd-de",
5700
5702
  "seur",
5701
5703
  "brt",
5702
- "relais-colis",
5703
- "ciblex"
5704
+ "relais-colis"
5704
5705
  ]
5705
5706
  },
5706
5707
  {
@@ -5844,10 +5845,10 @@
5844
5845
  ],
5845
5846
  "asked": [
5846
5847
  "dpd",
5848
+ "dpd-de",
5847
5849
  "seur",
5848
5850
  "brt",
5849
- "relais-colis",
5850
- "ciblex"
5851
+ "relais-colis"
5851
5852
  ]
5852
5853
  },
5853
5854
  {
@@ -6959,10 +6960,10 @@
6959
6960
  ],
6960
6961
  "asked": [
6961
6962
  "dpd",
6963
+ "dpd-de",
6962
6964
  "seur",
6963
6965
  "brt",
6964
- "relais-colis",
6965
- "ciblex"
6966
+ "relais-colis"
6966
6967
  ]
6967
6968
  },
6968
6969
  {
@@ -7356,10 +7357,10 @@
7356
7357
  ],
7357
7358
  "asked": [
7358
7359
  "dpd",
7360
+ "dpd-de",
7359
7361
  "seur",
7360
7362
  "brt",
7361
- "relais-colis",
7362
- "ciblex"
7363
+ "relais-colis"
7363
7364
  ]
7364
7365
  },
7365
7366
  {
@@ -3441,7 +3441,8 @@ var ce = {
3441
3441
  autoComplete: "postal-code",
3442
3442
  help: "Optional. DPD uses it to unlock verified scans and the delivery window.",
3443
3443
  validator: "germanyPostcode"
3444
- }]
3444
+ }],
3445
+ recognitionRank: 65
3445
3446
  },
3446
3447
  canaryUrl: "https://tracking.dpd.de/status/en_US/parcel/",
3447
3448
  trackingUrlTemplate: "https://tracking.dpd.de/status/en_US/parcel/{trackingNumber}",
@@ -8224,6 +8225,7 @@ var ts = {
8224
8225
  "dhl-ecommerce-uk": 17,
8225
8226
  "dhl-express": 75,
8226
8227
  dpd: 70,
8228
+ "dpd-de": 65,
8227
8229
  "four-px": 13,
8228
8230
  "gls-ch": 60,
8229
8231
  "gls-de": 50,
@@ -39,11 +39,12 @@ export interface DPDTrackerOptions {
39
39
  app?: DPDAppTier;
40
40
  }
41
41
  export interface DPDAppTier {
42
- /** `leads` when the guest tier is still to come and needs time left. */
42
+ /** `leads` when the guest tier is still to come and needs time left. `postcode` is empty when none was given. */
43
43
  run(number: string, context: {
44
44
  signal: AbortSignal;
45
45
  timeoutMs: number;
46
46
  leads: boolean;
47
+ postcode: string;
47
48
  }): Promise<CarrierResult>;
48
49
  /** The failures of either tier the other may answer after. */
49
50
  recovers(error: unknown): boolean;
@@ -65,9 +66,10 @@ export declare class DPDTracker {
65
66
  private browserService;
66
67
  fetch(raw: string, postcode?: string, context?: TrackingContext): Promise<CarrierResult>;
67
68
  /**
68
- * Whether the guest reply establishes Swiss activity for a 14-digit number.
69
- * The group-wide identity match alone must not promote another country's
70
- * parcel to the Swiss carrier id. Missing country evidence is inconclusive.
69
+ * Whether the guest reply establishes activity in this tracker's country for
70
+ * a 14-digit number. The group-wide identity match alone must not promote
71
+ * another country's parcel to this carrier id. Missing country evidence is
72
+ * inconclusive.
71
73
  * Guest API only, never the page tier. A positive not-found (404) and a
72
74
  * details lookup DPD refuses without the postcode (400, seen for old
73
75
  * parcels) are false; any other failure, a 400 from a token step included,
@@ -558,7 +558,9 @@ export class DPDTracker {
558
558
  const appStep = (leads) => ({
559
559
  id: 'app',
560
560
  ...(leads ? {} : { recovers: appRecovers }),
561
- run: ({ signal, remainingMs }) => app.run(trackingNumber, { signal, timeoutMs: remainingMs, leads }),
561
+ run: ({ signal, remainingMs }) => app.run(trackingNumber, {
562
+ signal, timeoutMs: remainingMs, leads, postcode: resolvedPostcode,
563
+ }),
562
564
  });
563
565
  const result = await runSteps({
564
566
  carrier, budgetMs: lookup.budgetMs, signal: lookup.signal, recorder: this.recorder,
@@ -589,9 +591,10 @@ export class DPDTracker {
589
591
  return result;
590
592
  }
591
593
  /**
592
- * Whether the guest reply establishes Swiss activity for a 14-digit number.
593
- * The group-wide identity match alone must not promote another country's
594
- * parcel to the Swiss carrier id. Missing country evidence is inconclusive.
594
+ * Whether the guest reply establishes activity in this tracker's country for
595
+ * a 14-digit number. The group-wide identity match alone must not promote
596
+ * another country's parcel to this carrier id. Missing country evidence is
597
+ * inconclusive.
595
598
  * Guest API only, never the page tier. A positive not-found (404) and a
596
599
  * details lookup DPD refuses without the postcode (400, seen for old
597
600
  * parcels) are false; any other failure, a 400 from a token step included,
@@ -601,17 +604,20 @@ export class DPDTracker {
601
604
  const trackingNumber = dpdParcelNumber(raw);
602
605
  if (!trackingNumber)
603
606
  return false;
604
- const lookup = lookupBudget(context, this.budgetMs, 'dpd');
607
+ const lookup = lookupBudget(context, this.budgetMs, this.country === 'DE' ? 'dpd-de' : 'dpd');
605
608
  try {
606
609
  const payload = await this.detailsWithFreshToken(trackingNumber, undefined, lookup);
607
610
  if (clean(payload.parcelNumber ?? payload.shipmentId) !== trackingNumber)
608
611
  return false;
609
612
  const current = isRecord(payload.status) ? payload.status : {};
610
613
  const country = known(current.countryCode).toUpperCase();
611
- if (/^[A-Z]{2}$/.test(country) && country !== 'CH')
614
+ if (/^[A-Z]{2}$/.test(country) && country !== this.country)
612
615
  return false;
613
- if (country !== 'CH')
614
- throw new IndeterminateError('DPD Switzerland', 'DPD returned no Swiss country evidence');
616
+ if (country !== this.country) {
617
+ throw this.country === 'DE'
618
+ ? new IndeterminateError('DPD Germany', 'DPD returned no German country evidence')
619
+ : new IndeterminateError('DPD Switzerland', 'DPD returned no Swiss country evidence');
620
+ }
615
621
  requireGuestActivity(payload, parseDPDTrackingApi(payload, trackingNumber));
616
622
  return true;
617
623
  }
@@ -13,8 +13,8 @@ export const adapter = (environment) => {
13
13
  recorder: environment.recorder,
14
14
  userAgent: environment.userAgent,
15
15
  app: {
16
- run: (number, { signal, timeoutMs, leads }) => app.track(number, {
17
- signal, timeoutMs, sessionWaitMs: leads ? Math.max(0, timeoutMs - GUEST_RESERVE_MS) : timeoutMs,
16
+ run: (number, { signal, timeoutMs, leads, postcode }) => app.track(number, {
17
+ signal, timeoutMs, postcode, sessionWaitMs: leads ? Math.max(0, timeoutMs - GUEST_RESERVE_MS) : timeoutMs,
18
18
  }),
19
19
  // Each service has its own host, limits and outages, so either answers when
20
20
  // the other fails. Both read the same parcel: neither can place another
@@ -26,5 +26,7 @@ export const adapter = (environment) => {
26
26
  return {
27
27
  id: 'dpd-de', recordsSteps: true, steps: ['app', 'direct'],
28
28
  track: (input, context) => tracker.fetch(input.number, input.postcode ?? '', context),
29
+ // The guest API's current country, as for Switzerland: the app's session takes too long to open.
30
+ recognize: async (number, context) => ({ known: await tracker.recognizes(number, context) }),
29
31
  };
30
32
  };
@@ -30,11 +30,16 @@ export declare class DpdDeAppClient {
30
30
  userAgent?: string;
31
31
  now?: () => number;
32
32
  });
33
- /** `sessionWaitMs` bounds the wait for a session still opening; the whole `timeoutMs` by default. */
33
+ /**
34
+ * `sessionWaitMs` bounds the wait for a session still opening; the whole `timeoutMs` by default.
35
+ * DPD checks a `postcode` against the recipient's: a rejected one gets one lookup without it, and
36
+ * the result says which, as on the guest API.
37
+ */
34
38
  track(number: string, options: {
35
39
  signal: AbortSignal;
36
40
  timeoutMs: number;
37
41
  sessionWaitMs?: number;
42
+ postcode?: string;
38
43
  }): Promise<CarrierResult>;
39
44
  /**
40
45
  * One session for every lookup. The opening runs while a lookup that asked for it is still
@@ -56,6 +56,13 @@ const elements = (fields) => Object.entries(fields)
56
56
  .map(([name, value]) => `<${name}>${typeof value === 'string' ? escaped(value) : elements(value)}</${name}>`).join('');
57
57
  class SessionExpired extends Error {
58
58
  }
59
+ class PostcodeRejected extends Error {
60
+ }
61
+ /** A two-letter country, where the service puts a placeholder three-letter one for an unknown depot. */
62
+ function countryOf(node) {
63
+ const country = node ? scalar(node, 'Country', 8) : '';
64
+ return /^[A-Z]{2}$/.test(country) ? country : '';
65
+ }
59
66
  function statusOf(stage) {
60
67
  if (stage === 'delivered' || stage === 'out_for_delivery' || stage === 'exception')
61
68
  return stage;
@@ -68,16 +75,12 @@ export function parseDpdDeApp(data, scans, number) {
68
75
  const tracking = one(data, 'TrackingData');
69
76
  if (scalar(tracking, 'ParcelNo', 40) !== number)
70
77
  throw new SchemaError('DPD Germany', 'DPD Germany returned a different parcel');
71
- // The service also answers for parcels delivered elsewhere in the group.
72
- const destination = scalar(one(tracking, 'ShipAddress'), 'Country', 8);
73
- if (/^[A-Z]{2}$/.test(destination) && destination !== 'DE') {
74
- throw new IndeterminateError('DPD Germany', 'DPD returned activity in another country', { reason: DPD_DE_OTHER_COUNTRY });
75
- }
76
78
  const rows = children(one(scans, 'TrackingScanList'), 'TrackingScan');
77
79
  if (rows.length > 500)
78
80
  invalid();
79
81
  const events = [];
80
82
  const seen = new Set();
83
+ let placed = '';
81
84
  for (const row of rows) {
82
85
  const wording = scalar(row, 'StatusText', 500);
83
86
  const day = scalar(row, 'ScanDate', 16);
@@ -88,6 +91,8 @@ export function parseDpdDeApp(data, scans, number) {
88
91
  if (/^Your parcel is estimated to be delivered\b/i.test(wording))
89
92
  continue;
90
93
  const place = /^(.{1,120}) \(([A-Z]{2})\)$/.exec(scalar(row, 'Location'));
94
+ if (place)
95
+ placed = place[2];
91
96
  // German facilities keep German civil time; a clock elsewhere has no established zone.
92
97
  const clock = zonedTime(`${day} ${time}`, 'dd.MM.yyyy HH:mm', place?.[2] === 'DE' ? 'Europe/Berlin' : 'UTC');
93
98
  if (!clock)
@@ -108,6 +113,14 @@ export function parseDpdDeApp(data, scans, number) {
108
113
  }
109
114
  if (!events.length)
110
115
  throw new IndeterminateError('DPD Germany', 'DPD Germany returned no parcel scans');
116
+ // The service also answers for parcels elsewhere in the group, and its recipient address reads
117
+ // Germany for them too. Where the parcel is now, as the guest API's current country: the newest
118
+ // placed scan, else the depot of the last status.
119
+ const depot = children(tracking, 'LastStatusInfo').flatMap(info => children(info, 'DepotData')).flatMap(data => children(data, 'Address'))[0];
120
+ const country = placed || countryOf(depot);
121
+ if (country && country !== 'DE') {
122
+ throw new IndeterminateError('DPD Germany', 'DPD returned activity in another country', { reason: DPD_DE_OTHER_COUNTRY });
123
+ }
111
124
  events.reverse();
112
125
  const current = events[0];
113
126
  const rail = DPD_DE_APP_RAIL[scalar(one(tracking, 'LastStatusInfo'), 'StatusID', 64)];
@@ -140,20 +153,46 @@ export class DpdDeAppClient {
140
153
  this.#userAgent = userAgentOf(options.userAgent);
141
154
  this.#now = options.now ?? Date.now;
142
155
  }
143
- /** `sessionWaitMs` bounds the wait for a session still opening; the whole `timeoutMs` by default. */
156
+ /**
157
+ * `sessionWaitMs` bounds the wait for a session still opening; the whole `timeoutMs` by default.
158
+ * DPD checks a `postcode` against the recipient's: a rejected one gets one lookup without it, and
159
+ * the result says which, as on the guest API.
160
+ */
144
161
  async track(number, options) {
145
162
  if (!/^\d{14}$/.test(number))
146
163
  throw new TypeError('DPD Germany app tracking takes 14 digits');
164
+ const postcode = options.postcode ?? '';
165
+ if (postcode && !/^\d{5}$/.test(postcode))
166
+ throw new TypeError('DPD Germany app tracking takes a 5-digit postcode');
147
167
  const deadline = performance.now() + options.timeoutMs;
148
168
  const left = () => Math.max(1, Math.floor(deadline - performance.now()));
149
169
  try {
150
170
  for (let attempt = 0;; attempt += 1) {
151
171
  const session = await this.session(options.signal, Math.min(left(), options.sessionWaitMs ?? Infinity));
172
+ // Read-only: the parcel is neither added to the session nor redirected.
173
+ const tracking = (zip) => this.call('getTrackingData', { SessionToken: session, ParcelNo: number, DeliveryZipCode: zip,
174
+ UpdateNewDeliveryData: 'false', addParcelIfNoTrackingdataAvailable: 'false', ParcelFlowTypeID: 'receiving' }, options.signal, left());
152
175
  try {
153
- const data = await this.call('getTrackingData', { SessionToken: session, ParcelNo: number, DeliveryZipCode: '',
154
- UpdateNewDeliveryData: 'false', addParcelIfNoTrackingdataAvailable: 'false', ParcelFlowTypeID: 'receiving' }, options.signal, left());
155
- const scans = await this.call('getTrackingScanList', { SessionToken: session, ParcelNo: number, DeliveryZipCode: '' }, options.signal, left());
156
- return parseDpdDeApp(data, scans, number);
176
+ let data;
177
+ let verified;
178
+ try {
179
+ data = await tracking(postcode);
180
+ if (postcode)
181
+ verified = ['DeliveryZipCode_isValid', 'Owner'].includes(scalar(one(data, 'TrackingData'), 'DataViewStatus', 40));
182
+ }
183
+ catch (error) {
184
+ // Also the answer for an unknown parcel, which the lookup without it then reports.
185
+ if (!(error instanceof PostcodeRejected))
186
+ throw error;
187
+ data = await tracking('');
188
+ verified = false;
189
+ }
190
+ const scans = await this.call('getTrackingScanList', { SessionToken: session, ParcelNo: number,
191
+ DeliveryZipCode: verified ? postcode : '' }, options.signal, left());
192
+ const result = parseDpdDeApp(data, scans, number);
193
+ if (verified !== undefined)
194
+ result.dpd_postcode_verified = verified;
195
+ return result;
157
196
  }
158
197
  catch (error) {
159
198
  if (!(error instanceof SessionExpired) || attempt)
@@ -255,6 +294,8 @@ export class DpdDeAppClient {
255
294
  }
256
295
  if (codes.includes('ERROR_SESSION_NOT_VALID'))
257
296
  throw new SessionExpired();
297
+ if (codes.includes('ERROR_TRACKING_DELIVERYZIPCODE_NOT_VALID'))
298
+ throw new PostcodeRejected();
258
299
  // "No tracking data" also answers for parcels the scan list still knows: it proves no absence.
259
300
  if (scalar(result, 'Ack', 8) !== 'true')
260
301
  throw new IndeterminateError('DPD Germany', 'DPD Germany returned no confirmed parcel');
@@ -21,6 +21,9 @@
21
21
  "app",
22
22
  "direct"
23
23
  ],
24
+ "recognition": {
25
+ "rank": 65
26
+ },
24
27
  "requirements": [
25
28
  {
26
29
  "field": "postcode",
@@ -4431,7 +4431,8 @@
4431
4431
  "help": "Optional. DPD uses it to unlock verified scans and the delivery window.",
4432
4432
  "validator": "germanyPostcode"
4433
4433
  }
4434
- ]
4434
+ ],
4435
+ "recognitionRank": 65
4435
4436
  },
4436
4437
  "canaryUrl": "https://tracking.dpd.de/status/en_US/parcel/",
4437
4438
  "trackingUrlTemplate": "https://tracking.dpd.de/status/en_US/parcel/{trackingNumber}",
@@ -2980,6 +2980,7 @@ export declare const CARRIER_CATALOG: {
2980
2980
  readonly help: "Optional. DPD uses it to unlock verified scans and the delivery window.";
2981
2981
  readonly validator: "germanyPostcode";
2982
2982
  }];
2983
+ readonly recognitionRank: 65;
2983
2984
  };
2984
2985
  readonly canaryUrl: "https://tracking.dpd.de/status/en_US/parcel/";
2985
2986
  readonly trackingUrlTemplate: "https://tracking.dpd.de/status/en_US/parcel/{trackingNumber}";
@@ -4432,7 +4432,8 @@ export const CARRIER_CATALOG = {
4432
4432
  "help": "Optional. DPD uses it to unlock verified scans and the delivery window.",
4433
4433
  "validator": "germanyPostcode"
4434
4434
  }
4435
- ]
4435
+ ],
4436
+ "recognitionRank": 65
4436
4437
  },
4437
4438
  "canaryUrl": "https://tracking.dpd.de/status/en_US/parcel/",
4438
4439
  "trackingUrlTemplate": "https://tracking.dpd.de/status/en_US/parcel/{trackingNumber}",
@@ -20,6 +20,7 @@ export const CARRIER_RECOGNITION_RANKS = {
20
20
  "dhl-ecommerce-uk": 17,
21
21
  "dhl-express": 75,
22
22
  "dpd": 70,
23
+ "dpd-de": 65,
23
24
  "four-px": 13,
24
25
  "gls-ch": 60,
25
26
  "gls-de": 50,
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.36.0"
5
+ "version": "0.37.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.36.0",
3
+ "version": "0.37.0-main.389",
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",