universal-parcel-scraper 0.40.0-main.395 → 0.41.0-main.397
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 +2 -2
- package/data/catalog.json +14 -15
- package/data/coverage-summary.json +1 -1
- package/dist/browser/scraper.js +28 -27
- package/dist/carriers/chronopost/adapter.d.ts +13 -0
- package/dist/carriers/chronopost/adapter.js +40 -0
- package/dist/carriers/chronopost/carrier.json +9 -8
- package/dist/carriers/chronopost/parser.d.ts +5 -0
- package/dist/carriers/chronopost/parser.js +163 -0
- package/dist/carriers/chronopost/status.d.ts +4 -0
- package/dist/carriers/chronopost/status.js +19 -0
- package/dist/carriers/chronopost/statuses.json +56 -0
- package/dist/carriers/dpd/adapter.js +2 -3
- package/dist/carriers/gls-ch/adapter.d.ts +8 -2
- package/dist/carriers/gls-ch/adapter.js +13 -7
- package/dist/carriers/gls-de/adapter.js +5 -4
- package/dist/carriers/gls-de/carrier.json +5 -6
- package/dist/carriers/mondial-relay/adapter.d.ts +8 -0
- package/dist/carriers/mondial-relay/adapter.js +23 -12
- package/dist/carriers/mondial-relay/app.d.ts +1 -1
- package/dist/carriers/mondial-relay/carrier.json +6 -7
- package/dist/core/catalog/inputs.js +12 -13
- package/dist/core/catalog/postcode.d.ts +4 -0
- package/dist/core/catalog/postcode.js +6 -0
- package/dist/data/catalog.json +14 -15
- package/dist/generated/catalog.d.ts +13 -14
- package/dist/generated/catalog.js +14 -15
- package/dist/generated/registry.js +3 -1
- package/dist/server/openapi.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ The engine behind [Peek](https://peektracker.com) ([GitHub](https://github.com/p
|
|
|
13
13
|
[](LICENSE)
|
|
14
14
|
|
|
15
15
|
<!-- GENERATED:summary -->
|
|
16
|
-
**3,500+ carriers** through **
|
|
16
|
+
**3,500+ carriers** through **104 dedicated adapters** and **5 universal fallbacks**
|
|
17
17
|
|
|
18
18
|
<sub>124 catalog entries · 58 countries represented</sub>
|
|
19
19
|
<!-- /GENERATED:summary -->
|
|
@@ -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,
|
|
152
|
+
The carrier folders record 2,211 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
|
@@ -949,15 +949,14 @@
|
|
|
949
949
|
"requirements": [
|
|
950
950
|
{
|
|
951
951
|
"field": "postcode",
|
|
952
|
-
"whenTrackingNumber": "^
|
|
953
|
-
"validator": "
|
|
952
|
+
"whenTrackingNumber": "^[0-9]{8}$",
|
|
953
|
+
"validator": "internationalPostcode",
|
|
954
954
|
"label": "Delivery postcode",
|
|
955
955
|
"type": "text",
|
|
956
|
-
"
|
|
957
|
-
"
|
|
958
|
-
"
|
|
959
|
-
"
|
|
960
|
-
"inputMode": "numeric",
|
|
956
|
+
"help": "8-digit Mondial Relay shipment numbers require the recipient postcode; longer numbers and label barcodes do not.",
|
|
957
|
+
"pattern": "^(?=.{3,12}$)(?=.*[0-9])[A-Za-z0-9]+(?:[ \\-][A-Za-z0-9]+)*$",
|
|
958
|
+
"maxLength": 12,
|
|
959
|
+
"inputMode": "text",
|
|
961
960
|
"autoComplete": "postal-code"
|
|
962
961
|
}
|
|
963
962
|
]
|
|
@@ -1109,8 +1108,9 @@
|
|
|
1109
1108
|
"timezone": "Europe/Paris",
|
|
1110
1109
|
"tracking": {
|
|
1111
1110
|
"mode": "automatic",
|
|
1112
|
-
"adapter": "
|
|
1113
|
-
"recognitionRank": 45
|
|
1111
|
+
"adapter": "chronopost",
|
|
1112
|
+
"recognitionRank": 45,
|
|
1113
|
+
"localClocks": true
|
|
1114
1114
|
},
|
|
1115
1115
|
"canaryUrl": "https://www.chronopost.fr/tracking-no-cms/suivi-page",
|
|
1116
1116
|
"trackingUrlTemplate": "https://www.chronopost.fr/tracking-no-cms/suivi-page?langue=fr&listeNumerosLT={trackingNumber}",
|
|
@@ -1644,14 +1644,13 @@
|
|
|
1644
1644
|
"requirements": [
|
|
1645
1645
|
{
|
|
1646
1646
|
"field": "postcode",
|
|
1647
|
-
"validator": "
|
|
1647
|
+
"validator": "internationalPostcode",
|
|
1648
1648
|
"label": "Delivery postcode",
|
|
1649
1649
|
"type": "text",
|
|
1650
|
-
"
|
|
1651
|
-
"
|
|
1652
|
-
"
|
|
1653
|
-
"
|
|
1654
|
-
"inputMode": "numeric",
|
|
1650
|
+
"help": "GLS requires the delivery postcode to show the detailed event history.",
|
|
1651
|
+
"pattern": "^(?=.{3,12}$)(?=.*[0-9])[A-Za-z0-9]+(?:[ \\-][A-Za-z0-9]+)*$",
|
|
1652
|
+
"maxLength": 12,
|
|
1653
|
+
"inputMode": "text",
|
|
1655
1654
|
"autoComplete": "postal-code"
|
|
1656
1655
|
}
|
|
1657
1656
|
],
|
package/dist/browser/scraper.js
CHANGED
|
@@ -852,15 +852,14 @@ var ve = {
|
|
|
852
852
|
adapter: "mondial-relay",
|
|
853
853
|
requirements: [{
|
|
854
854
|
field: "postcode",
|
|
855
|
-
whenTrackingNumber: "^
|
|
856
|
-
validator: "
|
|
855
|
+
whenTrackingNumber: "^[0-9]{8}$",
|
|
856
|
+
validator: "internationalPostcode",
|
|
857
857
|
label: "Delivery postcode",
|
|
858
858
|
type: "text",
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
inputMode: "numeric",
|
|
859
|
+
help: "8-digit Mondial Relay shipment numbers require the recipient postcode; longer numbers and label barcodes do not.",
|
|
860
|
+
pattern: "^(?=.{3,12}$)(?=.*[0-9])[A-Za-z0-9]+(?:[ \\-][A-Za-z0-9]+)*$",
|
|
861
|
+
maxLength: 12,
|
|
862
|
+
inputMode: "text",
|
|
864
863
|
autoComplete: "postal-code"
|
|
865
864
|
}]
|
|
866
865
|
},
|
|
@@ -984,8 +983,9 @@ var ve = {
|
|
|
984
983
|
timezone: "Europe/Paris",
|
|
985
984
|
tracking: {
|
|
986
985
|
mode: "automatic",
|
|
987
|
-
adapter: "
|
|
988
|
-
recognitionRank: 45
|
|
986
|
+
adapter: "chronopost",
|
|
987
|
+
recognitionRank: 45,
|
|
988
|
+
localClocks: !0
|
|
989
989
|
},
|
|
990
990
|
canaryUrl: "https://www.chronopost.fr/tracking-no-cms/suivi-page",
|
|
991
991
|
trackingUrlTemplate: "https://www.chronopost.fr/tracking-no-cms/suivi-page?langue=fr&listeNumerosLT={trackingNumber}",
|
|
@@ -1390,14 +1390,13 @@ var ve = {
|
|
|
1390
1390
|
adapter: "gls-de",
|
|
1391
1391
|
requirements: [{
|
|
1392
1392
|
field: "postcode",
|
|
1393
|
-
validator: "
|
|
1393
|
+
validator: "internationalPostcode",
|
|
1394
1394
|
label: "Delivery postcode",
|
|
1395
1395
|
type: "text",
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
inputMode: "numeric",
|
|
1396
|
+
help: "GLS requires the delivery postcode to show the detailed event history.",
|
|
1397
|
+
pattern: "^(?=.{3,12}$)(?=.*[0-9])[A-Za-z0-9]+(?:[ \\-][A-Za-z0-9]+)*$",
|
|
1398
|
+
maxLength: 12,
|
|
1399
|
+
inputMode: "text",
|
|
1401
1400
|
autoComplete: "postal-code"
|
|
1402
1401
|
}],
|
|
1403
1402
|
recognitionRank: 50,
|
|
@@ -8939,17 +8938,23 @@ function Ks(e, t) {
|
|
|
8939
8938
|
return r.protocol = "https:", r.hostname = Bs, r.port = "", r.toString();
|
|
8940
8939
|
}
|
|
8941
8940
|
//#endregion
|
|
8941
|
+
//#region core/catalog/postcode.ts
|
|
8942
|
+
var qs = /^(?=.{3,12}$)(?=.*\d)[A-Z0-9]+(?:[ -][A-Z0-9]+)*$/;
|
|
8943
|
+
function Js(e) {
|
|
8944
|
+
return e.trim().toUpperCase().replace(/\s+/g, " ");
|
|
8945
|
+
}
|
|
8946
|
+
//#endregion
|
|
8942
8947
|
//#region core/catalog/inputs.ts
|
|
8943
|
-
function
|
|
8948
|
+
function Ys(e, t, n, r) {
|
|
8944
8949
|
let i = {
|
|
8945
8950
|
trackingUrl: n.trim() || null,
|
|
8946
8951
|
postcode: r.trim() || null
|
|
8947
8952
|
}, a = e === "mondial-relay" && /^\d{26}$/.test(t);
|
|
8948
8953
|
if (a && !he(t)) throw TypeError("Invalid Mondial Relay barcode");
|
|
8949
8954
|
let o = new Map(je(e, t).map((e) => [e.field, e]));
|
|
8950
|
-
a && i.postcode && o.set("postcode", {
|
|
8955
|
+
(a || e === "mondial-relay" && /^(?:\d{10}|\d{12})$/.test(t)) && i.postcode && o.set("postcode", {
|
|
8951
8956
|
field: "postcode",
|
|
8952
|
-
validator: "
|
|
8957
|
+
validator: "internationalPostcode"
|
|
8953
8958
|
});
|
|
8954
8959
|
for (let [e, t] of Object.entries(i)) if (t !== null && !o.has(e)) throw TypeError(e === "trackingUrl" ? "A tracking URL is not used for this carrier or tracking number" : "A delivery postcode is not used for this carrier");
|
|
8955
8960
|
for (let [n, r] of o) {
|
|
@@ -8976,8 +8981,8 @@ function qs(e, t, n, r) {
|
|
|
8976
8981
|
if (!/^\d{4,5}$/.test(a)) throw TypeError(`${y(e).displayName} requires a four- or five-digit delivery postcode`);
|
|
8977
8982
|
break;
|
|
8978
8983
|
case "internationalPostcode":
|
|
8979
|
-
if (!
|
|
8980
|
-
i[n] =
|
|
8984
|
+
if (!qs.test(Js(a))) throw TypeError(`${y(e).displayName} requires a valid delivery postcode`);
|
|
8985
|
+
i[n] = Js(a);
|
|
8981
8986
|
break;
|
|
8982
8987
|
case "paackPostcode": {
|
|
8983
8988
|
let e = a.toLocaleUpperCase("en-US");
|
|
@@ -8990,13 +8995,9 @@ function qs(e, t, n, r) {
|
|
|
8990
8995
|
}
|
|
8991
8996
|
return i;
|
|
8992
8997
|
}
|
|
8993
|
-
var Js = /^(?=.{3,12}$)(?=.*\d)[A-Z0-9]+(?:[ -][A-Z0-9]+)*$/;
|
|
8994
|
-
function Ys(e) {
|
|
8995
|
-
return e.trim().toUpperCase().replace(/\s+/g, " ");
|
|
8996
|
-
}
|
|
8997
8998
|
function Xs(e) {
|
|
8998
|
-
let t =
|
|
8999
|
-
if (!
|
|
8999
|
+
let t = Js(e);
|
|
9000
|
+
if (!qs.test(t)) throw TypeError("A valid delivery postcode is required");
|
|
9000
9001
|
return t;
|
|
9001
9002
|
}
|
|
9002
9003
|
var Zs = { carriers: [
|
|
@@ -10778,4 +10779,4 @@ function pc(e = !1, t) {
|
|
|
10778
10779
|
}).sources;
|
|
10779
10780
|
}
|
|
10780
10781
|
//#endregion
|
|
10781
|
-
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,
|
|
10782
|
+
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 };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type AdapterFactory, type TrackingContext } from '../../core/adapter/index.js';
|
|
2
|
+
import type { CarrierResult } from '../../core/result/index.js';
|
|
3
|
+
import type { StepRecorder } from '../../core/telemetry/index.js';
|
|
4
|
+
export declare class ChronopostTracker {
|
|
5
|
+
private readonly options;
|
|
6
|
+
constructor(options?: {
|
|
7
|
+
fetcher?: typeof fetch;
|
|
8
|
+
recorder?: StepRecorder;
|
|
9
|
+
userAgent?: string;
|
|
10
|
+
});
|
|
11
|
+
fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
|
|
12
|
+
}
|
|
13
|
+
export declare const adapter: AdapterFactory;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
|
|
2
|
+
import { IndeterminateError } from '../../core/errors/index.js';
|
|
3
|
+
import { runSteps } from '../../core/runner/index.js';
|
|
4
|
+
import { decodeText, fetchBounded, userAgentOf } from '../../core/transport/index.js';
|
|
5
|
+
import { CHRONOPOST_MAX_BYTES, CHRONOPOST_NAMESPACE, normalizeChronopostNumber, parseChronopostTrackingXml } from './parser.js';
|
|
6
|
+
// The official WSDL declares this read-only operation without account credentials.
|
|
7
|
+
const ENDPOINT = 'https://ws.chronopost.fr/tracking-cxf/TrackingServiceWS';
|
|
8
|
+
export class ChronopostTracker {
|
|
9
|
+
options;
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.options = options;
|
|
12
|
+
}
|
|
13
|
+
fetch(raw, context = {}) {
|
|
14
|
+
const number = normalizeChronopostNumber(raw);
|
|
15
|
+
return runSteps({ carrier: 'chronopost', budgetMs: context.budgetMs ?? 15_000, signal: context.signal,
|
|
16
|
+
recorder: this.options.recorder }, [{ id: 'direct', run: async ({ signal, remainingMs }) => {
|
|
17
|
+
const { response, bytes } = await fetchBounded(ENDPOINT, {
|
|
18
|
+
method: 'POST', signal,
|
|
19
|
+
headers: { Accept: 'text/xml', 'Content-Type': 'text/xml; charset=utf-8', SOAPAction: '""',
|
|
20
|
+
'User-Agent': userAgentOf(this.options.userAgent) },
|
|
21
|
+
body: `<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:trk="${CHRONOPOST_NAMESPACE}">`
|
|
22
|
+
+ `<soap:Body><trk:trackSkybillV2><language>fr_FR</language><skybillNumber>${number}</skybillNumber>`
|
|
23
|
+
+ '</trk:trackSkybillV2></soap:Body></soap:Envelope>',
|
|
24
|
+
}, { provider: 'Chronopost tracking', timeoutMs: Math.max(1, Math.floor(remainingMs)),
|
|
25
|
+
maxBytes: CHRONOPOST_MAX_BYTES, allowHttpStatuses: [404, 410], fetcher: this.options.fetcher });
|
|
26
|
+
// An HTTP route failure is not the service's positive unknown-parcel reply.
|
|
27
|
+
if ([404, 410].includes(response.status))
|
|
28
|
+
throw new IndeterminateError('Chronopost', 'Chronopost tracking endpoint is unavailable');
|
|
29
|
+
return parseChronopostTrackingXml(decodeText(bytes), number);
|
|
30
|
+
} }]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export const adapter = environment => {
|
|
34
|
+
const tracker = new ChronopostTracker(environment);
|
|
35
|
+
return {
|
|
36
|
+
id: 'chronopost', recordsSteps: true, steps: ['direct'],
|
|
37
|
+
track: (input, context) => tracker.fetch(input.number, context),
|
|
38
|
+
recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizeChronopostNumber(number))),
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -16,20 +16,20 @@
|
|
|
16
16
|
"timezone": "Europe/Paris",
|
|
17
17
|
"tracking": {
|
|
18
18
|
"mode": "automatic",
|
|
19
|
-
"adapter": "
|
|
19
|
+
"adapter": "chronopost",
|
|
20
20
|
"steps": [
|
|
21
|
-
"direct"
|
|
22
|
-
"retry"
|
|
21
|
+
"direct"
|
|
23
22
|
],
|
|
24
23
|
"recognition": {
|
|
25
24
|
"rank": 45
|
|
26
|
-
}
|
|
25
|
+
},
|
|
26
|
+
"localClocks": true
|
|
27
27
|
},
|
|
28
28
|
"capabilities": [
|
|
29
29
|
"history",
|
|
30
30
|
"location",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
31
|
+
"provider_code",
|
|
32
|
+
"delivery_tracking_number"
|
|
33
33
|
],
|
|
34
34
|
"portal": {
|
|
35
35
|
"url": "https://www.chronopost.fr/tracking-no-cms/suivi-page?langue=fr&listeNumerosLT={trackingNumber}",
|
|
@@ -43,10 +43,11 @@
|
|
|
43
43
|
"scraped": [
|
|
44
44
|
"status",
|
|
45
45
|
"history",
|
|
46
|
-
"location"
|
|
46
|
+
"location"
|
|
47
|
+
],
|
|
48
|
+
"discarded": [
|
|
47
49
|
"eta"
|
|
48
50
|
],
|
|
49
|
-
"discarded": [],
|
|
50
51
|
"unavailable": []
|
|
51
52
|
},
|
|
52
53
|
"links": [
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CarrierResult } from '../../core/result/index.js';
|
|
2
|
+
export declare const CHRONOPOST_MAX_BYTES = 2000000;
|
|
3
|
+
export declare const CHRONOPOST_NAMESPACE = "http://cxf.tracking.soap.chronopost.fr/";
|
|
4
|
+
export declare function normalizeChronopostNumber(raw: string): string;
|
|
5
|
+
export declare function parseChronopostTrackingXml(xml: string, rawNumber: string): CarrierResult;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { DateTime } from 'luxon';
|
|
2
|
+
import { isValidDpdParcelNumber } from '../../core/detection/dpd.js';
|
|
3
|
+
import { ChallengeError, IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
|
|
4
|
+
import { languageStageStatus } from '../../core/status/index.js';
|
|
5
|
+
import { countryCode, explicitOffsetTime } from '../../core/time/index.js';
|
|
6
|
+
import { clean } from '../../core/transport/index.js';
|
|
7
|
+
import { xmlDocument } from '../../core/transport/xml.js';
|
|
8
|
+
import { chronopostStage, isChronopostNotification } from './status.js';
|
|
9
|
+
export const CHRONOPOST_MAX_BYTES = 2_000_000;
|
|
10
|
+
const SOAP = 'http://schemas.xmlsoap.org/soap/envelope/';
|
|
11
|
+
export const CHRONOPOST_NAMESPACE = 'http://cxf.tracking.soap.chronopost.fr/';
|
|
12
|
+
const MAX_EVENTS = 500;
|
|
13
|
+
export function normalizeChronopostNumber(raw) {
|
|
14
|
+
const value = raw.toUpperCase().replace(/[\s.-]/g, '');
|
|
15
|
+
if (!/^[A-Z]{2}\d{9}[A-Z]{2}$/.test(value) && !isValidDpdParcelNumber(value)) {
|
|
16
|
+
throw new InvalidInputError('Chronopost', 'Chronopost requires a postal-shaped identifier or a checked 15-character parcel number');
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function children(node, name, uri = '') {
|
|
21
|
+
return node.children.filter(child => child.name === name && child.uri === uri);
|
|
22
|
+
}
|
|
23
|
+
function one(node, name, uri = '') {
|
|
24
|
+
const found = children(node, name, uri);
|
|
25
|
+
if (found.length !== 1)
|
|
26
|
+
throw new SchemaError('Chronopost', 'Chronopost returned an invalid tracking structure');
|
|
27
|
+
return found[0];
|
|
28
|
+
}
|
|
29
|
+
function scalar(node, name, required = false) {
|
|
30
|
+
const found = children(node, name);
|
|
31
|
+
if (found.length > 1 || found[0]?.children.length || (required && found.length !== 1)) {
|
|
32
|
+
throw new SchemaError('Chronopost', 'Chronopost returned an invalid tracking field');
|
|
33
|
+
}
|
|
34
|
+
return found[0]?.text.trim() ?? '';
|
|
35
|
+
}
|
|
36
|
+
function clock(raw) {
|
|
37
|
+
// Reject impossible offsets that Luxon otherwise normalizes.
|
|
38
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/.test(raw)) {
|
|
39
|
+
const parsed = explicitOffsetTime(raw);
|
|
40
|
+
if (parsed)
|
|
41
|
+
return { time: parsed.iso };
|
|
42
|
+
}
|
|
43
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(raw) && DateTime.fromISO(raw, { zone: 'UTC' }).isValid) {
|
|
44
|
+
return { local_time: raw };
|
|
45
|
+
}
|
|
46
|
+
return raw ? { provider_time_text: clean(raw, 64) } : {};
|
|
47
|
+
}
|
|
48
|
+
/** Keep operational places, never the delivery-point address or shop contact block. */
|
|
49
|
+
function location(office, place) {
|
|
50
|
+
if (/^[\p{L}\p{M} .,'’()-]{1,100} - [A-Z]{2}(?: \(depot \d{1,6}\))?$/u.test(place))
|
|
51
|
+
return place;
|
|
52
|
+
const shop = /^([\p{L}\p{M} .,'’-]{1,100}) - ([A-Z]{2}) - /u.exec(office);
|
|
53
|
+
if (shop)
|
|
54
|
+
return `${shop[1]}, ${shop[2]}`;
|
|
55
|
+
return /^[\p{L}\p{M} .,'’()/-]{1,160}$/u.test(office) ? office : '';
|
|
56
|
+
}
|
|
57
|
+
export function parseChronopostTrackingXml(xml, rawNumber) {
|
|
58
|
+
const requested = normalizeChronopostNumber(rawNumber);
|
|
59
|
+
const root = xmlDocument(xml, CHRONOPOST_MAX_BYTES);
|
|
60
|
+
if (!root || root.name !== 'Envelope' || root.uri !== SOAP) {
|
|
61
|
+
if (/<title>\s*Just a moment|cf-chl-|challenge-platform/i.test(xml))
|
|
62
|
+
throw new ChallengeError('Chronopost');
|
|
63
|
+
throw new SchemaError('Chronopost', 'Chronopost returned invalid tracking XML');
|
|
64
|
+
}
|
|
65
|
+
const body = one(root, 'Body', SOAP);
|
|
66
|
+
if (children(body, 'Fault', SOAP).length)
|
|
67
|
+
throw new IndeterminateError('Chronopost', 'Chronopost tracking returned a SOAP fault');
|
|
68
|
+
if (body.children.length !== 1)
|
|
69
|
+
throw new SchemaError('Chronopost', 'Chronopost returned ambiguous tracking results');
|
|
70
|
+
const response = one(body, 'trackSkybillV2Response', CHRONOPOST_NAMESPACE);
|
|
71
|
+
const result = one(response, 'return');
|
|
72
|
+
const errorCode = scalar(result, 'errorCode', true);
|
|
73
|
+
if (!/^\d+$/.test(errorCode))
|
|
74
|
+
throw new SchemaError('Chronopost', 'Chronopost returned an invalid tracking error code');
|
|
75
|
+
if (Number(errorCode) !== 0)
|
|
76
|
+
throw new IndeterminateError('Chronopost', 'Chronopost tracking is unavailable');
|
|
77
|
+
const history = one(result, 'listEventInfoComp');
|
|
78
|
+
// Compare the entire identity, without truncation or accepting a linked parcel as an alias.
|
|
79
|
+
const identity = scalar(history, 'skybillNumber', true).toUpperCase().replace(/[\s.-]/g, '');
|
|
80
|
+
if (identity !== requested) {
|
|
81
|
+
throw new SchemaError('Chronopost', 'Chronopost returned a different shipment');
|
|
82
|
+
}
|
|
83
|
+
const scans = children(history, 'events');
|
|
84
|
+
if (!scans.length)
|
|
85
|
+
throw new NotFoundError('Chronopost');
|
|
86
|
+
if (scans.length > MAX_EVENTS)
|
|
87
|
+
throw new SchemaError('Chronopost', 'Chronopost returned excessive tracking history');
|
|
88
|
+
const references = new Set();
|
|
89
|
+
const geoReferences = new Set();
|
|
90
|
+
const destinations = new Set();
|
|
91
|
+
const projected = scans.map((scan, index) => {
|
|
92
|
+
const description = clean(scalar(scan, 'eventLabel', true), 500);
|
|
93
|
+
const code = clean(scalar(scan, 'code'), 40);
|
|
94
|
+
if (!description)
|
|
95
|
+
throw new SchemaError('Chronopost', 'Chronopost returned an empty tracking event');
|
|
96
|
+
const extras = children(scan, 'infoCompList');
|
|
97
|
+
let place = '';
|
|
98
|
+
for (const extra of extras) {
|
|
99
|
+
const name = scalar(extra, 'name');
|
|
100
|
+
const value = scalar(extra, 'value');
|
|
101
|
+
if (name === 'Lieu')
|
|
102
|
+
place = value;
|
|
103
|
+
if (name === 'Numéro partenaire') {
|
|
104
|
+
const match = /^(?:GEO\/)?([A-Z0-9]{4,40})$/.exec(value.toUpperCase());
|
|
105
|
+
if (match) {
|
|
106
|
+
references.add(match[1]);
|
|
107
|
+
if (value.toUpperCase().startsWith('GEO/') && isValidDpdParcelNumber(match[1]))
|
|
108
|
+
geoReferences.add(match[1]);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (name === 'Point de livraison') {
|
|
112
|
+
// Only the terminal country is projected; discard the address itself.
|
|
113
|
+
const country = countryCode(/ - ([A-Z]{2})$/.exec(value)?.[1]);
|
|
114
|
+
if (country)
|
|
115
|
+
destinations.add(country);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const event = {
|
|
119
|
+
...clock(scalar(scan, 'eventDate')),
|
|
120
|
+
description,
|
|
121
|
+
location: location(scalar(scan, 'officeLabel'), place),
|
|
122
|
+
...(code ? { provider_code: code } : {}),
|
|
123
|
+
};
|
|
124
|
+
const mapped = chronopostStage(code, description);
|
|
125
|
+
return { event, mapped, notification: isChronopostNotification(description), index };
|
|
126
|
+
});
|
|
127
|
+
// The operation returns oldest first. Reorder only when all scan clocks
|
|
128
|
+
// establish instants, so mixed local clocks cannot create a guessed order.
|
|
129
|
+
if (projected.every(scan => explicitOffsetTime(scan.event.time))) {
|
|
130
|
+
projected.sort((left, right) => explicitOffsetTime(left.event.time).timestamp
|
|
131
|
+
- explicitOffsetTime(right.event.time).timestamp || left.index - right.index);
|
|
132
|
+
}
|
|
133
|
+
let stage = 'pending';
|
|
134
|
+
let stageSource = 'none';
|
|
135
|
+
for (const scan of projected) {
|
|
136
|
+
if (!scan.notification && scan.mapped.source !== 'none') {
|
|
137
|
+
stage = scan.mapped.stage;
|
|
138
|
+
stageSource = scan.mapped.source;
|
|
139
|
+
}
|
|
140
|
+
// An alert records activity, but does not regress delivery or prove movement.
|
|
141
|
+
scan.event.stage = scan.notification ? stage : scan.mapped.stage;
|
|
142
|
+
scan.event.stage_source = scan.notification ? 'none' : scan.mapped.source;
|
|
143
|
+
}
|
|
144
|
+
const events = projected.reverse().slice(0, 100).map(scan => scan.event);
|
|
145
|
+
const latest = events[0];
|
|
146
|
+
const reference = references.size === 1 ? [...references][0] : undefined;
|
|
147
|
+
const destination = destinations.size === 1 ? [...destinations][0] : undefined;
|
|
148
|
+
// The typed Geopost reference and Germany destination propose a national
|
|
149
|
+
// lookup. The downstream adapter must still confirm ownership and activity.
|
|
150
|
+
const partner = reference && geoReferences.has(reference) && destination === 'DE' ? 'dpd-de' : undefined;
|
|
151
|
+
return {
|
|
152
|
+
status: languageStageStatus(stage),
|
|
153
|
+
current_stage: stage,
|
|
154
|
+
current_stage_source: stageSource,
|
|
155
|
+
last_status_text: latest.description,
|
|
156
|
+
last_update: latest.time ?? null,
|
|
157
|
+
expected_delivery: null,
|
|
158
|
+
...(reference ? { delivery_tracking_number: reference } : {}),
|
|
159
|
+
...(partner ? { delivery_carrier: partner } : {}),
|
|
160
|
+
...(destination ? { destination_country: destination } : {}),
|
|
161
|
+
events,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ClassifiedWording } from '../../core/status/index.js';
|
|
2
|
+
export declare function isChronopostNotification(label: string): boolean;
|
|
3
|
+
/** Partner events can reuse codes; the observed wording must agree with the code. */
|
|
4
|
+
export declare function chronopostStage(code: string, label: string): ClassifiedWording;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { classifyWording } from '../../core/status/index.js';
|
|
2
|
+
const observed = new Map([
|
|
3
|
+
['DC', { label: "Colis en cours de préparation chez l'expéditeur", stage: 'registered' }],
|
|
4
|
+
['DB', { label: "Colis déposé par l'expéditeur", stage: 'accepted' }],
|
|
5
|
+
['SC', { label: "Tri effectué dans l'agence de départ", stage: 'in_transit' }],
|
|
6
|
+
['TS', { label: "Colis en cours d'acheminement", stage: 'in_transit' }],
|
|
7
|
+
['O', { label: "Colis en cours d'acheminement", stage: 'in_transit' }],
|
|
8
|
+
['TP', { label: "Colis en cours d'acheminement", stage: 'in_transit' }],
|
|
9
|
+
]);
|
|
10
|
+
export function isChronopostNotification(label) {
|
|
11
|
+
return label === 'Destinataire informé par SMS ou mail';
|
|
12
|
+
}
|
|
13
|
+
/** Partner events can reuse codes; the observed wording must agree with the code. */
|
|
14
|
+
export function chronopostStage(code, label) {
|
|
15
|
+
const known = observed.get(code);
|
|
16
|
+
if (known?.label === label)
|
|
17
|
+
return { stage: known.stage, source: 'carrier_map' };
|
|
18
|
+
return classifyWording(label, 'pending');
|
|
19
|
+
}
|
|
@@ -42,6 +42,62 @@
|
|
|
42
42
|
"confirmedBy": "fixture",
|
|
43
43
|
"firstSeen": "2026-09-12",
|
|
44
44
|
"note": "Observed on a Chronopost shipment with an empty group and the label 'Livraison effectuée'."
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"code": "DC",
|
|
48
|
+
"stage": "registered",
|
|
49
|
+
"confirmedBy": "live",
|
|
50
|
+
"note": "Chronopost trackSkybillV2; code mapping requires matching wording.",
|
|
51
|
+
"wording": "Colis en cours de préparation chez l'expéditeur",
|
|
52
|
+
"language": "fr"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"code": "DB",
|
|
56
|
+
"stage": "accepted",
|
|
57
|
+
"confirmedBy": "live",
|
|
58
|
+
"note": "Chronopost trackSkybillV2; code mapping requires matching wording.",
|
|
59
|
+
"wording": "Colis déposé par l'expéditeur",
|
|
60
|
+
"language": "fr"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"code": "SC",
|
|
64
|
+
"stage": "in_transit",
|
|
65
|
+
"confirmedBy": "live",
|
|
66
|
+
"note": "Chronopost trackSkybillV2; code mapping requires matching wording.",
|
|
67
|
+
"wording": "Tri effectué dans l'agence de départ",
|
|
68
|
+
"language": "fr"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"code": "TS",
|
|
72
|
+
"stage": "in_transit",
|
|
73
|
+
"confirmedBy": "live",
|
|
74
|
+
"note": "Chronopost trackSkybillV2; code mapping requires matching wording.",
|
|
75
|
+
"wording": "Colis en cours d'acheminement",
|
|
76
|
+
"language": "fr"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"code": "O",
|
|
80
|
+
"stage": "in_transit",
|
|
81
|
+
"confirmedBy": "live",
|
|
82
|
+
"note": "Chronopost trackSkybillV2; code mapping requires matching wording.",
|
|
83
|
+
"wording": "Colis en cours d'acheminement",
|
|
84
|
+
"language": "fr"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"code": "TP",
|
|
88
|
+
"stage": "in_transit",
|
|
89
|
+
"confirmedBy": "live",
|
|
90
|
+
"note": "Chronopost trackSkybillV2; code mapping requires matching wording.",
|
|
91
|
+
"wording": "Colis en cours d'acheminement",
|
|
92
|
+
"language": "fr"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"code": "SM",
|
|
96
|
+
"stage": "in_transit",
|
|
97
|
+
"confirmedBy": "live",
|
|
98
|
+
"note": "Notification after an in-transit scan; inherits the preceding shipment stage rather than establishing a milestone.",
|
|
99
|
+
"wording": "Destinataire informé par SMS ou mail",
|
|
100
|
+
"language": "fr"
|
|
45
101
|
}
|
|
46
102
|
]
|
|
47
103
|
}
|
|
@@ -2,6 +2,7 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { load } from 'cheerio';
|
|
4
4
|
import { lookupBudget } from '../../core/adapter/index.js';
|
|
5
|
+
import { DELIVERY_POSTCODE, deliveryPostcodeText } from '../../core/catalog/postcode.js';
|
|
5
6
|
import { dpdParcelNumber } from '../../core/detection/dpd.js';
|
|
6
7
|
import { BudgetExceededError, carrierErrorKind, ChallengeError, IndeterminateError, InvalidInputError, NotFoundError, SchemaError, UpstreamHttpError } from '../../core/errors/index.js';
|
|
7
8
|
import { runSteps } from '../../core/runner/index.js';
|
|
@@ -47,8 +48,6 @@ const TOKEN_FAILURE_MEMORY_MS = 30_000;
|
|
|
47
48
|
/** The browser service may spend the whole request timeout plus its own transport allowance. */
|
|
48
49
|
const SOLVER_ALLOWANCE_MS = 15_000;
|
|
49
50
|
const MAX_BYTES = 10_000_000;
|
|
50
|
-
/** Any country's postcode, as the catalog's `internationalPostcode` input takes it. */
|
|
51
|
-
const DELIVERY_POSTCODE = /^(?=.{3,12}$)(?=.*\d)[A-Z0-9]+(?:[ -][A-Z0-9]+)*$/;
|
|
52
51
|
/** Cloudflare interrupted the consignee page with an interactive challenge. */
|
|
53
52
|
export class DPDChallengeError extends ChallengeError {
|
|
54
53
|
constructor(message = 'DPD returned a Cloudflare browser challenge', options) {
|
|
@@ -537,7 +536,7 @@ export class DPDTracker {
|
|
|
537
536
|
// DPD checks the postcode against the recipient's, and the service this
|
|
538
537
|
// adapter asks also answers for parcels delivered in other countries: any
|
|
539
538
|
// country's postcode goes through. The German unit reads German deliveries.
|
|
540
|
-
const resolvedPostcode = postcode
|
|
539
|
+
const resolvedPostcode = deliveryPostcodeText(postcode);
|
|
541
540
|
if (resolvedPostcode && !(this.country === 'DE' ? /^\d{5}$/ : DELIVERY_POSTCODE).test(resolvedPostcode)) {
|
|
542
541
|
throw new InvalidInputError('DPD', this.country === 'DE'
|
|
543
542
|
? 'DPD postcode must contain exactly 5 digits'
|
|
@@ -15,10 +15,16 @@ export declare class GLSSwitzerlandTrackingError extends NotFoundError {
|
|
|
15
15
|
constructor();
|
|
16
16
|
}
|
|
17
17
|
export declare function normalizeGLSSwitzerlandTrackingNumber(raw: string): string;
|
|
18
|
-
export
|
|
18
|
+
export type GLSPostcodeShape = 4 | 'any';
|
|
19
|
+
/**
|
|
20
|
+
* The recipient postcode: four Swiss digits, or with `any` the postcode of any
|
|
21
|
+
* country GLS delivers in. GLS refuses only what no country writes, such as a
|
|
22
|
+
* symbol or fewer than three characters.
|
|
23
|
+
*/
|
|
24
|
+
export declare function normalizeGLSSwitzerlandPostcode(raw: string, shape?: GLSPostcodeShape): string;
|
|
19
25
|
export declare function glsSwitzerlandTrackingUrl(rawTrackingNumber: string): string;
|
|
20
26
|
export declare function glsSwitzerlandOverviewApiUrl(rawTrackingNumber: string, millis?: number): string;
|
|
21
|
-
export declare function glsSwitzerlandDetailApiUrl(rawParcelNumber: string, rawPostcode: string, millis?: number, ownerCode?: string,
|
|
27
|
+
export declare function glsSwitzerlandDetailApiUrl(rawParcelNumber: string, rawPostcode: string, millis?: number, ownerCode?: string, postcodeShape?: GLSPostcodeShape): string;
|
|
22
28
|
export declare function selectGLSParcel(payload: unknown, rawTrackingNumber: string): JsonObject;
|
|
23
29
|
export declare function parseGLSSwitzerlandTrackingResponse(payload: unknown, rawTrackingNumber: string): CarrierResult;
|
|
24
30
|
export declare class GLSSwitzerlandTracker {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { load } from 'cheerio';
|
|
2
2
|
import { DateTime } from 'luxon';
|
|
3
3
|
import { lookupBudget } from '../../core/adapter/index.js';
|
|
4
|
+
import { DELIVERY_POSTCODE, deliveryPostcodeText } from '../../core/catalog/postcode.js';
|
|
4
5
|
import { InputRequiredError, InvalidInputError, NotFoundError, SchemaError, UpstreamHttpError } from '../../core/errors/index.js';
|
|
5
6
|
import { cleanScalar, fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
|
|
6
7
|
import { isRecord } from '../../core/types.js';
|
|
@@ -49,11 +50,16 @@ export function normalizeGLSSwitzerlandTrackingNumber(raw) {
|
|
|
49
50
|
}
|
|
50
51
|
return value;
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
/**
|
|
54
|
+
* The recipient postcode: four Swiss digits, or with `any` the postcode of any
|
|
55
|
+
* country GLS delivers in. GLS refuses only what no country writes, such as a
|
|
56
|
+
* symbol or fewer than three characters.
|
|
57
|
+
*/
|
|
58
|
+
export function normalizeGLSSwitzerlandPostcode(raw, shape = 4) {
|
|
59
|
+
const value = shape === 'any' ? deliveryPostcodeText(raw) : raw.trim();
|
|
60
|
+
if (!(shape === 'any' ? DELIVERY_POSTCODE : /^\d{4}$/).test(value)) {
|
|
61
|
+
const wanted = shape === 'any' ? 'recipient postcode' : '4-digit recipient postcode';
|
|
62
|
+
throw new InputRequiredError('GLS', `the ${wanted}`, `GLS detailed tracking requires the ${wanted}`);
|
|
57
63
|
}
|
|
58
64
|
return value;
|
|
59
65
|
}
|
|
@@ -70,7 +76,7 @@ export function glsSwitzerlandOverviewApiUrl(rawTrackingNumber, millis = Date.no
|
|
|
70
76
|
url.searchParams.set('millis', String(millis));
|
|
71
77
|
return url.toString();
|
|
72
78
|
}
|
|
73
|
-
export function glsSwitzerlandDetailApiUrl(rawParcelNumber, rawPostcode, millis = Date.now(), ownerCode = '',
|
|
79
|
+
export function glsSwitzerlandDetailApiUrl(rawParcelNumber, rawPostcode, millis = Date.now(), ownerCode = '', postcodeShape = 4) {
|
|
74
80
|
const parcelNumber = normalizeGLSSwitzerlandTrackingNumber(rawParcelNumber);
|
|
75
81
|
if (!/^\d{11,14}$/.test(parcelNumber)) {
|
|
76
82
|
throw new InputRequiredError('GLS', 'the numeric parcel number', 'GLS details require the numeric parcel number');
|
|
@@ -78,7 +84,7 @@ export function glsSwitzerlandDetailApiUrl(rawParcelNumber, rawPostcode, millis
|
|
|
78
84
|
const url = new URL(`${TRACKING_API}/rstt028/${encodeURIComponent(parcelNumber)}`);
|
|
79
85
|
url.searchParams.set('caller', 'witt002');
|
|
80
86
|
url.searchParams.set('millis', String(millis));
|
|
81
|
-
url.searchParams.set('postalCode', normalizeGLSSwitzerlandPostcode(rawPostcode,
|
|
87
|
+
url.searchParams.set('postalCode', normalizeGLSSwitzerlandPostcode(rawPostcode, postcodeShape));
|
|
82
88
|
const owner = text(ownerCode, 32);
|
|
83
89
|
if (owner && /^[A-Z0-9_-]+$/i.test(owner))
|
|
84
90
|
url.searchParams.set('tuOwnerCode', owner);
|
|
@@ -3,8 +3,9 @@ import { NotFoundError, SchemaError, UpstreamHttpError } from '../../core/errors
|
|
|
3
3
|
import { cleanScalar, fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
|
|
4
4
|
import { isRecord } from '../../core/types.js';
|
|
5
5
|
import { glsSwitzerlandDetailApiUrl, glsDeliveryReference, glsSwitzerlandOverviewApiUrl, GLSSwitzerlandTrackingError, normalizeGLSSwitzerlandPostcode, normalizeGLSSwitzerlandTrackingNumber, parseGLSSwitzerlandTrackingResponse, selectGLSParcel, } from '../gls-ch/adapter.js';
|
|
6
|
-
// GLS's GROUP recipient service covers
|
|
7
|
-
//
|
|
6
|
+
// GLS's GROUP recipient service covers the parcels GLS delivers across its
|
|
7
|
+
// network, so the delivery postcode can be any country's. Keep the shared
|
|
8
|
+
// response parser.
|
|
8
9
|
const PROVIDER = 'GLS Germany tracking';
|
|
9
10
|
const CARRIER = 'GLS Germany';
|
|
10
11
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
@@ -33,7 +34,7 @@ export class GLSGermanyTracker {
|
|
|
33
34
|
}
|
|
34
35
|
async fetch(rawTrackingNumber, rawPostcode, context = {}) {
|
|
35
36
|
const number = normalizeGLSSwitzerlandTrackingNumber(rawTrackingNumber);
|
|
36
|
-
const postcode = normalizeGLSSwitzerlandPostcode(rawPostcode, '
|
|
37
|
+
const postcode = normalizeGLSSwitzerlandPostcode(rawPostcode, 'any');
|
|
37
38
|
// Two requests, each with the full request timeout.
|
|
38
39
|
const budget = lookupBudget(context, 2 * this.timeoutMs);
|
|
39
40
|
const overview = await this.request(glsSwitzerlandOverviewApiUrl(number, this.now()), budget);
|
|
@@ -55,7 +56,7 @@ export class GLSGermanyTracker {
|
|
|
55
56
|
}
|
|
56
57
|
const owners = Array.isArray(parcel.owners) ? parcel.owners.filter(isRecord) : [];
|
|
57
58
|
const owner = owners.find((row) => row.type === 'REQUEST');
|
|
58
|
-
const detail = await this.request(glsSwitzerlandDetailApiUrl(String(parcel.tuNo), postcode, this.now(), cleanScalar(owner?.code, 64), '
|
|
59
|
+
const detail = await this.request(glsSwitzerlandDetailApiUrl(String(parcel.tuNo), postcode, this.now(), cleanScalar(owner?.code, 64), 'any'), budget);
|
|
59
60
|
try {
|
|
60
61
|
const result = parseGLSSwitzerlandTrackingResponse(detail, String(parcel.tuNo));
|
|
61
62
|
// Both services use CET/CEST; expose the regional timezone to clients.
|
|
@@ -21,14 +21,13 @@
|
|
|
21
21
|
"requirements": [
|
|
22
22
|
{
|
|
23
23
|
"field": "postcode",
|
|
24
|
-
"validator": "
|
|
24
|
+
"validator": "internationalPostcode",
|
|
25
25
|
"label": "Delivery postcode",
|
|
26
26
|
"type": "text",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"inputMode": "numeric",
|
|
27
|
+
"help": "GLS requires the delivery postcode to show the detailed event history.",
|
|
28
|
+
"pattern": "^(?=.{3,12}$)(?=.*[0-9])[A-Za-z0-9]+(?:[ \\-][A-Za-z0-9]+)*$",
|
|
29
|
+
"maxLength": 12,
|
|
30
|
+
"inputMode": "text",
|
|
32
31
|
"autoComplete": "postal-code"
|
|
33
32
|
}
|
|
34
33
|
],
|
|
@@ -5,11 +5,19 @@ import { TrawlClient } from '../../core/transport/index.js';
|
|
|
5
5
|
import { MondialRelayAppClient } from './app.js';
|
|
6
6
|
interface MondialRelayCredential {
|
|
7
7
|
shipment: string;
|
|
8
|
+
/** The recipient's, as typed; empty when the number needs none. */
|
|
8
9
|
postcode: string;
|
|
9
10
|
canonicalShipment?: string;
|
|
10
11
|
/** Read from a checksummed label barcode, which needs no postcode. */
|
|
11
12
|
barcode?: true;
|
|
12
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* The shipment and the postcode Mondial Relay is asked with. The postcode is
|
|
16
|
+
* compared with the recipient's as typed, in any country's format: the country
|
|
17
|
+
* sent alongside only picks the reply's language. Only an 8-digit shipment
|
|
18
|
+
* needs it; the 10- and 12-digit forms carry the brand and are found without
|
|
19
|
+
* one, so a postcode typed for them is not sent.
|
|
20
|
+
*/
|
|
13
21
|
export declare function normalizeMondialRelayCredential(rawShipment: string, rawPostcode?: string): MondialRelayCredential;
|
|
14
22
|
export declare function mondialRelayTrackingUrl(rawShipment: string, rawPostcode?: string): string;
|
|
15
23
|
export declare function parseMondialRelayTrackingResponse(payload: unknown, rawShipment: string, rawPostcode?: string): CarrierResult;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { load } from 'cheerio';
|
|
2
2
|
import { DateTime } from 'luxon';
|
|
3
|
+
import { DELIVERY_POSTCODE, deliveryPostcodeText } from '../../core/catalog/postcode.js';
|
|
3
4
|
import { isValidMondialRelayBarcode } from '../../core/detection/index.js';
|
|
4
5
|
import { ChallengeError, InputRequiredError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
|
|
5
6
|
import { runSteps, singleFlight, takeTurn } from '../../core/runner/index.js';
|
|
@@ -24,8 +25,8 @@ const DEFAULT_TIMEOUT_MS = 90_000;
|
|
|
24
25
|
/** The app answers in about a second; past this the website gets the rest of the budget. */
|
|
25
26
|
const DEFAULT_DIRECT_TIMEOUT_MS = 10_000;
|
|
26
27
|
const ZONE = 'Europe/Paris';
|
|
27
|
-
const CREDENTIAL_MESSAGE = 'Mondial Relay tracking requires
|
|
28
|
-
+ '
|
|
28
|
+
const CREDENTIAL_MESSAGE = 'Mondial Relay tracking requires a 26-digit label barcode, a 10- or 12-digit shipment '
|
|
29
|
+
+ 'number, or an 8-digit shipment number with the recipient postcode';
|
|
29
30
|
/** Strip the markup Mondial Relay embeds in its labels, entities included. */
|
|
30
31
|
function plainText(value, limit = 500) {
|
|
31
32
|
if (typeof value !== 'string' && typeof value !== 'number')
|
|
@@ -36,32 +37,40 @@ function plainText(value, limit = 500) {
|
|
|
36
37
|
function plausibleFrenchPostcode(value) {
|
|
37
38
|
return /^(?:0[1-9]|[1-8]\d|9[0-5]|97|98)\d{3}$/.test(value);
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The shipment and the postcode Mondial Relay is asked with. The postcode is
|
|
42
|
+
* compared with the recipient's as typed, in any country's format: the country
|
|
43
|
+
* sent alongside only picks the reply's language. Only an 8-digit shipment
|
|
44
|
+
* needs it; the 10- and 12-digit forms carry the brand and are found without
|
|
45
|
+
* one, so a postcode typed for them is not sent.
|
|
46
|
+
*/
|
|
39
47
|
export function normalizeMondialRelayCredential(rawShipment, rawPostcode = '') {
|
|
40
48
|
let shipment = rawShipment.trim().replace(/[\s.-]/g, '');
|
|
41
|
-
let postcode = rawPostcode
|
|
49
|
+
let postcode = deliveryPostcodeText(rawPostcode);
|
|
42
50
|
if (/^\d{26}$/.test(shipment)) {
|
|
43
|
-
if (!isValidMondialRelayBarcode(shipment) || (postcode && !
|
|
51
|
+
if (!isValidMondialRelayBarcode(shipment) || (postcode && !DELIVERY_POSTCODE.test(postcode))) {
|
|
44
52
|
throw new InvalidInputError('Mondial Relay', 'Invalid Mondial Relay barcode or postcode');
|
|
45
53
|
}
|
|
46
54
|
// The public alias carries brand/shipment/parcel sequence, not a postcode.
|
|
47
55
|
return { shipment: shipment.slice(0, 12), postcode, canonicalShipment: shipment.slice(2, 10), barcode: true };
|
|
48
56
|
}
|
|
49
57
|
if (!postcode && /^(?:\d{13}|\d{15}|\d{17})$/.test(shipment)) {
|
|
58
|
+
// A French label prints the shipment followed by the 5-digit postcode.
|
|
59
|
+
if (!plausibleFrenchPostcode(shipment.slice(-5)))
|
|
60
|
+
throw new InvalidInputError('Mondial Relay', CREDENTIAL_MESSAGE);
|
|
50
61
|
postcode = shipment.slice(-5);
|
|
51
62
|
shipment = shipment.slice(0, -5);
|
|
52
63
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
throw
|
|
58
|
-
? new InputRequiredError('Mondial Relay', 'the recipient postcode', CREDENTIAL_MESSAGE)
|
|
59
|
-
: new InvalidInputError('Mondial Relay', CREDENTIAL_MESSAGE);
|
|
64
|
+
if (!/^(?:\d{8}|\d{10}|\d{12})$/.test(shipment))
|
|
65
|
+
throw new InvalidInputError('Mondial Relay', CREDENTIAL_MESSAGE);
|
|
66
|
+
if (shipment.length === 8 && !DELIVERY_POSTCODE.test(postcode)) {
|
|
67
|
+
// A well-shaped number without a usable postcode is a missing credential.
|
|
68
|
+
throw new InputRequiredError('Mondial Relay', 'the recipient postcode', CREDENTIAL_MESSAGE);
|
|
60
69
|
}
|
|
61
70
|
// The longer forms put the 2-digit brand before the 8-digit shipment (the
|
|
62
71
|
// 12-digit one adds the parcel sequence), and the API echoes the shipment.
|
|
63
72
|
return shipment.length > 8
|
|
64
|
-
? { shipment, postcode, canonicalShipment: shipment.slice(2, 10) }
|
|
73
|
+
? { shipment, postcode: '', canonicalShipment: shipment.slice(2, 10) }
|
|
65
74
|
: { shipment, postcode };
|
|
66
75
|
}
|
|
67
76
|
export function mondialRelayTrackingUrl(rawShipment, rawPostcode = '') {
|
|
@@ -75,6 +84,8 @@ function trackingApiUrl(credential, brand = '') {
|
|
|
75
84
|
url.searchParams.set('shipment', credential.shipment);
|
|
76
85
|
url.searchParams.set('postcode', credential.postcode);
|
|
77
86
|
url.searchParams.set('brand', brand);
|
|
87
|
+
// Only the reply's language: the postcode matches whatever country it is from.
|
|
88
|
+
// The status and event parsers read French.
|
|
78
89
|
url.searchParams.set('codePays', 'fr');
|
|
79
90
|
return url.toString();
|
|
80
91
|
}
|
|
@@ -5,7 +5,7 @@ export declare const MONDIAL_RELAY_APP_API = "https://mobile-app-bff.mondialrela
|
|
|
5
5
|
* HTTP/2, and answers the same request over plain HTTP/1.1.
|
|
6
6
|
*/
|
|
7
7
|
export declare const http1Fetch: typeof fetch;
|
|
8
|
-
/** A normalized credential: 8, 10 or 12 digits, or a barcode's 12-digit alias, and
|
|
8
|
+
/** A normalized credential: 8, 10 or 12 digits, or a barcode's 12-digit alias, and the postcode an 8-digit one needs. */
|
|
9
9
|
export interface MondialRelayAppQuery {
|
|
10
10
|
shipment: string;
|
|
11
11
|
postcode: string;
|
|
@@ -22,15 +22,14 @@
|
|
|
22
22
|
"requirements": [
|
|
23
23
|
{
|
|
24
24
|
"field": "postcode",
|
|
25
|
-
"whenTrackingNumber": "^
|
|
26
|
-
"validator": "
|
|
25
|
+
"whenTrackingNumber": "^[0-9]{8}$",
|
|
26
|
+
"validator": "internationalPostcode",
|
|
27
27
|
"label": "Delivery postcode",
|
|
28
28
|
"type": "text",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"inputMode": "numeric",
|
|
29
|
+
"help": "8-digit Mondial Relay shipment numbers require the recipient postcode; longer numbers and label barcodes do not.",
|
|
30
|
+
"pattern": "^(?=.{3,12}$)(?=.*[0-9])[A-Za-z0-9]+(?:[ \\-][A-Za-z0-9]+)*$",
|
|
31
|
+
"maxLength": 12,
|
|
32
|
+
"inputMode": "text",
|
|
34
33
|
"autoComplete": "postal-code"
|
|
35
34
|
}
|
|
36
35
|
],
|
|
@@ -3,6 +3,7 @@ import { activeRequirements, carrierDefinition } from './index.js';
|
|
|
3
3
|
import { isValidMondialRelayBarcode } from '../detection/index.js';
|
|
4
4
|
import { validateDachserTrackingUrl } from './urls.js';
|
|
5
5
|
import { validatePlanzerSharedUrl } from './urls.js';
|
|
6
|
+
import { DELIVERY_POSTCODE, deliveryPostcodeText } from './postcode.js';
|
|
6
7
|
export { AUTOMATIC_CARRIER_IDS, CARRIER_NAMES, activeRequirements, carrierAdapter, carrierDefinition, carrierTimezone, requiredRequirements, } from './index.js';
|
|
7
8
|
export { isValidS10TrackingNumber, supportsSwissPostHandoff, } from '../detection/index.js';
|
|
8
9
|
export function normalizeCarrierInputs(carrierId, trackingNumber, trackingUrl, postcode) {
|
|
@@ -15,10 +16,13 @@ export function normalizeCarrierInputs(carrierId, trackingNumber, trackingUrl, p
|
|
|
15
16
|
throw new TypeError('Invalid Mondial Relay barcode');
|
|
16
17
|
}
|
|
17
18
|
const requirements = new Map(activeRequirements(carrierId, trackingNumber).map((item) => [item.field, item]));
|
|
18
|
-
// Older clients may still supply a postcode for
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
// Older clients may still supply a postcode for Mondial Relay numbers that
|
|
20
|
+
// need none: label barcodes and the 10- and 12-digit forms. Validate it when
|
|
21
|
+
// present, while allowing them to work without one.
|
|
22
|
+
const mondialWithoutPostcode = mondialBarcode
|
|
23
|
+
|| (carrierId === 'mondial-relay' && /^(?:\d{10}|\d{12})$/.test(trackingNumber));
|
|
24
|
+
if (mondialWithoutPostcode && supplied.postcode) {
|
|
25
|
+
requirements.set('postcode', { field: 'postcode', validator: 'internationalPostcode' });
|
|
22
26
|
}
|
|
23
27
|
for (const [field, value] of Object.entries(supplied)) {
|
|
24
28
|
if (value !== null && !requirements.has(field)) {
|
|
@@ -76,10 +80,10 @@ export function normalizeCarrierInputs(carrierId, trackingNumber, trackingUrl, p
|
|
|
76
80
|
break;
|
|
77
81
|
case 'internationalPostcode':
|
|
78
82
|
// The carrier delivers abroad too: any country's postcode, checked by the carrier itself.
|
|
79
|
-
if (!
|
|
83
|
+
if (!DELIVERY_POSTCODE.test(deliveryPostcodeText(value))) {
|
|
80
84
|
throw new TypeError(`${carrierDefinition(carrierId).displayName} requires a valid delivery postcode`);
|
|
81
85
|
}
|
|
82
|
-
supplied[field] =
|
|
86
|
+
supplied[field] = deliveryPostcodeText(value);
|
|
83
87
|
break;
|
|
84
88
|
case 'paackPostcode': {
|
|
85
89
|
const rawPostcode = value.toLocaleUpperCase('en-US');
|
|
@@ -95,15 +99,10 @@ export function normalizeCarrierInputs(carrierId, trackingNumber, trackingUrl, p
|
|
|
95
99
|
}
|
|
96
100
|
return supplied;
|
|
97
101
|
}
|
|
98
|
-
/** Any country's postcode: 3 to 12 characters with a digit, in groups joined by one space or hyphen. */
|
|
99
|
-
const POSTCODE_SHAPE = /^(?=.{3,12}$)(?=.*\d)[A-Z0-9]+(?:[ -][A-Z0-9]+)*$/;
|
|
100
|
-
function postcodeText(value) {
|
|
101
|
-
return value.trim().toUpperCase().replace(/\s+/g, ' ');
|
|
102
|
-
}
|
|
103
102
|
/** A recipient postcode for universal providers; independent of a carrier's own credentials. */
|
|
104
103
|
export function normalizeDeliveryPostcode(value) {
|
|
105
|
-
const normalized =
|
|
106
|
-
if (!
|
|
104
|
+
const normalized = deliveryPostcodeText(value);
|
|
105
|
+
if (!DELIVERY_POSTCODE.test(normalized)) {
|
|
107
106
|
throw new TypeError('A valid delivery postcode is required');
|
|
108
107
|
}
|
|
109
108
|
return normalized;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Any country's postcode: 3 to 12 characters with a digit, in groups joined by one space or hyphen. */
|
|
2
|
+
export declare const DELIVERY_POSTCODE: RegExp;
|
|
3
|
+
/** A typed postcode as it is checked and sent: trimmed, in capitals, with single spaces. */
|
|
4
|
+
export declare function deliveryPostcodeText(value: string): string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Any country's postcode: 3 to 12 characters with a digit, in groups joined by one space or hyphen. */
|
|
2
|
+
export const DELIVERY_POSTCODE = /^(?=.{3,12}$)(?=.*\d)[A-Z0-9]+(?:[ -][A-Z0-9]+)*$/;
|
|
3
|
+
/** A typed postcode as it is checked and sent: trimmed, in capitals, with single spaces. */
|
|
4
|
+
export function deliveryPostcodeText(value) {
|
|
5
|
+
return value.trim().toUpperCase().replace(/\s+/g, ' ');
|
|
6
|
+
}
|
package/dist/data/catalog.json
CHANGED
|
@@ -949,15 +949,14 @@
|
|
|
949
949
|
"requirements": [
|
|
950
950
|
{
|
|
951
951
|
"field": "postcode",
|
|
952
|
-
"whenTrackingNumber": "^
|
|
953
|
-
"validator": "
|
|
952
|
+
"whenTrackingNumber": "^[0-9]{8}$",
|
|
953
|
+
"validator": "internationalPostcode",
|
|
954
954
|
"label": "Delivery postcode",
|
|
955
955
|
"type": "text",
|
|
956
|
-
"
|
|
957
|
-
"
|
|
958
|
-
"
|
|
959
|
-
"
|
|
960
|
-
"inputMode": "numeric",
|
|
956
|
+
"help": "8-digit Mondial Relay shipment numbers require the recipient postcode; longer numbers and label barcodes do not.",
|
|
957
|
+
"pattern": "^(?=.{3,12}$)(?=.*[0-9])[A-Za-z0-9]+(?:[ \\-][A-Za-z0-9]+)*$",
|
|
958
|
+
"maxLength": 12,
|
|
959
|
+
"inputMode": "text",
|
|
961
960
|
"autoComplete": "postal-code"
|
|
962
961
|
}
|
|
963
962
|
]
|
|
@@ -1109,8 +1108,9 @@
|
|
|
1109
1108
|
"timezone": "Europe/Paris",
|
|
1110
1109
|
"tracking": {
|
|
1111
1110
|
"mode": "automatic",
|
|
1112
|
-
"adapter": "
|
|
1113
|
-
"recognitionRank": 45
|
|
1111
|
+
"adapter": "chronopost",
|
|
1112
|
+
"recognitionRank": 45,
|
|
1113
|
+
"localClocks": true
|
|
1114
1114
|
},
|
|
1115
1115
|
"canaryUrl": "https://www.chronopost.fr/tracking-no-cms/suivi-page",
|
|
1116
1116
|
"trackingUrlTemplate": "https://www.chronopost.fr/tracking-no-cms/suivi-page?langue=fr&listeNumerosLT={trackingNumber}",
|
|
@@ -1644,14 +1644,13 @@
|
|
|
1644
1644
|
"requirements": [
|
|
1645
1645
|
{
|
|
1646
1646
|
"field": "postcode",
|
|
1647
|
-
"validator": "
|
|
1647
|
+
"validator": "internationalPostcode",
|
|
1648
1648
|
"label": "Delivery postcode",
|
|
1649
1649
|
"type": "text",
|
|
1650
|
-
"
|
|
1651
|
-
"
|
|
1652
|
-
"
|
|
1653
|
-
"
|
|
1654
|
-
"inputMode": "numeric",
|
|
1650
|
+
"help": "GLS requires the delivery postcode to show the detailed event history.",
|
|
1651
|
+
"pattern": "^(?=.{3,12}$)(?=.*[0-9])[A-Za-z0-9]+(?:[ \\-][A-Za-z0-9]+)*$",
|
|
1652
|
+
"maxLength": 12,
|
|
1653
|
+
"inputMode": "text",
|
|
1655
1654
|
"autoComplete": "postal-code"
|
|
1656
1655
|
}
|
|
1657
1656
|
],
|
|
@@ -584,15 +584,14 @@ export declare const CARRIER_CATALOG: {
|
|
|
584
584
|
readonly adapter: "mondial-relay";
|
|
585
585
|
readonly requirements: readonly [{
|
|
586
586
|
readonly field: "postcode";
|
|
587
|
-
readonly whenTrackingNumber: "^
|
|
588
|
-
readonly validator: "
|
|
587
|
+
readonly whenTrackingNumber: "^[0-9]{8}$";
|
|
588
|
+
readonly validator: "internationalPostcode";
|
|
589
589
|
readonly label: "Delivery postcode";
|
|
590
590
|
readonly type: "text";
|
|
591
|
-
readonly
|
|
592
|
-
readonly
|
|
593
|
-
readonly
|
|
594
|
-
readonly
|
|
595
|
-
readonly inputMode: "numeric";
|
|
591
|
+
readonly help: "8-digit Mondial Relay shipment numbers require the recipient postcode; longer numbers and label barcodes do not.";
|
|
592
|
+
readonly pattern: "^(?=.{3,12}$)(?=.*[0-9])[A-Za-z0-9]+(?:[ \\-][A-Za-z0-9]+)*$";
|
|
593
|
+
readonly maxLength: 12;
|
|
594
|
+
readonly inputMode: "text";
|
|
596
595
|
readonly autoComplete: "postal-code";
|
|
597
596
|
}];
|
|
598
597
|
};
|
|
@@ -691,8 +690,9 @@ export declare const CARRIER_CATALOG: {
|
|
|
691
690
|
readonly timezone: "Europe/Paris";
|
|
692
691
|
readonly tracking: {
|
|
693
692
|
readonly mode: "automatic";
|
|
694
|
-
readonly adapter: "
|
|
693
|
+
readonly adapter: "chronopost";
|
|
695
694
|
readonly recognitionRank: 45;
|
|
695
|
+
readonly localClocks: true;
|
|
696
696
|
};
|
|
697
697
|
readonly canaryUrl: "https://www.chronopost.fr/tracking-no-cms/suivi-page";
|
|
698
698
|
readonly trackingUrlTemplate: "https://www.chronopost.fr/tracking-no-cms/suivi-page?langue=fr&listeNumerosLT={trackingNumber}";
|
|
@@ -1045,14 +1045,13 @@ export declare const CARRIER_CATALOG: {
|
|
|
1045
1045
|
readonly adapter: "gls-de";
|
|
1046
1046
|
readonly requirements: readonly [{
|
|
1047
1047
|
readonly field: "postcode";
|
|
1048
|
-
readonly validator: "
|
|
1048
|
+
readonly validator: "internationalPostcode";
|
|
1049
1049
|
readonly label: "Delivery postcode";
|
|
1050
1050
|
readonly type: "text";
|
|
1051
|
-
readonly
|
|
1052
|
-
readonly
|
|
1053
|
-
readonly
|
|
1054
|
-
readonly
|
|
1055
|
-
readonly inputMode: "numeric";
|
|
1051
|
+
readonly help: "GLS requires the delivery postcode to show the detailed event history.";
|
|
1052
|
+
readonly pattern: "^(?=.{3,12}$)(?=.*[0-9])[A-Za-z0-9]+(?:[ \\-][A-Za-z0-9]+)*$";
|
|
1053
|
+
readonly maxLength: 12;
|
|
1054
|
+
readonly inputMode: "text";
|
|
1056
1055
|
readonly autoComplete: "postal-code";
|
|
1057
1056
|
}];
|
|
1058
1057
|
readonly recognitionRank: 50;
|
|
@@ -950,15 +950,14 @@ export const CARRIER_CATALOG = {
|
|
|
950
950
|
"requirements": [
|
|
951
951
|
{
|
|
952
952
|
"field": "postcode",
|
|
953
|
-
"whenTrackingNumber": "^
|
|
954
|
-
"validator": "
|
|
953
|
+
"whenTrackingNumber": "^[0-9]{8}$",
|
|
954
|
+
"validator": "internationalPostcode",
|
|
955
955
|
"label": "Delivery postcode",
|
|
956
956
|
"type": "text",
|
|
957
|
-
"
|
|
958
|
-
"
|
|
959
|
-
"
|
|
960
|
-
"
|
|
961
|
-
"inputMode": "numeric",
|
|
957
|
+
"help": "8-digit Mondial Relay shipment numbers require the recipient postcode; longer numbers and label barcodes do not.",
|
|
958
|
+
"pattern": "^(?=.{3,12}$)(?=.*[0-9])[A-Za-z0-9]+(?:[ \\-][A-Za-z0-9]+)*$",
|
|
959
|
+
"maxLength": 12,
|
|
960
|
+
"inputMode": "text",
|
|
962
961
|
"autoComplete": "postal-code"
|
|
963
962
|
}
|
|
964
963
|
]
|
|
@@ -1110,8 +1109,9 @@ export const CARRIER_CATALOG = {
|
|
|
1110
1109
|
"timezone": "Europe/Paris",
|
|
1111
1110
|
"tracking": {
|
|
1112
1111
|
"mode": "automatic",
|
|
1113
|
-
"adapter": "
|
|
1114
|
-
"recognitionRank": 45
|
|
1112
|
+
"adapter": "chronopost",
|
|
1113
|
+
"recognitionRank": 45,
|
|
1114
|
+
"localClocks": true
|
|
1115
1115
|
},
|
|
1116
1116
|
"canaryUrl": "https://www.chronopost.fr/tracking-no-cms/suivi-page",
|
|
1117
1117
|
"trackingUrlTemplate": "https://www.chronopost.fr/tracking-no-cms/suivi-page?langue=fr&listeNumerosLT={trackingNumber}",
|
|
@@ -1645,14 +1645,13 @@ export const CARRIER_CATALOG = {
|
|
|
1645
1645
|
"requirements": [
|
|
1646
1646
|
{
|
|
1647
1647
|
"field": "postcode",
|
|
1648
|
-
"validator": "
|
|
1648
|
+
"validator": "internationalPostcode",
|
|
1649
1649
|
"label": "Delivery postcode",
|
|
1650
1650
|
"type": "text",
|
|
1651
|
-
"
|
|
1652
|
-
"
|
|
1653
|
-
"
|
|
1654
|
-
"
|
|
1655
|
-
"inputMode": "numeric",
|
|
1651
|
+
"help": "GLS requires the delivery postcode to show the detailed event history.",
|
|
1652
|
+
"pattern": "^(?=.{3,12}$)(?=.*[0-9])[A-Za-z0-9]+(?:[ \\-][A-Za-z0-9]+)*$",
|
|
1653
|
+
"maxLength": 12,
|
|
1654
|
+
"inputMode": "text",
|
|
1656
1655
|
"autoComplete": "postal-code"
|
|
1657
1656
|
}
|
|
1658
1657
|
],
|
|
@@ -12,6 +12,7 @@ import { adapter as brt } from '../carriers/brt/adapter.js';
|
|
|
12
12
|
import { adapter as cChezVous } from '../carriers/c-chez-vous/adapter.js';
|
|
13
13
|
import { adapter as canadaPost } from '../carriers/canada-post/adapter.js';
|
|
14
14
|
import { adapter as canpar } from '../carriers/canpar/adapter.js';
|
|
15
|
+
import { adapter as chronopost } from '../carriers/chronopost/adapter.js';
|
|
15
16
|
import { adapter as ciblex } from '../carriers/ciblex/adapter.js';
|
|
16
17
|
import { adapter as cne } from '../carriers/cne/adapter.js';
|
|
17
18
|
import { adapter as colisPrive } from '../carriers/colis-prive/adapter.js';
|
|
@@ -117,6 +118,7 @@ export const REGISTRY = {
|
|
|
117
118
|
"c-chez-vous": cChezVous,
|
|
118
119
|
"canada-post": canadaPost,
|
|
119
120
|
"canpar": canpar,
|
|
121
|
+
"chronopost": chronopost,
|
|
120
122
|
"ciblex": ciblex,
|
|
121
123
|
"cne": cne,
|
|
122
124
|
"colis-prive": colisPrive,
|
|
@@ -225,7 +227,7 @@ export const REGISTRY = {
|
|
|
225
227
|
"canada-post": "canada-post",
|
|
226
228
|
"canpar": "canpar",
|
|
227
229
|
"china-post": "universal",
|
|
228
|
-
"chronopost": "
|
|
230
|
+
"chronopost": "chronopost",
|
|
229
231
|
"ciblex": "ciblex",
|
|
230
232
|
"cne": "cne",
|
|
231
233
|
"colis-prive": "colis-prive",
|
package/dist/server/openapi.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-parcel-scraper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.0-main.397",
|
|
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",
|