diff --git a/messages/en.json b/messages/en.json index cf0bac2..ae226d5 100644 --- a/messages/en.json +++ b/messages/en.json @@ -208,6 +208,8 @@ "laposte": "La Poste", "colissimo": "Colissimo", "chronopost": "Chronopost", + "mondialrelay": "Mondial Relay", + "speedpak": "SpeedPAK", "usps": "USPS", "ups": "UPS", "fedex": "FedEx", diff --git a/messages/fr.json b/messages/fr.json index 0fa9b24..85d98c5 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -208,6 +208,8 @@ "laposte": "La Poste", "colissimo": "Colissimo", "chronopost": "Chronopost", + "mondialrelay": "Mondial Relay", + "speedpak": "SpeedPAK", "usps": "USPS", "ups": "UPS", "fedex": "FedEx", diff --git a/src/components/shipments/shipment-card.tsx b/src/components/shipments/shipment-card.tsx index 5f3cab8..5a9fd04 100644 --- a/src/components/shipments/shipment-card.tsx +++ b/src/components/shipments/shipment-card.tsx @@ -50,7 +50,7 @@ export function ShipmentCard({ const { locale } = useI18n(); const protection = getShipmentProtection(shipment); const trackingUrl = shipment.trackingNumber - ? buildTrackingUrl(shipment.trackingNumber, shipment.carrier) + ? buildTrackingUrl(shipment.trackingNumber, shipment.carrier, locale) : null; const platformOrderUrl = buildPlatformOrderUrl( shipment.platform, diff --git a/src/lib/shipment-utils.test.ts b/src/lib/shipment-utils.test.ts index 49ed3f5..71275a7 100644 --- a/src/lib/shipment-utils.test.ts +++ b/src/lib/shipment-utils.test.ts @@ -69,6 +69,30 @@ describe("detectCarrier", () => { it("detects La Poste international format", () => { expect(detectCarrier("6A12345678901")).toBe("laposte"); }); + + it("detects La Poste S10 lettre suivie / Colissimo", () => { + expect(detectCarrier("RV123456789FR")).toBe("laposte"); + }); + + it("detects La Poste S10 with alphanumeric prefix (6A…FR)", () => { + expect(detectCarrier("6A123456789FR")).toBe("laposte"); + }); + + it("detects Mondial Relay 8-digit expedition numbers", () => { + expect(detectCarrier("12345678")).toBe("mondialrelay"); + }); + + it("detects SpeedPAK / Orange Connex S10 from China", () => { + expect(detectCarrier("LK123456789CN")).toBe("speedpak"); + }); + + it("detects SpeedPAK Standard (ES… 28 chars)", () => { + expect(detectCarrier("ES10024287564060001010001D0N")).toBe("speedpak"); + }); + + it("detects SpeedPAK Economy (EE… 28 chars)", () => { + expect(detectCarrier("EE1000080117450AB01010001N0N")).toBe("speedpak"); + }); }); describe("buildTrackingUrl", () => { @@ -76,8 +100,26 @@ describe("buildTrackingUrl", () => { expect(buildTrackingUrl("6A12345678901")).toContain("laposte.fr"); }); - it("falls back to 17track for unknown carriers", () => { - expect(buildTrackingUrl("XYZ123")).toContain("17track.net"); + it("builds Mondial Relay URL", () => { + expect(buildTrackingUrl("12345678")).toContain("mondialrelay.fr"); + }); + + it("builds 17track URL for SpeedPAK / Orange Connex", () => { + expect(buildTrackingUrl("LK123456789CN", null, "fr")).toContain( + "17track.net" + ); + }); + + it("builds 17track URL for official SpeedPAK ES numbers", () => { + expect( + buildTrackingUrl("ES10024287564060001010001D0N", null, "fr") + ).toContain("17track.net"); + }); + + it("falls back to ParcelsApp for unknown carriers", () => { + expect(buildTrackingUrl("XYZ123", null, "en")).toContain( + "parcelsapp.com/en/tracking/" + ); }); }); diff --git a/src/lib/shipment-utils.ts b/src/lib/shipment-utils.ts index 5d58a20..ddea4dc 100644 --- a/src/lib/shipment-utils.ts +++ b/src/lib/shipment-utils.ts @@ -251,16 +251,41 @@ export type DetectedCarrier = | "laposte" | "colissimo" | "chronopost" + | "mondialrelay" + | "speedpak" | "usps" | "ups" | "fedex" | "dhl" | "unknown"; +/** Préfixes SpeedPAK / Orange Connex (eBay Chine → Europe). */ +const SPEEDPAK_PREFIXES = [ + // Format officiel Orange Connex (28 car. : ES/EE/EX/EM + 26). + "ES", + "EE", + "EX", + "EM", + // Anciens / variantes S10 Chine parfois vues sur eBay. + "LK", + "LM", + "LX", + "LY", + "LZ", + "UC", + "JD", + "SF", +] as const; + export function detectCarrier(trackingNumber: string): DetectedCarrier { const normalized = trackingNumber.trim().toUpperCase().replace(/\s+/g, ""); if (!normalized) return "unknown"; + // SpeedPAK / Orange Connex — avant les heuristiques génériques (sinon → unknown → ParcelsApp). + if (/^(ES|EE|EX|EM)[A-Z0-9]{20,30}$/.test(normalized)) { + return "speedpak"; + } + if (/^1Z[A-Z0-9]{16}$/.test(normalized)) return "ups"; if (/^\d{20,22}$/.test(normalized) || /^9\d{21,}$/.test(normalized)) { return "usps"; @@ -268,19 +293,56 @@ export function detectCarrier(trackingNumber: string): DetectedCarrier { if (/^\d{12,15}$/.test(normalized) && normalized.startsWith("3")) { return "fedex"; } - if (/^\d{10,11}$/.test(normalized)) return "dhl"; - if (/^[A-Z]{2}\d{9}[A-Z]{2}$/.test(normalized)) return "laposte"; + + // Mondial Relay : n° d'expédition 8 chiffres (format classique). + if (/^\d{8}$/.test(normalized)) return "mondialrelay"; + + // DHL Express : 10 chiffres — 11 chiffres trop ambigus (souvent MR / autres). + if (/^\d{10}$/.test(normalized)) return "dhl"; + + // La Poste / Colissimo / Lettre suivie (S10 UPU, préfixe parfois chiffre+lettre ex. 6A…FR). + if (/^[A-Z0-9]{2}\d{9}[A-Z]{2}$/.test(normalized)) { + const destination = normalized.slice(-2); + // Variantes S10 Chine parfois routées SpeedPAK. + if ( + destination === "CN" && + SPEEDPAK_PREFIXES.some((prefix) => normalized.startsWith(prefix)) + ) { + return "speedpak"; + } + return "laposte"; + } + if (/^[A-Z0-9]{2}\d{9,11}$/.test(normalized)) return "laposte"; if (/^\d{13}$/.test(normalized)) return "colissimo"; + // Autres formats SpeedPAK / Orange Connex (longueur variable). + if ( + /^[A-Z0-9]{12,32}$/.test(normalized) && + SPEEDPAK_PREFIXES.some((prefix) => normalized.startsWith(prefix)) + ) { + return "speedpak"; + } + return "unknown"; } -export function buildTrackingUrl( +function buildParcelsAppUrl( trackingNumber: string, - carrier?: string | null + locale: "fr" | "en" = "fr" ): string { const code = encodeURIComponent(trackingNumber.trim()); + const lang = locale === "fr" ? "fr" : "en"; + return `https://parcelsapp.com/${lang}/tracking/${code}`; +} + +export function buildTrackingUrl( + trackingNumber: string, + carrier?: string | null, + locale: "fr" | "en" = "fr" +): string { + const trimmed = trackingNumber.trim(); + const code = encodeURIComponent(trimmed); const detected = carrier && carrier !== "unknown" ? (carrier as DetectedCarrier) @@ -291,6 +353,11 @@ export function buildTrackingUrl( case "colissimo": case "chronopost": return `https://www.laposte.fr/outils/suivre-vos-envois?code=${code}`; + case "mondialrelay": + return `https://www.mondialrelay.fr/suivi-de-colis/?numeroExpedition=${code}`; + case "speedpak": + // SpeedPAK / Orange Connex (eBay Chine) — suivi via 17track. + return `https://t.17track.net/en#nums=${code}`; case "usps": return `https://tools.usps.com/go/TrackConfirmAction?tLabels=${code}`; case "ups": @@ -300,7 +367,8 @@ export function buildTrackingUrl( case "dhl": return `https://www.dhl.com/fr-fr/home/tracking.html?tracking-id=${code}`; default: - return `https://t.17track.net/en#nums=${code}`; + // Agrégateur multi-transporteurs (MR, Colissimo, Lettre suivie…). + return buildParcelsAppUrl(trimmed, locale); } }