universal-parcel-scraper 0.43.1-main.403 → 0.44.0-main.411

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.
Files changed (36) hide show
  1. package/README.md +1 -1
  2. package/data/catalog.json +4 -4
  3. package/data/detection-golden.json +43 -0
  4. package/dist/browser/scraper.js +18 -13
  5. package/dist/carriers/chronopost/carrier.json +4 -4
  6. package/dist/carriers/chronopost/numbers.json +56 -0
  7. package/dist/carriers/chronopost/parser.js +29 -3
  8. package/dist/carriers/dpd/adapter.js +39 -2
  9. package/dist/carriers/intl-post/carrier.json +1 -1
  10. package/dist/carriers/intl-post/numbers.json +18 -0
  11. package/dist/carriers/la-poste/carrier.json +1 -1
  12. package/dist/carriers/paack/adapter.d.ts +2 -2
  13. package/dist/carriers/paack/adapter.js +88 -44
  14. package/dist/carriers/paack/status.d.ts +10 -0
  15. package/dist/carriers/paack/status.js +27 -14
  16. package/dist/carriers/paack/statuses.json +44 -4
  17. package/dist/carriers/spring-gds/carrier.json +1 -1
  18. package/dist/cli/index.js +1 -1
  19. package/dist/core/adapter/index.d.ts +1 -1
  20. package/dist/data/catalog.json +4 -4
  21. package/dist/facade/index.d.ts +1 -1
  22. package/dist/generated/catalog.d.ts +4 -4
  23. package/dist/generated/catalog.js +4 -4
  24. package/dist/providers/parcelsapp/adapter.d.ts +1 -1
  25. package/dist/providers/parcelsapp/adapter.js +11 -16
  26. package/dist/providers/parcelsapp/http.d.ts +2 -4
  27. package/dist/providers/parcelsapp/http.js +3 -13
  28. package/dist/providers/plan.d.ts +2 -2
  29. package/dist/providers/plan.js +17 -5
  30. package/dist/providers/shared/hints.d.ts +9 -1
  31. package/dist/providers/shared/hints.js +18 -3
  32. package/dist/providers/shared/scans.js +2 -0
  33. package/dist/providers/universal.d.ts +9 -9
  34. package/dist/providers/universal.js +11 -5
  35. package/dist/server/openapi.json +3 -2
  36. package/package.json +1 -1
package/README.md CHANGED
@@ -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 2,211 such statuses, each filed under one stage.
152
+ The carrier folders record 2,216 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.
package/data/catalog.json CHANGED
@@ -331,7 +331,7 @@
331
331
  ],
332
332
  "detectionRules": [
333
333
  {
334
- "pattern": "^(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}NL$",
334
+ "pattern": "^(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}NL$",
335
335
  "confidence": "high",
336
336
  "checksum": "s10"
337
337
  },
@@ -1090,7 +1090,7 @@
1090
1090
  "confidence": "low"
1091
1091
  },
1092
1092
  {
1093
- "pattern": "^(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}FR$",
1093
+ "pattern": "^(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}FR$",
1094
1094
  "confidence": "high",
1095
1095
  "checksum": "s10"
1096
1096
  }
@@ -1131,7 +1131,7 @@
1131
1131
  ],
1132
1132
  "detectionRules": [
1133
1133
  {
1134
- "pattern": "^(?:PZ|XU|XW|XY)\\d{9}[A-Z]{2}$",
1134
+ "pattern": "^(?:PZ|XR|XT|XU|XW|XY)\\d{9}[A-Z]{2}$",
1135
1135
  "confidence": "high",
1136
1136
  "checksum": "s10"
1137
1137
  },
@@ -1746,7 +1746,7 @@
1746
1746
  "linkRules": [],
1747
1747
  "detectionRules": [
1748
1748
  {
1749
- "pattern": "^(?![CL][A-Z]\\d{9}DE$)(?!AS\\d{9}US$)(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}(?!CA$|CH$|FR$|IN$|NL$|GB$|IE$|BE$|PT$|NZ$|SG$|JP$|KR$|TH$|HK$|MY$|CN$|NO$|BR$|ES$|SE$|DK$|AT$|YP$|CL$)[A-Z]{2}$",
1749
+ "pattern": "^(?![CL][A-Z]\\d{9}DE$)(?!AS\\d{9}US$)(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}(?!CA$|CH$|FR$|IN$|NL$|GB$|IE$|BE$|PT$|NZ$|SG$|JP$|KR$|TH$|HK$|MY$|CN$|NO$|BR$|ES$|SE$|DK$|AT$|YP$|TS$|JF$|CL$)[A-Z]{2}$",
1750
1750
  "confidence": "high",
1751
1751
  "checksum": "s10"
1752
1752
  }
@@ -10876,6 +10876,17 @@
10876
10876
  "thailand-post"
10877
10877
  ]
10878
10878
  },
