universal-parcel-scraper 0.11.0 → 0.12.0-main.336

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 (45) hide show
  1. package/README.md +4 -4
  2. package/data/catalog.json +3 -3
  3. package/data/coverage-summary.json +3 -3
  4. package/data/detection-golden.json +140 -0
  5. package/dist/browser/scraper.js +22 -19
  6. package/dist/carriers/cne/adapter.d.ts +12 -0
  7. package/dist/carriers/cne/adapter.js +43 -0
  8. package/dist/carriers/cne/carrier.json +17 -5
  9. package/dist/carriers/cne/numbers.json +28 -2
  10. package/dist/carriers/cne/parser.d.ts +3 -0
  11. package/dist/carriers/cne/parser.js +74 -0
  12. package/dist/carriers/cne/statuses.json +63 -1
  13. package/dist/carriers/evri-uk/numbers.json +19 -2
  14. package/dist/carriers/intelcom/numbers.json +26 -0
  15. package/dist/carriers/intelcom/parser.js +9 -2
  16. package/dist/carriers/intelcom/statuses.json +9 -1
  17. package/dist/carriers/lbc-express/adapter.d.ts +13 -0
  18. package/dist/carriers/lbc-express/adapter.js +150 -0
  19. package/dist/carriers/lbc-express/carrier.json +20 -7
  20. package/dist/carriers/lbc-express/numbers.json +52 -0
  21. package/dist/carriers/lbc-express/parser.d.ts +3 -0
  22. package/dist/carriers/lbc-express/parser.js +68 -0
  23. package/dist/carriers/lbc-express/statuses.json +20 -1
  24. package/dist/carriers/nova-poshta/adapter.js +22 -5
  25. package/dist/carriers/nova-poshta/carrier.json +18 -6
  26. package/dist/carriers/nova-poshta/numbers.json +49 -0
  27. package/dist/carriers/nova-poshta/parser.d.ts +2 -0
  28. package/dist/carriers/nova-poshta/parser.js +98 -1
  29. package/dist/carriers/nova-poshta/statuses.json +93 -1
  30. package/dist/carriers/royal-mail/adapter.d.ts +3 -0
  31. package/dist/carriers/royal-mail/adapter.js +94 -47
  32. package/dist/carriers/royal-mail/numbers.json +13 -0
  33. package/dist/carriers/royal-mail/status.d.ts +1 -0
  34. package/dist/carriers/royal-mail/status.js +14 -0
  35. package/dist/carriers/royal-mail/statuses.json +151 -11
  36. package/dist/carriers/spx-ph/numbers.json +16 -0
  37. package/dist/carriers/spx-ph/parser.js +10 -4
  38. package/dist/carriers/spx-ph/statuses.json +56 -1
  39. package/dist/data/catalog.json +3 -3
  40. package/dist/generated/catalog.d.ts +3 -3
  41. package/dist/generated/catalog.js +3 -3
  42. package/dist/generated/registry.js +6 -2
  43. package/dist/providers/coverage.json +19 -15
  44. package/dist/server/openapi.json +1 -1
  45. 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: Apache-2.0](https://img.shields.io/badge/core-Apache--2.0-blue.svg)](LICENSE)
14
14
 
15
15
  <!-- GENERATED:summary -->
16
- **3,500+ carriers** through **94 dedicated adapters** and **5 universal fallbacks**
16
+ **3,500+ carriers** through **96 dedicated adapters** and **5 universal fallbacks**
17
17
 
18
18
  <sub>119 catalog entries · 58 countries represented</sub>
19
19
  <!-- /GENERATED:summary -->
@@ -33,9 +33,9 @@ its own as a command, a Node library or an HTTP server.
33
33
  ## Benchmark
34
34
 
35
35
  <!-- GENERATED:coverage -->
36
- In a curated benchmark of 113 carriers, dedicated adapters return tracking history for **66**, rising to **79** with all fallbacks enabled. The best single aggregator returns 53.
36
+ In a curated benchmark of 113 carriers, dedicated adapters return tracking history for **69**, rising to **82** with all fallbacks enabled. The best single aggregator returns 53.
37
37
 
38
- <img src="docs/assets/coverage.svg" alt="Carriers with tracking history: This project, all fallbacks enabled 79, This project, dedicated adapters alone 66, ParcelsApp 53, Postal Ninja 43, 17TRACK 40, Ship24 42, UPU 12." width="760">
38
+ <img src="docs/assets/coverage.svg" alt="Carriers with tracking history: This project, all fallbacks enabled 82, This project, dedicated adapters alone 69, ParcelsApp 53, Postal Ninja 43, 17TRACK 40, Ship24 42, UPU 12." width="760">
39
39
  <!-- /GENERATED:coverage -->
40
40
 
41
41
  [Method and results per carrier](providers/COVERAGE.md)
@@ -149,7 +149,7 @@ createTracker({ providers: ['ParcelsApp', 'Ship24', '17TRACK', 'Postal Ninja', '
149
149
  <!-- GENERATED:stages -->
150
150
  <img src="docs/assets/stages.svg" alt="DHL: Die Sendung wurde in das Zustellfahrzeug geladen.; Mondial Relay: En cours de livraison; Correios Brazil: Objeto saiu para entrega ao destinatário; Correos Express: EN REPARTO; Yamato Transport: 配達中; La Poste / Colissimo: DISTOU. All are filed under out_for_delivery." width="760">
151
151
 
152
- The carrier folders record 1,730 such statuses, each filed under one stage.
152
+ The carrier folders record 1,779 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
@@ -4054,7 +4054,7 @@
4054
4054
  "timezone": "UTC",
4055
4055
  "tracking": {
4056
4056
  "mode": "automatic",
4057
- "adapter": "universal"
4057
+ "adapter": "cne"
4058
4058
  },
4059
4059
  "canaryUrl": "https://www.cne.com/track",
4060
4060
  "trackingUrlTemplate": "https://www.cne.com/en/track?no={trackingNumber}",
@@ -4316,10 +4316,10 @@
4316
4316
  "timezone": "Asia/Manila",
4317
4317
  "tracking": {
4318
4318
  "mode": "automatic",
4319
- "adapter": "universal"
4319
+ "adapter": "lbc-express",
4320
+ "localClocks": true
4320
4321
  },
4321
4322
  "canaryUrl": "https://www.lbcexpress.com/track/",
4322
- "trackingUrlTemplate": "https://www.lbcexpress.com/track/?tracking_no={trackingNumber}",
4323
4323
  "linkRules": [
4324
4324
  {
4325
4325
  "domains": [
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "catalog": 119,
3
- "activeAdapters": 94,
3
+ "activeAdapters": 96,
4
4
  "countries": 58,
5
5
  "referenceCarriers": 113,
6
6
  "comparison": {
7
- "combined": 79,
8
- "direct": 66,
7
+ "combined": 82,
8
+ "direct": 69,
9
9
  "providers": {
10
10
  "ParcelsApp": 53,
11
11
  "Postal Ninja": 43,
@@ -837,6 +837,28 @@
837
837
  "ciblex"
838
838
  ]
839
839
  },
840
+ {
841
+ "input": "100000000001",
842
+ "carrier": "unknown",
843
+ "confidence": "low",
844
+ "candidates": [
845
+ "fedex",
846
+ "dpd-fr",
847
+ "mondial-relay",
848
+ "colis-prive",
849
+ "mrw",
850
+ "purolator",
851
+ "sf-express",
852
+ "sto",
853
+ "zto",
854
+ "yamato",
855
+ "j-and-t",
856
+ "lbc-express"
857
+ ],
858
+ "asked": [
859
+ "colis-prive"
860
+ ]
861
+ },
840
862
  {
841
863
  "input": "1000478495775",
842
864
  "carrier": "unknown",
@@ -1336,6 +1358,28 @@
1336
1358
  "colis-prive"
1337
1359
  ]
1338
1360
  },
1361
+ {
1362
+ "input": "142062960271",
1363
+ "carrier": "unknown",
1364
+ "confidence": "low",
1365
+ "candidates": [
1366
+ "fedex",
1367
+ "dpd-fr",
1368
+ "mondial-relay",
1369
+ "colis-prive",
1370
+ "mrw",
1371
+ "purolator",
1372
+ "sf-express",
1373
+ "sto",
1374
+ "zto",
1375
+ "yamato",
1376
+ "j-and-t",
1377
+ "lbc-express"
1378
+ ],
1379
+ "asked": [
1380
+ "colis-prive"
1381
+ ]
1382
+ },
1339
1383
  {
1340
1384
  "input": "1595193512495",
1341
1385
  "carrier": "unknown",
@@ -1743,6 +1787,30 @@
1743
1787
  "postlogistics"
1744
1788
  ]
1745
1789
  },
1790
+ {
1791
+ "input": "20710517491331",
1792
+ "carrier": "unknown",
1793
+ "confidence": "low",
1794
+ "candidates": [
1795
+ "dpd",
1796
+ "relais-colis",
1797
+ "ciblex",
1798
+ "hermes-de",
1799
+ "seur",
1800
+ "brt",
1801
+ "delhivery",
1802
+ "dpd-de",
1803
+ "dpd-uk",
1804
+ "nova-poshta"
1805
+ ],
1806
+ "asked": [
1807
+ "dpd",
1808
+ "seur",
1809
+ "brt",
1810
+ "hermes-de",
1811
+ "relais-colis"
1812
+ ]
1813
+ },
1746
1814
  {
1747
1815
  "input": "2103/11207088",
1748
1816
  "carrier": "nacex",
@@ -2333,6 +2401,18 @@
2333
2401
  "relais-colis"
2334
2402
  ]
2335
2403
  },
2404
+ {
2405
+ "input": "3A5V609829713",
2406
+ "carrier": "unknown",
2407
+ "confidence": "none",
2408
+ "candidates": []
2409
+ },
2410
+ {
2411
+ "input": "3A5V721250488",
2412
+ "carrier": "unknown",
2413
+ "confidence": "none",
2414
+ "candidates": []
2415
+ },
2336
2416
  {
2337
2417
  "input": "3SA5210528875",
2338
2418
  "carrier": "spring-gds",
@@ -2941,6 +3021,29 @@
2941
3021
  "ciblex"
2942
3022
  ]
2943
3023
  },
3024
+ {
3025
+ "input": "59001766134310",
3026
+ "carrier": "unknown",
3027
+ "confidence": "low",
3028
+ "candidates": [
3029
+ "dpd",
3030
+ "relais-colis",
3031
+ "ciblex",
3032
+ "seur",
3033
+ "brt",
3034
+ "delhivery",
3035
+ "dpd-de",
3036
+ "dpd-uk",
3037
+ "nova-poshta"
3038
+ ],
3039
+ "asked": [
3040
+ "dpd",
3041
+ "seur",
3042
+ "brt",
3043
+ "relais-colis",
3044
+ "ciblex"
3045
+ ]
3046
+ },
2944
3047
  {
2945
3048
  "input": "5N30053962734",
2946
3049
  "carrier": "la-poste",
@@ -4536,6 +4639,18 @@
4536
4639
  "dhl"
4537
4640
  ]
4538
4641
  },
4642
+ {
4643
+ "input": "CRIN26062320118472",
4644
+ "carrier": "unknown",
4645
+ "confidence": "none",
4646
+ "candidates": []
4647
+ },
4648
+ {
4649
+ "input": "CRIN26082718020483",
4650
+ "carrier": "unknown",
4651
+ "confidence": "none",
4652
+ "candidates": []
4653
+ },
4539
4654
  {
4540
4655
  "input": "CV000562646ES",
4541
4656
  "carrier": "unknown",
@@ -5013,6 +5128,15 @@
5013
5128
  "hermes-de"
5014
5129
  ]
5015
5130
  },
5131
+ {
5132
+ "input": "H00RVA0538924343",
5133
+ "carrier": "unknown",
5134
+ "confidence": "low",
5135
+ "candidates": [
5136
+ "evri",
5137
+ "evri-uk"
5138
+ ]
5139
+ },
5016
5140
  {
5017
5141
  "input": "H0595C0000314450",
5018
5142
  "carrier": "unknown",
@@ -5772,6 +5896,14 @@
5772
5896
  "spx-ph"
5773
5897
  ]
5774
5898
  },
5899
+ {
5900
+ "input": "PH2542959127193",
5901
+ "carrier": "unknown",
5902
+ "confidence": "low",
5903
+ "candidates": [
5904
+ "spx-ph"
5905
+ ]
5906
+ },
5775
5907
  {
5776
5908
  "input": "PHAS9C0419962810128017D",
5777
5909
  "carrier": "correos-spain",
@@ -6663,6 +6795,14 @@
6663
6795
  "yunexpress"
6664
6796
  ]
6665
6797
  },
6798
+ {
6799
+ "input": "YT660296803GB",
6800
+ "carrier": "royal-mail",
6801
+ "confidence": "high",
6802
+ "candidates": [
6803
+ "royal-mail"
6804
+ ]
6805
+ },
6666
6806
  {
6667
6807
  "input": "YT8879092508074",
6668
6808
  "carrier": "yto",
@@ -3131,7 +3131,7 @@ var ce = {
3131
3131
  timezone: "UTC",
3132
3132
  tracking: {
3133
3133
  mode: "automatic",
3134
- adapter: "universal"
3134
+ adapter: "cne"
3135
3135
  },
3136
3136
  canaryUrl: "https://www.cne.com/track",
3137
3137
  trackingUrlTemplate: "https://www.cne.com/en/track?no={trackingNumber}",
@@ -3312,10 +3312,10 @@ var ce = {
3312
3312
  timezone: "Asia/Manila",
3313
3313
  tracking: {
3314
3314
  mode: "automatic",
3315
- adapter: "universal"
3315
+ adapter: "lbc-express",
3316
+ localClocks: !0
3316
3317
  },
3317
3318
  canaryUrl: "https://www.lbcexpress.com/track/",
3318
- trackingUrlTemplate: "https://www.lbcexpress.com/track/?tracking_no={trackingNumber}",
3319
3319
  linkRules: [{
3320
3320
  domains: ["lbcexpress.com"],
3321
3321
  params: ["tracking_no"]
@@ -8288,7 +8288,10 @@ var gs = { carriers: [
8288
8288
  "17TRACK": "no_history",
8289
8289
  "Postal Ninja": 4,
8290
8290
  UPU: "no_history"
8291
- } }]
8291
+ } }, {
8292
+ note: "Alternate public reference returned eight identity-bound full-history scans in an interactive official session; fresh automated browser retrieval remains denied: https://www.reddit.com/r/royalmail/comments/1wh27pw/cant_track_my_shipment/",
8293
+ results: {}
8294
+ }]
8292
8295
  },