10879
+ {
10880
+ "input": "RR123456785TS",
10881
+ "carrier": "unknown",
10882
+ "confidence": "low",
10883
+ "candidates": [
10884
+ "chronopost"
10885
+ ],
10886
+ "asked": [
10887
+ "chronopost"
10888
+ ]
10889
+ },
10879
10890
  {
10880
10891
  "input": "RR123456789PL",
10881
10892
  "carrier": "unknown",
@@ -11585,6 +11596,38 @@
11585
11596
  "confidence": "none",
11586
11597
  "candidates": []
11587
11598
  },
11599
+ {
11600
+ "input": "XR123456785TS",
11601
+ "carrier": "chronopost",
11602
+ "confidence": "high",
11603
+ "candidates": [
11604
+ "chronopost"
11605
+ ]
11606
+ },
11607
+ {
11608
+ "input": "XT010847192TS",
11609
+ "carrier": "chronopost",
11610
+ "confidence": "high",
11611
+ "candidates": [
11612
+ "chronopost"
11613
+ ]
11614
+ },
11615
+ {
11616
+ "input": "XT123456785TS",
11617
+ "carrier": "chronopost",
11618
+ "confidence": "high",
11619
+ "candidates": [
11620
+ "chronopost"
11621
+ ]
11622
+ },
11623
+ {
11624
+ "input": "XT335285235FR",
11625
+ "carrier": "chronopost",
11626
+ "confidence": "high",
11627
+ "candidates": [
11628
+ "chronopost"
11629
+ ]
11630
+ },
11588
11631
  {
11589
11632
  "input": "XU123456785FR",
11590
11633
  "carrier": "chronopost",
@@ -423,7 +423,7 @@ var ve = {
423
423
  }
424
424
  ],