8293
8296
  {
8294
8297
  carrier: "swiss-post",
@@ -9728,7 +9731,7 @@ var gs = { carriers: [
9728
9731
  direct: "No adapter",
9729
9732
  sample: "Blocked",
9730
9733
  references: [{
9731
- note: "Domestic UK bootstrap requires a challenge; Evri International is a separate adapter.",
9734
+ note: "The official interactive navigation flow obtained rotating keys and six dated parcel scans. Fresh automated browser sessions are denied the keys: https://www.trustpilot.com/review/printerval.co.uk",
9732
9735
  results: {
9733
9736
  Ship24: "unverified",
9734
9737
  ParcelsApp: "unverified",
@@ -9756,9 +9759,9 @@ var gs = { carriers: [
9756
9759
  {
9757
9760
  carrier: "intelcom",
9758
9761
  direct: "Yes (Canada)",
9759
- sample: "Not found",
9762
+ sample: "Not found; alternate ✓ 6",
9760
9763
  references: [{
9761
- note: "Public Intelcom reference returned not_found; successful live history is unverified: https://www.justanswer.com/software/vud02-intelcom-package-not-delivered-address-dispute.html",
9764
+ note: "Comparison reference returned not_found: https://www.justanswer.com/software/vud02-intelcom-package-not-delivered-address-dispute.html. An alternate public reference returned six scans: https://intelcom.ca/en/track-your-package/?tracking-id=CRIN26082718020483",
9762
9765
  results: {
9763
9766
  Ship24: "unverified",
9764
9767
  ParcelsApp: "unverified",
@@ -9800,10 +9803,10 @@ var gs = { carriers: [
9800
9803
  },
9801
9804
  {
9802
9805
  carrier: "lbc-express",
9803
- direct: "No adapter",
9804
- sample: "Blocked",
9806
+ direct: "Yes (local Chromium)",
9807
+ sample: "Unverified; alternate ✓ 6",
9805
9808
  references: [{
9806
- note: "Public LBC reference: https://www.trustpilot.com/review/www.lbcexpress.com. Official anonymous retrieval is blocked.",
9809
+ note: "Original comparison reference has not been retried through the implemented browser flow: https://www.trustpilot.com/review/www.lbcexpress.com. An alternate public reference returned six unique date-only scans in a fresh automated browser: https://uk.trustpilot.com/review/www.lbcexpress.com",
9807
9810
  results: {
9808
9811
  Ship24: "unverified",
9809
9812
  ParcelsApp: "unverified",
@@ -9815,10 +9818,10 @@ var gs = { carriers: [
9815
9818
  },
9816
9819
  {
9817
9820
  carrier: "nova-poshta",
9818
- direct: "Yes (Ukraine; summary)",
9819
- sample: "Unverified",
9821
+ direct: "Yes (Ukraine)",
9822
+ sample: "✓ 10",
9820
9823
  references: [{
9821
- note: "Anonymous summary schema and explicit not-found response are established; successful live retrieval is unverified.",
9824
+ note: "Public Ukrainian waybill returned ten identity-bound movement scans: https://www.vidhuk.ua/uk/novaya-pochta-nova-poshta/review-2377100. A redirected import waybill returned twenty-two scans: https://www.vidhuk.ua/uk/novaya-pochta-nova-poshta/review-2376836",
9822
9825
  results: {
9823
9826
  Ship24: "unverified",
9824
9827
  ParcelsApp: "unverified",
@@ -9831,9 +9834,9 @@ var gs = { carriers: [
9831
9834
  {
9832
9835
  carrier: "spx-ph",
9833
9836
  direct: "Yes (Philippines)",
9834
- sample: "Unverified",
9837
+ sample: "✓ 10",
9835
9838
  references: [{
9836
- note: "Anonymous single-parcel schema is established; successful live history is unverified.",
9839
+ note: "Public marketplace reference returned ten visible identity-bound scans: https://www.reddit.com/r/ShopeePH/comments/1i67ifq/",
9837
9840
  results: {
9838
9841
  Ship24: "unverified",
9839
9842
  ParcelsApp: "unverified",
@@ -9845,10 +9848,10 @@ var gs = { carriers: [
9845
9848
  },
9846
9849
  {
9847
9850
  carrier: "cne",
9848
- direct: "No adapter",
9849
- sample: "Blocked",
9851
+ direct: "Yes",
9852
+ sample: "✓ 9",
9850
9853
  references: [{
9851
- note: "Official tracking requires a generated signature and challenges anonymous retrieval.",
9854
+ note: "Public CNE reference returned nine movement scans with unresolved unqualified clocks: https://nz.trustpilot.com/review/oriental-secrets.com. A second public reference returned a registration scan: https://ca.trustpilot.com/review/firalli.com?page=8",
9852
9855
  results: {
9853
9856
  Ship24: "unverified",
9854
9857
  ParcelsApp: "unverified",
@@ -9863,7 +9866,7 @@ var gs = { carriers: [
9863
9866
  direct: "No adapter",
9864
9867
  sample: "Blocked",
9865
9868
  references: [{
9866
- note: "Official tracking front door blocks anonymous retrieval.",
9869
+ note: "The current corporate tracking form posts to the public tracking service, which returns access denied. This is not evidence of parcel absence.",
9867
9870
  results: {
9868
9871
  Ship24: "unverified",
9869
9872
  ParcelsApp: "unverified",
@@ -0,0 +1,12 @@
1
+ import type { AdapterFactory, TrackingContext } from '../../core/adapter/index.js';
2
+ import { type StepRecorder } from '../../core/telemetry/index.js';
3
+ export declare class CneTracker {
4
+ private readonly options;
5
+ constructor(options?: {
6
+ fetcher?: typeof fetch;
7
+ recorder?: StepRecorder;
8
+ userAgent?: string;
9
+ });
10
+ fetch(raw: string, context?: TrackingContext): Promise<import("../../index.js").CarrierResult>;
11
+ }
12
+ export declare const adapter: AdapterFactory;
@@ -0,0 +1,43 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { TransportError, UpstreamHttpError } from '../../core/errors/index.js';
3
+ import { runSteps } from '../../core/runner/index.js';
4
+ import { NOOP_RECORDER } from '../../core/telemetry/index.js';
5
+ import { fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
6
+ import { normalizeCneNumber, parseCne } from './parser.js';
7
+ const ENDPOINT = 'https://wapi.cne.com/tracking/officialWebsite';
8
+ // The anonymous website's public WASM client prefixes inputs before MD5:
9
+ // https://www.cne.com/assets/wasm/wasm_md5_bg-074da2d9.wasm
10
+ // This is a form-protocol constant, not an account credential. Keep its space.
11
+ const PUBLIC_FORM_PREFIX = '01979c0bfa33772dbb3f5c07bd651b21 ';
12
+ const sign = (input) => createHash('md5').update(PUBLIC_FORM_PREFIX + input).digest('hex');
13
+ export class CneTracker {
14
+ options;
15
+ constructor(options = {}) {
16
+ this.options = options;
17
+ }
18
+ async fetch(raw, context = {}) {
19
+ const number = normalizeCneNumber(raw);
20
+ return runSteps({ carrier: 'cne', signal: context.signal, budgetMs: context.budgetMs ?? 15_000,
21
+ recorder: this.options.recorder ?? NOOP_RECORDER }, [{ id: 'direct', run: async ({ signal, remainingMs }) => {
22
+ const timestamp = Date.now().toString();
23
+ try {
24
+ const { bytes } = await fetchBounded(`${ENDPOINT}?t=${timestamp}`, { method: 'POST', signal,
25
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/plain, */*',
26
+ Origin: 'https://www.cne.com', Referer: 'https://www.cne.com/', 'User-Agent': userAgentOf(this.options.userAgent),
27
+ signature: sign(timestamp.substring(0, 12) + number) },
28
+ body: JSON.stringify({ lan: 'en', logisticsNo: number, md5: sign(number + 'Track') }),
29
+ }, { provider: 'CNE Express', timeoutMs: Math.max(1, Math.floor(remainingMs)), maxBytes: 1_000_000, fetcher: this.options.fetcher });
30
+ return parseCne(parseJsonBytes(bytes, 'CNE Express'), number);
31
+ }
32
+ catch (error) {
33
+ if (error instanceof UpstreamHttpError && [404, 410].includes(error.status))
34
+ throw new TransportError('CNE Express', 'CNE tracking endpoint is unavailable', { cause: error });
35
+ throw error;
36
+ }
37
+ } }]);
38
+ }
39
+ }
40
+ export const adapter = environment => {
41
+ const tracker = new CneTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
42
+ return { id: 'cne', recordsSteps: true, steps: ['direct'], track: (input, context) => tracker.fetch(input.number, context) };
43
+ };
@@ -17,10 +17,15 @@
17
17
  "timezone": "UTC",
18
18
  "tracking": {
19
19
  "mode": "automatic",
20
- "adapter": "universal",
21
- "steps": []
20
+ "adapter": "cne",
21
+ "steps": [
22
+ "direct"
23
+ ]
22
24
  },
23
- "capabilities": [],
25
+ "capabilities": [
26
+ "history",
27
+ "location"
28
+ ],
24
29
  "portal": {
25
30
  "url": "https://www.cne.com/en/track?no={trackingNumber}",
26
31
  "canaryUrl": "https://www.cne.com/track",
@@ -30,8 +35,15 @@
30
35
  "location",
31
36
  "delivery_partner"
32
37
  ],
33
- "scraped": [],
34
- "discarded": [],
38
+ "scraped": [
39
+ "status",
40
+ "history",
41
+ "location"
42
+ ],
43
+ "discarded": [
44
+ "transfer_reference",
45
+ "merchant_reference"
46
+ ],
35
47
  "unavailable": [
36
48
  "eta",
37
49
  "weight"
@@ -1,5 +1,31 @@
1
1
  {
2
2
  "carrier": "cne",
3
- "gap": "no public sample found yet",
4
- "records": []
3
+ "records": [
4
+ {
5
+ "number": "3A5V721250488",
6
+ "role": "shipment",
7
+ "evidence": "public_shipment_report",
8
+ "source": {
9
+ "url": "https://ca.trustpilot.com/review/firalli.com?page=8"
10
+ },
11
+ "expect": {
12
+ "carrier": "unknown",
13
+ "confidence": "none"
14
+ },
15
+ "note": "Customer report explicitly attributes the number to CNE. Requires explicit carrier selection."
16
+ },
17
+ {
18
+ "number": "3A5V609829713",
19
+ "role": "shipment",
20
+ "evidence": "public_shipment_report",
21
+ "source": {
22
+ "url": "https://nz.trustpilot.com/review/oriental-secrets.com"
23
+ },
24
+ "expect": {
25
+ "carrier": "unknown",
26
+ "confidence": "none"
27
+ },
28
+ "note": "Customer report explicitly attributes the number to CNE and describes a return."
29
+ }
30
+ ]
5
31
  }
@@ -0,0 +1,3 @@
1
+ import type { CarrierResult } from '../../core/result/index.js';
2
+ export declare function normalizeCneNumber(raw: string): string;
3
+ export declare function parseCne(payload: unknown, raw: string): CarrierResult;
@@ -0,0 +1,74 @@
1
+ import { normalizeTrackingNumber } from '../../core/detection/index.js';
2
+ import { IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
3
+ import { classifyWording, languageStageStatus } from '../../core/status/index.js';
4
+ import { explicitOffsetTime } from '../../core/time/index.js';
5
+ import { clean, cleanScalar } from '../../core/transport/index.js';
6
+ import { isRecord } from '../../core/types.js';
7
+ const PROVIDER = 'CNE Express';
8
+ const WORDING = {
9
+ 'Parcel Infomation Received': 'registered', 'Picked Up by Courier': 'accepted',
10
+ 'Shipment Loaded': 'in_transit', 'Departed CNE Facility': 'in_transit',
11
+ 'Arrived at CNE Facility': 'in_transit', 'Inbound scan': 'in_transit',
12
+ 'Shipment Processed': 'in_transit', 'Sorting failed []': 'exception',
13
+ "It was returned to customer's address by CNE": 'returned',
14
+ };
15
+ export function normalizeCneNumber(raw) {
16
+ const number = normalizeTrackingNumber(raw);
17
+ if (!/^[A-Z0-9]{6,40}$/.test(number))
18
+ throw new InvalidInputError(PROVIDER, 'CNE requires an alphanumeric parcel reference');
19
+ return number;
20
+ }
21
+ function scanTime(value) {
22
+ const match = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-](\d{2}):?(\d{2}))$/i.exec(value);
23
+ if (!match || (match[1] !== undefined && (Number(match[1]) > 14 || Number(match[2]) > 59
24
+ || (Number(match[1]) === 14 && Number(match[2]) !== 0))))
25
+ return null;
26
+ return explicitOffsetTime(value);
27
+ }
28
+ function scan(raw) {
29
+ if (!isRecord(raw) || !clean(raw.details, 1000))
30
+ throw new SchemaError(PROVIDER, 'CNE returned an invalid movement');
31
+ const description = clean(raw.details, 1000), clock = clean(raw.date, 64);
32
+ const location = clean(raw.place, 200), stage = WORDING[description];
33
+ const mapped = stage ? { stage, source: 'carrier_map' } : classifyWording(description, 'pending');
34
+ const time = scanTime(clock);
35
+ return { description, ...(location ? { location } : {}),
36
+ ...(time ? { time: time.iso } : clock ? { provider_time_text: clock } : {}),
37
+ ...(mapped.source !== 'none' ? { stage: mapped.stage, stage_source: mapped.source } : {}),
38
+ ...(cleanScalar(raw.state) ? { provider_code: cleanScalar(raw.state) } : {}) };
39
+ }
40
+ export function parseCne(payload, raw) {
41
+ const number = normalizeCneNumber(raw);
42
+ if (!isRecord(payload) || typeof payload.ReturnValue !== 'number')
43
+ throw new SchemaError(PROVIDER, 'CNE returned an invalid envelope');
44
+ if (payload.ReturnValue !== 1) {
45
+ if (payload.ReturnValue === 0 && payload.cMess === '订单不存在'
46
+ && payload.Response_Info === undefined && payload.trackingEventList === undefined)
47
+ throw new NotFoundError(PROVIDER);
48
+ throw new IndeterminateError(PROVIDER, 'CNE did not complete the lookup');
49
+ }
50
+ const identity = payload.Response_Info;
51
+ if (!isRecord(identity) || clean(identity.trackingNbr) !== number)
52
+ throw new SchemaError(PROVIDER, 'CNE returned a different shipment');
53
+ // These are echoed source identifiers, unlike referNbr and transNbr, which
54
+ // may be merchant and downstream references. Do not follow those aliases.
55
+ if (identity.Number_t !== undefined && clean(identity.Number_t) !== number)
56
+ throw new SchemaError(PROVIDER, 'CNE returned conflicting shipment identifiers');
57
+ if (!Array.isArray(payload.trackingEventList) || payload.trackingEventList.length > 1000)
58
+ throw new SchemaError(PROVIDER, 'CNE returned an invalid history');
59
+ if (!payload.trackingEventList.length)
60
+ throw new IndeterminateError(PROVIDER, 'CNE returned no shipment movements');
61
+ // The official client reverses the oldest-first array. Preserve that order
62
+ // even when its unqualified cross-border clocks cannot resolve to instants.
63
+ const events = payload.trackingEventList.map(scan).reverse();
64
+ const latest = events[0];
65
+ const summaryStage = cleanScalar(identity.status) === '8' ? 'returned' : undefined;
66
+ const stage = summaryStage ?? latest.stage;
67
+ const source = summaryStage ? 'carrier_map' : latest.stage_source;
68
+ const country = clean(identity.Destination, 64).toUpperCase();
69
+ return { status: stage ? languageStageStatus(stage) : 'unknown',
70
+ ...(stage ? { current_stage: stage, current_stage_source: source } : {}),
71
+ last_status_text: latest.description, last_update: latest.time ?? null,
72
+ ...(stage === 'delivered' && latest.time ? { delivered_at: latest.time } : {}),
73
+ ...(/^[A-Z]{2}$/.test(country) ? { destination_country: country } : {}), events: events.slice(0, 100) };
74
+ }
@@ -1,4 +1,66 @@
1
1
  {
2
2
  "carrier": "cne",
3
- "entries": []
3
+ "entries": [
4
+ {
5
+ "wording": "Parcel Infomation Received",
6
+ "language": "en",
7
+ "stage": "registered",
8
+ "confirmedBy": "live"
9
+ },
10
+ {
11
+ "wording": "Picked Up by Courier",
12
+ "language": "en",
13
+ "stage": "accepted",
14
+ "confirmedBy": "live"
15
+ },
16
+ {
17
+ "wording": "Shipment Loaded",
18
+ "language": "en",
19
+ "stage": "in_transit",
20
+ "confirmedBy": "live"
21
+ },
22
+ {
23
+ "wording": "Departed CNE Facility",
24
+ "language": "en",
25
+ "stage": "in_transit",
26
+ "confirmedBy": "live"
27
+ },
28
+ {
29
+ "wording": "Arrived at CNE Facility",
30
+ "language": "en",
31
+ "stage": "in_transit",
32
+ "confirmedBy": "live"
33
+ },
34
+ {
35
+ "wording": "Inbound scan",
36
+ "language": "en",
37
+ "stage": "in_transit",
38
+ "confirmedBy": "live"
39
+ },
40
+ {
41
+ "wording": "Shipment Processed",
42
+ "language": "en",
43
+ "stage": "in_transit",
44
+ "confirmedBy": "live"
45
+ },
46
+ {
47
+ "wording": "Sorting failed []",
48
+ "language": "en",
49
+ "stage": "exception",
50
+ "confirmedBy": "live"
51
+ },
52
+ {
53
+ "wording": "It was returned to customer's address by CNE",
54
+ "language": "en",
55
+ "stage": "returned",
56
+ "confirmedBy": "live"
57
+ },
58
+ {
59
+ "code": "8",
60
+ "wording": "退件",
61
+ "language": "zh",
62
+ "stage": "returned",
63
+ "confirmedBy": "live"
64
+ }
65
+ ]
4
66
  }