425
425
  detectionRules: [{
426
- pattern: "^(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}NL$",
426
+ pattern: "^(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}NL$",
427
427
  confidence: "high",
428
428
  checksum: "s10"
429
429
  }, {
@@ -966,7 +966,7 @@ var ve = {
966
966
  confidence: "low"
967
967
  },
968
968
  {
969
- pattern: "^(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}FR$",
969
+ pattern: "^(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}FR$",
970
970
  confidence: "high",
971
971
  checksum: "s10"
972
972
  }
@@ -1001,7 +1001,7 @@ var ve = {
1001
1001
  }],
1002
1002
  detectionRules: [
1003
1003
  {
1004
- pattern: "^(?:PZ|XU|XW|XY)\\d{9}[A-Z]{2}$",
1004
+ pattern: "^(?:PZ|XR|XT|XU|XW|XY)\\d{9}[A-Z]{2}$",
1005
1005
  confidence: "high",
1006
1006
  checksum: "s10"
1007
1007
  },
@@ -1472,7 +1472,7 @@ var ve = {
1472
1472
  trackingSiteName: "17TRACK",
1473
1473
  linkRules: [],
1474
1474
  detectionRules: [{
1475
- pattern: "^(?![CL][A-Z]\\d{9}DE$)(?!AS\\d{9}US$)(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}(?!CA$|CH$|FR$|IN$|NL$|GB$|IE$|BE$|PT$|NZ$|SG$|JP$|KR$|TH$|HK$|MY$|CN$|NO$|BR$|ES$|SE$|DK$|AT$|YP$|CL$)[A-Z]{2}$",
1475
+ pattern: "^(?![CL][A-Z]\\d{9}DE$)(?!AS\\d{9}US$)(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}(?!CA$|CH$|FR$|IN$|NL$|GB$|IE$|BE$|PT$|NZ$|SG$|JP$|KR$|TH$|HK$|MY$|CN$|NO$|BR$|ES$|SE$|DK$|AT$|YP$|TS$|JF$|CL$)[A-Z]{2}$",
1476
1476
  confidence: "high",
1477
1477
  checksum: "s10"
1478
1478
  }]
@@ -10758,19 +10758,24 @@ var lc = {
10758
10758
  unknown: 2,
10759
10759
  empty: 3,
10760
10760
  excluded: 4
10761
- }, uc = (e, t) => lc[t === "UPU" ? "unknown" : e?.[t] ?? "unknown"];
10762
- function dc(e, t) {
10761
+ };
10762
+ function uc(e, t) {
10763
+ let n = lc[e?.[t] ?? "unknown"];
10764
+ return t === "UPU" ? lc.unknown : t === "Postal Ninja" ? Math.max(n, lc.empty) : n;
10765
+ }
10766
+ var dc = (e) => e === "UPU" ? 2 : +(e === "Postal Ninja");
10767
+ function fc(e, t) {
10763
10768
  let n = (e) => e !== "UPU" && t?.[e] === "excluded";
10764
- return (e.some((e) => e !== "UPU" && !n(e)) ? e.filter((e) => !n(e)) : [...e]).sort((n, r) => Number(n === "UPU") - Number(r === "UPU") || uc(t, n) - uc(t, r) || Number(oc.has(n)) - Number(oc.has(r)) || e.indexOf(n) - e.indexOf(r));
10769
+ return (e.some((e) => e !== "UPU" && !n(e)) ? e.filter((e) => !n(e)) : [...e]).sort((n, r) => dc(n) - dc(r) || uc(t, n) - uc(t, r) || Number(oc.has(n)) - Number(oc.has(r)) || e.indexOf(n) - e.indexOf(r));
10765
10770
  }
10766
- function fc(e = {}) {
10771
+ function pc(e = {}) {
10767
10772
  let { trackingNumber: t, enablePostalNinja: n = !1 } = e, r = (n ? [
10768
10773
  "ParcelsApp",
10769
10774
  "Ship24",
10770
- "Postal Ninja",
10771
10775
  "17TRACK",
10776
+ "Postal Ninja",
10772
10777
  "UPU"
10773
- ] : [...ac]).filter((e) => e !== "UPU" || t === void 0 || b(t)), i = (e.carriers ?? []).find((e) => ic(e)) ?? null, a = ic(i), o = dc(r, a), s = cc(t);
10778
+ ] : [...ac]).filter((e) => e !== "UPU" || t === void 0 || b(t)), i = (e.carriers ?? []).find((e) => ic(e)) ?? null, a = ic(i), o = fc(r, a), s = cc(t);
10774
10779
  return {
10775
10780
  sources: s ? [s, ...o.filter((e) => e !== s)] : o,
10776
10781
  carrier: i,
@@ -10778,11 +10783,11 @@ function fc(e = {}) {
10778
10783
  rank: (e) => uc(a, e)
10779
10784
  };
10780
10785
  }
10781
- function pc(e = !1, t) {
10782
- return fc({
10786
+ function mc(e = !1, t) {
10787
+ return pc({
10783
10788
  trackingNumber: t,
10784
10789
  enablePostalNinja: e
10785
10790
  }).sources;
10786
10791
  }
10787
10792
  //#endregion
10788
- export { Se as AMAZON_NUMBER_PATTERN, Ne as AUTOMATIC_CARRIER_IDS, oc as BROWSER_SOURCES, f as BudgetExceededError, Ve as CARRIERS, ve as CARRIER_CATALOG, v as CARRIER_DEFINITIONS, ye as CARRIER_IDS, Pe as CARRIER_NAMES, t as CarrierError, a as ChallengeError, No as EXPLICIT_OFFSET_PATTERN, r as IndeterminateError, l as InputRequiredError, u as InvalidInputError, ys as MAX_RECOGNITIONS, s as MaintenanceError, i as NoHistoryError, n as NotFoundError, o as RateLimitedError, Ue as SELECTABLE_CARRIERS, xe as STAGES, c as SchemaError, St as TRACKING_CANDIDATE_PATTERNS, Fe as TRACKING_LINK_RULES, d as TransportError, ac as UNIVERSAL_SOURCES, m as UpstreamHttpError, ee as UpstreamNetworkError, je as activeRequirements, Te as amazonMarketplace, Ee as amazonOrdersUrl, De as amazonShippingOrigin, Oe as amazonShippingUrl, os as calendarDay, es as canadaProvinceTimeZone, Ae as carrierAdapter, y as carrierDefinition, h as carrierErrorKind, He as carrierInfo, Ie as carrierLinkRules, We as carrierRequirements, ke as carrierTimezone, _t as checksumRejections, us as classifyStage, Ut as classifyWording, Jo as countryCode, Yo as countryTimeZone, Is as deliveryHandoff, vt as detectCarrier, x as detectCarrierMatch, is as epochMillisTime, as as epochSecondsTime, ne as errorTypeOf, Rt as eventPoint, Fo as explicitOffsetTime, hs as failureHint, ms as failureKind, _e as formatTrackingNumber, Fs as hasDirectHandoffAdapter, ds as inferStage, Ce as isAmazonTrackingNumber, ge as isPlanzerSharedTrackingNumber, Xe as isValidCorreosSpainCheckLetter, ae as isValidDhlExpressWaybill, $e as isValidDpdParcelNumber, at as isValidGlsParcelNumber, ot as isValidHermesParcelNumber, he as isValidMondialRelayBarcode, pe as isValidPocztaPolskaBarcode, b as isValidS10TrackingNumber, me as isValidSscc, se as isValidTntConsignmentNumber, dt as isValidUspsPackageBarcode, Lo as isoTime, Et as keywordNumberInText, Ht as languageStageStatus, qe as localizedCarrierUrl, Le as matchesDomain, zo as mislabeledLocalTime, Bo as mislabeledWallTime, Ys as normalizeCarrierInputs, zt as normalizeCarrierResult, Xs as normalizeDeliveryPostcode, _ as normalizeTrackingNumber, dc as orderUniversalSources, Mt as parseTrackingInput, cc as priorityUniversalSource, xs as recognitionCandidates, bs as recognitionNumberShape, Cs as recognizeAll, Ct as recognizedNumberInText, rs as regionHasTown, Me as requiredRequirements, Ge as requirementSatisfied, we as requiresAmazonAccount, ps as resolveResult, ls as resultHasUpdate, cs as resultStage, g as retryAfterMsOf, Uo as settleGuessedClocks, Ts as settleRecognition, Vo as sharedClockZone, fs as stageSource, st as supportsSwissPostHandoff, Xo as timeZoneCountry, ts as townKey, Vt as trackingLanguageStage, ze as trackingLink, Re as trackingNumberForLink, Ke as tracksAutomatically, fc as universalPlan, sc as universalSourceBudget, pc as universalSources, Qo as usStateTimeZone, bt as validTrackingNumber, Ks as validateDachserTrackingUrl, Gs as validatePlanzerSharedUrl, Wt as wordingStage, Io as zonedTime };
10793
+ export { Se as AMAZON_NUMBER_PATTERN, Ne as AUTOMATIC_CARRIER_IDS, oc as BROWSER_SOURCES, f as BudgetExceededError, Ve as CARRIERS, ve as CARRIER_CATALOG, v as CARRIER_DEFINITIONS, ye as CARRIER_IDS, Pe as CARRIER_NAMES, t as CarrierError, a as ChallengeError, No as EXPLICIT_OFFSET_PATTERN, r as IndeterminateError, l as InputRequiredError, u as InvalidInputError, ys as MAX_RECOGNITIONS, s as MaintenanceError, i as NoHistoryError, n as NotFoundError, o as RateLimitedError, Ue as SELECTABLE_CARRIERS, xe as STAGES, c as SchemaError, St as TRACKING_CANDIDATE_PATTERNS, Fe as TRACKING_LINK_RULES, d as TransportError, ac as UNIVERSAL_SOURCES, m as UpstreamHttpError, ee as UpstreamNetworkError, je as activeRequirements, Te as amazonMarketplace, Ee as amazonOrdersUrl, De as amazonShippingOrigin, Oe as amazonShippingUrl, os as calendarDay, es as canadaProvinceTimeZone, Ae as carrierAdapter, y as carrierDefinition, h as carrierErrorKind, He as carrierInfo, Ie as carrierLinkRules, We as carrierRequirements, ke as carrierTimezone, _t as checksumRejections, us as classifyStage, Ut as classifyWording, Jo as countryCode, Yo as countryTimeZone, Is as deliveryHandoff, vt as detectCarrier, x as detectCarrierMatch, is as epochMillisTime, as as epochSecondsTime, ne as errorTypeOf, Rt as eventPoint, Fo as explicitOffsetTime, hs as failureHint, ms as failureKind, _e as formatTrackingNumber, Fs as hasDirectHandoffAdapter, ds as inferStage, Ce as isAmazonTrackingNumber, ge as isPlanzerSharedTrackingNumber, Xe as isValidCorreosSpainCheckLetter, ae as isValidDhlExpressWaybill, $e as isValidDpdParcelNumber, at as isValidGlsParcelNumber, ot as isValidHermesParcelNumber, he as isValidMondialRelayBarcode, pe as isValidPocztaPolskaBarcode, b as isValidS10TrackingNumber, me as isValidSscc, se as isValidTntConsignmentNumber, dt as isValidUspsPackageBarcode, Lo as isoTime, Et as keywordNumberInText, Ht as languageStageStatus, qe as localizedCarrierUrl, Le as matchesDomain, zo as mislabeledLocalTime, Bo as mislabeledWallTime, Ys as normalizeCarrierInputs, zt as normalizeCarrierResult, Xs as normalizeDeliveryPostcode, _ as normalizeTrackingNumber, fc as orderUniversalSources, Mt as parseTrackingInput, cc as priorityUniversalSource, xs as recognitionCandidates, bs as recognitionNumberShape, Cs as recognizeAll, Ct as recognizedNumberInText, rs as regionHasTown, Me as requiredRequirements, Ge as requirementSatisfied, we as requiresAmazonAccount, ps as resolveResult, ls as resultHasUpdate, cs as resultStage, g as retryAfterMsOf, Uo as settleGuessedClocks, Ts as settleRecognition, Vo as sharedClockZone, fs as stageSource, st as supportsSwissPostHandoff, Xo as timeZoneCountry, ts as townKey, Vt as trackingLanguageStage, ze as trackingLink, Re as trackingNumberForLink, Ke as tracksAutomatically, pc as universalPlan, sc as universalSourceBudget, mc as universalSources, Qo as usStateTimeZone, bt as validTrackingNumber, Ks as validateDachserTrackingUrl, Gs as validatePlanzerSharedUrl, Wt as wordingStage, Io as zonedTime };
@@ -28,6 +28,7 @@
28
28
  "capabilities": [
29
29
  "history",
30
30
  "location",
31
+ "eta",
31
32
  "provider_code",
32
33
  "delivery_tracking_number"
33
34
  ],
@@ -43,11 +44,10 @@
43
44
  "scraped": [
44
45
  "status",
45
46
  "history",
46
- "location"
47
- ],
48
- "discarded": [
47
+ "location",
49
48
  "eta"
50
49
  ],
50
+ "discarded": [],
51
51
  "unavailable": []
52
52
  },
53
53
  "links": [
@@ -67,7 +67,7 @@
67
67
  "detection": [
68
68
  {
69
69
  "id": "chronopost-1",
70
- "pattern": "^(?:PZ|XU|XW|XY)\\d{9}[A-Z]{2}$",
70
+ "pattern": "^(?:PZ|XR|XT|XU|XW|XY)\\d{9}[A-Z]{2}$",
71
71
  "confidence": "high",
72
72
  "checksum": "s10"
73
73
  },
@@ -109,6 +109,62 @@
109
109
  },
110
110
  "note": "Made-up number that follows the published format."
111
111
  },
112
+ {
113
+ "number": "XR123456785TS",
114
+ "role": "shipment",
115
+ "evidence": "synthetic",
116
+ "derivedFrom": "invented",
117
+ "shapeConfirmed": "2026-10-08",
118
+ "expect": {
119
+ "carrier": "chronopost",
120
+ "confidence": "high"
121
+ },
122
+ "note": "Made-up number in the shape of Chronopost parcels whose S10 check digit passes and whose TS suffix is no country."
123
+ },
124
+ {
125
+ "number": "XT123456785TS",
126
+ "role": "shipment",
127
+ "evidence": "synthetic",
128
+ "derivedFrom": "invented",
129
+ "shapeConfirmed": "2026-10-08",
130
+ "expect": {
131
+ "carrier": "chronopost",
132
+ "confidence": "high"
133
+ },
134
+ "note": "Made-up number in the shape of Chronopost parcels whose S10 check digit passes and whose TS suffix is no country."
135
+ },
136
+ {
137
+ "number": "XT010847192TS",
138
+ "role": "shipment",
139
+ "evidence": "public_shipment_report",
140
+ "source": {
141
+ "url": "https://suivi-de-colis.fr/chronopost/",
142
+ "access": "full_page",
143
+ "date": "2020-06-04",
144
+ "dateKind": "comment publication"
145
+ },
146
+ "expect": {
147
+ "carrier": "chronopost",
148
+ "confidence": "high"
149
+ },
150
+ "note": "Parcel number a reader posted under a third-party Chronopost tracking page; the attribution was not independently verified."
151
+ },
152
+ {
153
+ "number": "XT335285235FR",
154
+ "role": "shipment",
155
+ "evidence": "public_shipment_report",
156
+ "source": {
157
+ "url": "https://suivi-de-colis.fr/chronopost/",
158
+ "access": "full_page",
159
+ "date": "2020-06-18",
160
+ "dateKind": "comment publication"
161
+ },
162
+ "expect": {
163
+ "carrier": "chronopost",
164
+ "confidence": "high"
165
+ },
166
+ "note": "Parcel number a reader posted under a third-party Chronopost tracking page, describing a Chronopost agency; the attribution was not independently verified."
167
+ },
112
168
  {
113
169
  "number": "XW295445484JF",
114
170
  "role": "shipment",
@@ -2,7 +2,7 @@ import { DateTime } from 'luxon';
2
2
  import { isValidDpdParcelNumber } from '../../core/detection/dpd.js';
3
3
  import { ChallengeError, IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
4
4
  import { languageStageStatus } from '../../core/status/index.js';
5
- import { countryCode, explicitOffsetTime } from '../../core/time/index.js';
5
+ import { calendarDay, countryCode, explicitOffsetTime } from '../../core/time/index.js';
6
6
  import { clean } from '../../core/transport/index.js';
7
7
  import { xmlDocument } from '../../core/transport/xml.js';
8
8
  import { chronopostStage, isChronopostNotification } from './status.js';
@@ -45,6 +45,18 @@ function clock(raw) {
45
45
  }
46
46
  return raw ? { provider_time_text: clean(raw, 64) } : {};
47
47
  }
48
+ /** The recipient's chosen redelivery day, printed as dd/MM/yyyy. */
49
+ function redeliveryDay(value) {
50
+ const match = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(value);
51
+ return match ? calendarDay(Number(match[3]), Number(match[2]), Number(match[1])) : null;
52
+ }
53
+ /** The calendar day of a scan's own clock, or '' without one. */
54
+ const clockDay = (event) => {
55
+ const clock = event.time ?? event.local_time;
56
+ return typeof clock === 'string' ? /^\d{4}-\d{2}-\d{2}/.exec(clock)?.[0] ?? '' : '';
57
+ };
58
+ /** Stages on the way to a planned delivery. */
59
+ const ONGOING = new Set(['pending', 'registered', 'accepted', 'in_transit', 'customs', 'out_for_delivery']);
48
60
  /** Keep operational places, never the delivery-point address or shop contact block. */
49
61
  function location(office, place) {
50
62
  if (/^[\p{L}\p{M} .,'’()-]{1,100} - [A-Z]{2}(?: \(depot \d{1,6}\))?$/u.test(place))
@@ -95,11 +107,14 @@ export function parseChronopostTrackingXml(xml, rawNumber) {
95
107
  throw new SchemaError('Chronopost', 'Chronopost returned an empty tracking event');
96
108
  const extras = children(scan, 'infoCompList');
97
109
  let place = '';
110
+ let redelivery;
98
111
  for (const extra of extras) {
99
112
  const name = scalar(extra, 'name');
100
113
  const value = scalar(extra, 'value');
101
114
  if (name === 'Lieu')
102
115
  place = value;
116
+ if (name === 'Date de relivraison')
117
+ redelivery = redeliveryDay(value);
103
118
  if (name === 'Numéro partenaire') {
104
119
  const match = /^(?:GEO\/)?([A-Z0-9]{4,40})$/.exec(value.toUpperCase());
105
120
  if (match) {
@@ -122,7 +137,7 @@ export function parseChronopostTrackingXml(xml, rawNumber) {
122
137
  ...(code ? { provider_code: code } : {}),
123
138
  };
124
139
  const mapped = chronopostStage(code, description);
125
- return { event, mapped, notification: isChronopostNotification(description), index };
140
+ return { event, mapped, notification: isChronopostNotification(description), index, redelivery };
126
141
  });
127
142
  // The operation returns oldest first. Reorder only when all scan clocks
128
143
  // establish instants, so mixed local clocks cannot create a guessed order.
@@ -141,6 +156,17 @@ export function parseChronopostTrackingXml(xml, rawNumber) {
141
156
  scan.event.stage = scan.notification ? stage : scan.mapped.stage;
142
157
  scan.event.stage_source = scan.notification ? 'none' : scan.mapped.source;
143
158
  }
159
+ // The newest instruction that names a redelivery day decides, even when its
160
+ // day is unreadable. A later scan that is not progress towards that delivery,
161
+ // or that falls on a later day, ends it.
162
+ let promised = null;
163
+ for (const scan of projected) {
164
+ if (scan.redelivery !== undefined)
165
+ promised = scan.redelivery;
166
+ else if (promised && (clockDay(scan.event) > promised || (!scan.notification && !ONGOING.has(scan.mapped.stage)))) {
167
+ promised = null;
168
+ }
169
+ }
144
170
  const events = projected.reverse().slice(0, 100).map(scan => scan.event);
145
171
  const latest = events[0];
146
172
  const reference = references.size === 1 ? [...references][0] : undefined;
@@ -154,7 +180,7 @@ export function parseChronopostTrackingXml(xml, rawNumber) {
154
180
  current_stage_source: stageSource,
155
181
  last_status_text: latest.description,
156
182
  last_update: latest.time ?? null,
157
- expected_delivery: null,
183
+ expected_delivery: ['delivered', 'returned', 'ready_for_pickup'].includes(stage) ? null : promised,
158
184
  ...(reference ? { delivery_tracking_number: reference } : {}),
159
185
  ...(partner ? { delivery_carrier: partner } : {}),
160
186
  ...(destination ? { destination_country: destination } : {}),
@@ -6,7 +6,7 @@ import { DELIVERY_POSTCODE, deliveryPostcodeText } from '../../core/catalog/post
6
6
  import { dpdParcelNumber } from '../../core/detection/dpd.js';
7
7
  import { BudgetExceededError, carrierErrorKind, ChallengeError, IndeterminateError, InvalidInputError, NotFoundError, SchemaError, UpstreamHttpError } from '../../core/errors/index.js';
8
8
  import { runSteps } from '../../core/runner/index.js';
9
- import { isoTime, explicitOffsetTime, zonedTime } from '../../core/time/index.js';
9
+ import { calendarDay, isoTime, explicitOffsetTime, zonedTime } from '../../core/time/index.js';
10
10
  import { TrawlClient, decodeText, fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
11
11
  import { isRecord } from '../../core/types.js';
12
12
  import { API_LABELS, PROOF_OF_DELIVERY_SCAN, apiStage, apiStatus, scanStage, wordingStatus, } from './status.js';
@@ -254,6 +254,32 @@ function expectedDelivery(payload) {
254
254
  return `${date} ${from}–${to}`;
255
255
  return from || to ? `${date} ${from || to}` : date;
256
256
  }
257
+ /** Scans in which DPD states a delivery day: an estimate, a changed date or the email notice. */
258
+ const DELIVERY_NOTICE = /\b(?:estimated to be|will be) delivered on\b/i;
259
+ const MONTHS = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september',
260
+ 'october', 'november', 'december'];
261
+ /** A notice clock as 24-hour `HH:mm`; it stays the recipient's wall clock. */
262
+ function noticeClock(text) {
263
+ const match = /^(\d{1,2}):([0-5]\d)(?:\s*([AP])M)?$/i.exec(text ?? '');
264
+ if (!match)
265
+ return null;
266
+ const hour = Number(match[1]);
267
+ const half = match[3]?.toUpperCase();
268
+ if (half ? hour < 1 || hour > 12 : hour > 23)
269
+ return null;
270
+ return `${String(half ? (hour % 12) + (half === 'P' ? 12 : 0) : hour).padStart(2, '0')}:${match[2]}`;
271
+ }
272
+ /** "…delivered on: Thursday, July 16, 2026", optionally followed by "between 9:00 AM and 12:00 PM". */
273
+ function noticeEstimate(text) {
274
+ const match = /\bdelivered on:?\s+(?:[a-z]+,\s*)?([a-z]+)\s+(\d{1,2}),\s*(\d{4})(?:\s+between\s+(\d{1,2}:\d{2}(?:\s*[AP]M)?)\s+and\s+(\d{1,2}:\d{2}(?:\s*[AP]M)?))?/i.exec(text);
275
+ const month = match ? MONTHS.indexOf(match[1].toLowerCase()) + 1 : 0;
276
+ const day = match && month ? calendarDay(Number(match[3]), month, Number(match[2])) : null;
277
+ if (!match || !day)
278
+ return null;
279
+ const from = noticeClock(match[4]);
280
+ const to = noticeClock(match[5]);
281
+ return from && to && from <= to ? `${day} ${from}–${to}` : day;
282
+ }
257
283
  /**
258
284
  * Newest scan first. The guest API has listed a parcel's scans oldest first,
259
285
  * and the summary and freshness watermark read the first event. Equal or
@@ -358,6 +384,16 @@ export function parseDPDTrackingApi(payload, trackingNumber, postcodeVerified) {
358
384
  const deliveredAt = status === 'delivered'
359
385
  ? events.find((event) => event.stage === 'delivered')?.time
360
386
  : undefined;
387
+ // Only the newest notice counts, so an undated or unreadable one leaves no
388
+ // older day behind. A day that a later scan has passed, or a parcel waiting
389
+ // at a pickup point, ends the notice. The payload's own estimate wins; a
390
+ // notice for that same day can only add the email's window.
391
+ const notice = events.find((event) => DELIVERY_NOTICE.test(event.description ?? ''));
392
+ const announced = notice ? noticeEstimate(notice.description ?? '') : null;
393
+ const newestDay = /^\d{4}-\d{2}-\d{2}/.exec(events[0]?.time ?? '')?.[0];
394
+ const forecast = announced && stage !== 'ready_for_pickup' && !(newestDay && announced.slice(0, 10) < newestDay)
395
+ ? announced : null;
396
+ const listed = expectedDelivery(payload);
361
397
  // Never projected: `receiver` (name, contact, address, geoPosition), the
362
398
  // sender's id and address, `customerReference1/2`, `gttsZipCode`, `podUrl`
363
399
  // and `product`, which holds the recipient's delivery preference.
@@ -366,7 +402,8 @@ export function parseDPDTrackingApi(payload, trackingNumber, postcodeVerified) {
366
402
  ...(stage ? { current_stage: stage } : {}),
367
403
  last_status_text: statusText,
368
404
  last_update: events[0]?.time || apiEventTime(current.eventDateAndTime, '', current.eventDateAndTimeZoneId) || null,
369
- expected_delivery: status === 'delivered' || status === 'exception' ? null : expectedDelivery(payload),
405
+ expected_delivery: status === 'delivered' || status === 'exception' ? null
406
+ : listed && listed !== forecast?.slice(0, 10) ? listed : forecast ?? listed,
370
407
  events,
371
408
  source: 'mydpd_guest_api',
372
409
  delivery_date: optionalText(payload.deliveryDate),
@@ -38,7 +38,7 @@
38
38
  "detection": [
39
39
  {
40
40
  "id": "intl-post-1",
41
- "pattern": "^(?![CL][A-Z]\\d{9}DE$)(?!AS\\d{9}US$)(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}(?!CA$|CH$|FR$|IN$|NL$|GB$|IE$|BE$|PT$|NZ$|SG$|JP$|KR$|TH$|HK$|MY$|CN$|NO$|BR$|ES$|SE$|DK$|AT$|YP$|CL$)[A-Z]{2}$",
41
+ "pattern": "^(?![CL][A-Z]\\d{9}DE$)(?!AS\\d{9}US$)(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}(?!CA$|CH$|FR$|IN$|NL$|GB$|IE$|BE$|PT$|NZ$|SG$|JP$|KR$|TH$|HK$|MY$|CN$|NO$|BR$|ES$|SE$|DK$|AT$|YP$|TS$|JF$|CL$)[A-Z]{2}$",
42
42
  "confidence": "high",
43
43
  "checksum": "s10"
44
44
  }
@@ -24,6 +24,24 @@
24
24
  "confidence": "high"
25
25
  },
26
26
  "note": "Made-up number that follows the published format."
27
+ },
28
+ {
29
+ "number": "RR123456785TS",
30
+ "role": "negative",
31
+ "evidence": "synthetic",
32
+ "derivedFrom": "invented",
33
+ "shapeConfirmed": "2026-10-08",
34
+ "expect": {
35
+ "carrier": "unknown",
36
+ "confidence": "low",
37
+ "candidates": [
38
+ "chronopost"
39
+ ],
40
+ "not": [
41
+ "intl-post"
42
+ ]
43
+ },
44
+ "note": "Invented number whose S10 check digit passes but whose TS suffix is a Chronopost code rather than an issuing country, so it is not international mail."
27
45
  }
28
46
  ]
29
47
  }
@@ -96,7 +96,7 @@
96
96
  },
97
97
  {
98
98
  "id": "la-poste-3",
99
- "pattern": "^(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}FR$",
99
+ "pattern": "^(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}FR$",
100
100
  "confidence": "high",
101
101
  "checksum": "s10"
102
102
  }
@@ -3,8 +3,8 @@ import type { CarrierResult } from '../../core/result/index.js';
3
3
  export declare function normalizePaackTrackingNumber(raw: string): string;
4
4
  export declare function normalizePaackPostcode(raw: string): string;
5
5
  export declare function paackTrackingUrl(rawTrackingNumber: string, rawPostcode: string): string;
6
- export declare function parsePaackTrackingResponse(payload: unknown, rawTrackingNumber: string): CarrierResult;
7
- export declare function parsePaackTrackingHtml(html: string, rawTrackingNumber: string): CarrierResult;
6
+ export declare function parsePaackTrackingResponse(payload: unknown, rawPostcode: string): CarrierResult;
7
+ export declare function parsePaackTrackingHtml(html: string, rawPostcode: string): CarrierResult;
8
8
  export interface PaackTrackerOptions {
9
9
  timeoutMs?: number;
10
10
  /** Test seam; production uses the global fetch. */