universal-parcel-scraper 0.904.0-main.4813 → 0.905.0-main.4818
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 +2 -1
- package/data/coverage-summary.json +1 -1
- package/data/detection-golden.json +3 -0
- package/dist/browser/scraper.js +6 -2
- package/dist/carriers/ecom-logistics/adapter.d.ts +18 -0
- package/dist/carriers/ecom-logistics/adapter.js +51 -0
- package/dist/carriers/ecom-logistics/carrier.json +39 -7
- package/dist/carriers/ecom-logistics/parser.d.ts +15 -0
- package/dist/carriers/ecom-logistics/parser.js +95 -0
- package/dist/carriers/ecom-logistics/status.d.ts +7 -0
- package/dist/carriers/ecom-logistics/status.js +22 -0
- package/dist/carriers/ecom-logistics/statuses.json +9 -1
- package/dist/carriers/ywe/statuses.json +1 -0
- package/dist/core/catalog/statusMaps.js +2 -1
- package/dist/core/status/language.js +5 -0
- package/dist/data/catalog.json +2 -1
- package/dist/generated/catalog.d.ts +2 -1
- package/dist/generated/catalog.js +2 -1
- package/dist/generated/recognition.js +1 -0
- 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 **676 dedicated adapters** and **5 universal fallbacks**
|
|
17
17
|
|
|
18
18
|
<sub>1201 catalog entries · 154 countries represented</sub>
|
|
19
19
|
<!-- /GENERATED:summary -->
|
|
@@ -163,7 +163,7 @@ the command line's `track --source ParcelsApp` does the same.
|
|
|
163
163
|
<!-- GENERATED:stages -->
|
|
164
164
|
<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">
|
|
165
165
|
|
|
166
|
-
The carrier folders record 18,
|
|
166
|
+
The carrier folders record 18,049 such statuses, each filed under one stage.
|
|
167
167
|
<!-- /GENERATED:stages -->
|
|
168
168
|
|
|
169
169
|
Wording nobody recorded yet goes through a classifier that reads seven European languages.
|
package/data/catalog.json
CHANGED
|
@@ -16599,7 +16599,8 @@
|
|
|
16599
16599
|
"timezone": "America/Toronto",
|
|
16600
16600
|
"tracking": {
|
|
16601
16601
|
"mode": "automatic",
|
|
16602
|
-
"adapter": "
|
|
16602
|
+
"adapter": "ecom-logistics",
|
|
16603
|
+
"recognitionRank": 576
|
|
16603
16604
|
},
|
|
16604
16605
|
"canaryUrl": "https://shipecom.com/track",
|
|
16605
16606
|
"trackingUrlTemplate": "https://shipecom.com/track/{trackingNumber}",
|
|
@@ -76609,6 +76609,9 @@
|
|
|
76609
76609
|
"confidence": "low",
|
|
76610
76610
|
"candidates": [
|
|
76611
76611
|
"ecom-logistics"
|
|
76612
|
+
],
|
|
76613
|
+
"asked": [
|
|
76614
|
+
"ecom-logistics"
|
|
76612
76615
|
]
|
|
76613
76616
|
},
|
|
76614
76617
|
{
|
package/dist/browser/scraper.js
CHANGED
|
@@ -13036,7 +13036,8 @@ var at = {
|
|
|
13036
13036
|
timezone: "America/Toronto",
|
|
13037
13037
|
tracking: {
|
|
13038
13038
|
mode: "automatic",
|
|
13039
|
-
adapter: "
|
|
13039
|
+
adapter: "ecom-logistics",
|
|
13040
|
+
recognitionRank: 576
|
|
13040
13041
|
},
|
|
13041
13042
|
canaryUrl: "https://shipecom.com/track",
|
|
13042
13043
|
trackingUrlTemplate: "https://shipecom.com/track/{trackingNumber}",
|
|
@@ -42190,7 +42191,9 @@ function Jl(e) {
|
|
|
42190
42191
|
if (El.test(t) || /(?<!\bnot (?:yet )?(?:been )?)\b(?:picked up|collected|received) by (?:the |a |our )?(?:last|final)[- ]mile (?:carrier|courier|(?:delivery )?partner|provider)s?\b/.test(t) || /^hand(?:ed )?over to (?:the )?(?:last|final)[- ]mile(?: (?:carrier|courier|(?:delivery )?partner|provider))?$/.test(t)) return "in_transit";
|
|
42191
42192
|
if (/^(?:the |your )?(?:item|package|parcel|shipment) (?:has been |was |is )?(?:received for transport|(?:dropped off|deposited) by (?:the )?sender (?:at|with) (?:our |the |a )?postal partner)\b/.test(t)) return "accepted";
|
|
42192
42193
|
if (/^(?:load vehicle|scan ok gateway)$/.test(t) || /^(?:the )?item in process in office of exchange\b/.test(t) || /^acceptance of (?:the )?incoming (?:shipment|item|mail) at the exchange office$/.test(t)) return "in_transit";
|
|
42193
|
-
if (/^courier pick up$/.test(t) || /^shipment pick ?up$/.test(t)
|
|
42194
|
+
if (/^courier pick up$/.test(t) || /^shipment pick ?up$/.test(t)) return "accepted";
|
|
42195
|
+
if (/^中国邮政 ?已收取快件$/.test(t)) return "in_transit";
|
|
42196
|
+
if (/^(?:the )?salesperson (?:has )?(?:collects|collected|picks up|picked up) (?:the )?(?:package|parcel|shipment)$/.test(t)) return "accepted";
|
|
42194
42197
|
if (/^registered and being prepared$/.test(t)) return "registered";
|
|
42195
42198
|
if (/^left the branch(?: \d{1,3})?$/.test(t) || /^collo (?:in der nox niederlassung eingetroffen\. sendung eingetroffen|von der nox niederlassung weiterverladen\. sendung weiterverladen|is reloaded by the nox branch\. consignment reloaded)$/.test(t) || /^la spedizione (?:e'|e) regolarmente partita$|^tendered to (?:the )?international gateway$/.test(t)) return "in_transit";
|
|
42196
42199
|
if (/^parcel stored by (?:the )?customer$/.test(t)) return "accepted";
|
|
@@ -43055,6 +43058,7 @@ var cd = {
|
|
|
43055
43058
|
dsv: 436,
|
|
43056
43059
|
dynalogic: 120,
|
|
43057
43060
|
"early-bird": 350,
|
|
43061
|
+
"ecom-logistics": 576,
|
|
43058
43062
|
econt: 290,
|
|
43059
43063
|
ecoscooting: 655,
|
|
43060
43064
|
ecourier: 743,
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
/** Ecom Logistics' public tracking page, one number per path. */
|
|
5
|
+
export declare const ECOM_TRACKING = "https://shipecom.com/track/";
|
|
6
|
+
/** The tracking page's address for one number. */
|
|
7
|
+
export declare function ecomTrackingUrl(number: string): string;
|
|
8
|
+
export declare class EcomTracker {
|
|
9
|
+
private readonly options;
|
|
10
|
+
constructor(options?: {
|
|
11
|
+
fetcher?: typeof fetch;
|
|
12
|
+
recorder?: StepRecorder;
|
|
13
|
+
userAgent?: string;
|
|
14
|
+
});
|
|
15
|
+
private read;
|
|
16
|
+
fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
|
|
17
|
+
}
|
|
18
|
+
export declare const adapter: AdapterFactory;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
|
|
2
|
+
import { TransportError } from '../../core/errors/index.js';
|
|
3
|
+
import { runSteps } from '../../core/runner/index.js';
|
|
4
|
+
import { firstAttemptMs, networkRetryStep } from '../../core/runner/networkRetry.js';
|
|
5
|
+
import { NOOP_RECORDER } from '../../core/telemetry/index.js';
|
|
6
|
+
import { decodeText, fetchBounded, userAgentOf } from '../../core/transport/index.js';
|
|
7
|
+
import { CARRIER, ECOM_MAX_BYTES, normalizeEcomNumber, parseEcom, PROVIDER } from './parser.js';
|
|
8
|
+
/** Ecom Logistics' public tracking page, one number per path. */
|
|
9
|
+
export const ECOM_TRACKING = 'https://shipecom.com/track/';
|
|
10
|
+
const REDIRECTS = [301, 302, 303, 307, 308];
|
|
11
|
+
const DEFAULT_BUDGET_MS = 20_000;
|
|
12
|
+
/** The tracking page's address for one number. */
|
|
13
|
+
export function ecomTrackingUrl(number) {
|
|
14
|
+
return `${ECOM_TRACKING}${encodeURIComponent(number)}`;
|
|
15
|
+
}
|
|
16
|
+
export class EcomTracker {
|
|
17
|
+
options;
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.options = options;
|
|
20
|
+
}
|
|
21
|
+
async read(number, signal, timeoutMs) {
|
|
22
|
+
const { response, bytes } = await fetchBounded(ecomTrackingUrl(number), { method: 'GET', signal, headers: {
|
|
23
|
+
Accept: 'text/html', 'Accept-Language': 'en', 'User-Agent': userAgentOf(this.options.userAgent),
|
|
24
|
+
} }, { provider: PROVIDER, timeoutMs: Math.max(1, Math.floor(timeoutMs)), maxBytes: ECOM_MAX_BYTES, redirect: 'manual',
|
|
25
|
+
fetcher: this.options.fetcher, allowHttpStatuses: [...REDIRECTS, 404, 410] });
|
|
26
|
+
const html = decodeText(bytes);
|
|
27
|
+
// A number the page does not know comes with HTTP 404 and its own message; a redirect, or a 404 without it, is a moved route.
|
|
28
|
+
if (response.status !== 200 && !(response.status === 404 && html.includes('data-track-result="not_found"'))) {
|
|
29
|
+
throw new TransportError(PROVIDER, 'Ecom Logistics tracking page moved', { status: response.status });
|
|
30
|
+
}
|
|
31
|
+
return parseEcom(html, number);
|
|
32
|
+
}
|
|
33
|
+
async fetch(raw, context = {}) {
|
|
34
|
+
const number = normalizeEcomNumber(raw);
|
|
35
|
+
// A read that fails to reach the tracker, or hangs, is sent once more: the
|
|
36
|
+
// first try gets half of the budget.
|
|
37
|
+
return runSteps({ carrier: CARRIER, budgetMs: context.budgetMs ?? DEFAULT_BUDGET_MS, signal: context.signal,
|
|
38
|
+
recorder: this.options.recorder ?? NOOP_RECORDER }, [
|
|
39
|
+
{ id: 'direct', run: ({ signal, remainingMs }) => this.read(number, signal, firstAttemptMs(remainingMs)) },
|
|
40
|
+
networkRetryStep(({ signal, remainingMs }) => this.read(number, signal, remainingMs)),
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export const adapter = (environment) => {
|
|
45
|
+
const tracker = new EcomTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
|
|
46
|
+
return {
|
|
47
|
+
id: CARRIER, recordsSteps: true, steps: ['direct', 'retry'],
|
|
48
|
+
track: (input, context) => tracker.fetch(input.number, context),
|
|
49
|
+
recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizeEcomNumber(number))),
|
|
50
|
+
};
|
|
51
|
+
};
|
|
@@ -19,18 +19,50 @@
|
|
|
19
19
|
"timezone": "America/Toronto",
|
|
20
20
|
"tracking": {
|
|
21
21
|
"mode": "automatic",
|
|
22
|
-
"adapter": "
|
|
23
|
-
"steps": [
|
|
22
|
+
"adapter": "ecom-logistics",
|
|
23
|
+
"steps": [
|
|
24
|
+
"direct",
|
|
25
|
+
"retry"
|
|
26
|
+
],
|
|
27
|
+
"recognition": {
|
|
28
|
+
"rank": 576
|
|
29
|
+
}
|
|
24
30
|
},
|
|
25
|
-
"capabilities": [
|
|
31
|
+
"capabilities": [
|
|
32
|
+
"history",
|
|
33
|
+
"location"
|
|
34
|
+
],
|
|
26
35
|
"portal": {
|
|
27
36
|
"url": "https://shipecom.com/track/{trackingNumber}",
|
|
28
37
|
"searchUrl": "https://shipecom.com/track",
|
|
29
38
|
"canaryUrl": "https://shipecom.com/track",
|
|
30
|
-
"shows": [
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
"shows": [
|
|
40
|
+
"status",
|
|
41
|
+
"progress_rail",
|
|
42
|
+
"history",
|
|
43
|
+
"location",
|
|
44
|
+
"offset_timestamps",
|
|
45
|
+
"delivered_at",
|
|
46
|
+
"destination"
|
|
47
|
+
],
|
|
48
|
+
"scraped": [
|
|
49
|
+
"history",
|
|
50
|
+
"location",
|
|
51
|
+
"offset_timestamps"
|
|
52
|
+
],
|
|
53
|
+
"discarded": [
|
|
54
|
+
"status",
|
|
55
|
+
"progress_rail",
|
|
56
|
+
"delivered_at",
|
|
57
|
+
"destination"
|
|
58
|
+
],
|
|
59
|
+
"unavailable": [
|
|
60
|
+
"provider_code",
|
|
61
|
+
"eta",
|
|
62
|
+
"service_name",
|
|
63
|
+
"weight",
|
|
64
|
+
"proof_of_delivery"
|
|
65
|
+
]
|
|
34
66
|
},
|
|
35
67
|
"links": [
|
|
36
68
|
{
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CarrierResult } from '../../core/result/index.js';
|
|
2
|
+
export declare const PROVIDER = "Ecom Logistics";
|
|
3
|
+
export declare const CARRIER = "ecom-logistics";
|
|
4
|
+
export declare const ECOM_MAX_BYTES: number;
|
|
5
|
+
/** A number for Ecom Logistics' tracker: eight to thirty letters and digits. */
|
|
6
|
+
export declare function normalizeEcomNumber(raw: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* The page `shipecom.com/track/{number}`, which the server renders. Its result
|
|
9
|
+
* section names a state (`delivered`, `not_found`) and repeats the number
|
|
10
|
+
* asked. A known parcel has a status heading, a progress rail, the delivery
|
|
11
|
+
* time, the destination's postal code prefix and the tracking history. A number
|
|
12
|
+
* it does not know comes with HTTP 404, the state `not_found` and no history.
|
|
13
|
+
* Only the history is kept.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseEcom(html: string, raw: string): CarrierResult;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { load } from 'cheerio';
|
|
2
|
+
import { normalizeTrackingNumber } from '../../core/detection/index.js';
|
|
3
|
+
import { ChallengeError, IndeterminateError, InvalidInputError, NoHistoryError, NotFoundError, SchemaError } from '../../core/errors/index.js';
|
|
4
|
+
import { languageStageStatus } from '../../core/status/index.js';
|
|
5
|
+
import { explicitOffsetTime } from '../../core/time/index.js';
|
|
6
|
+
import { clean } from '../../core/transport/index.js';
|
|
7
|
+
import { ecomStage } from './status.js';
|
|
8
|
+
export const PROVIDER = 'Ecom Logistics';
|
|
9
|
+
export const CARRIER = 'ecom-logistics';
|
|
10
|
+
// A page is about 60 kB, and 90 kB for a number it does not know.
|
|
11
|
+
export const ECOM_MAX_BYTES = 1024 * 1024;
|
|
12
|
+
// Letters and digits, such as E, two digits, a letter and seven digits.
|
|
13
|
+
const NUMBER = /^(?=.*\d)[A-Z0-9]{8,30}$/;
|
|
14
|
+
// Bot checks and firewall refusals that could stand in for the page.
|
|
15
|
+
const CHALLENGE = /_cf_chl_opt|cf-turnstile|<title>\s*(?:Just a moment|Attention Required!|Access Denied)|g-recaptcha|h-captcha|The requested URL was rejected|Request Rejected/i;
|
|
16
|
+
const MAX_EVENTS = 200;
|
|
17
|
+
/** A number for Ecom Logistics' tracker: eight to thirty letters and digits. */
|
|
18
|
+
export function normalizeEcomNumber(raw) {
|
|
19
|
+
const number = typeof raw === 'string' && raw.length <= 64 ? normalizeTrackingNumber(raw) : '';
|
|
20
|
+
if (!NUMBER.test(number))
|
|
21
|
+
throw new InvalidInputError(PROVIDER, 'Ecom Logistics numbers are letters and digits');
|
|
22
|
+
return number;
|
|
23
|
+
}
|
|
24
|
+
/** The tracking history, newest first: each scan gives its wording, its instant with an offset and at times a place. */
|
|
25
|
+
function eventsOf($) {
|
|
26
|
+
const lists = $('ol.track-history');
|
|
27
|
+
if (lists.length > 1)
|
|
28
|
+
throw new SchemaError(PROVIDER, 'Ecom Logistics returned several histories');
|
|
29
|
+
const rows = lists.children('li.track-event').toArray();
|
|
30
|
+
if (rows.length > MAX_EVENTS)
|
|
31
|
+
throw new SchemaError(PROVIDER, 'Ecom Logistics returned an invalid history');
|
|
32
|
+
const events = rows.map((row) => {
|
|
33
|
+
const description = clean($(row).find('.track-event__title').first().text(), 300);
|
|
34
|
+
const clocks = $(row).find('time[datetime]');
|
|
35
|
+
const time = clocks.length === 1 ? explicitOffsetTime(clocks.attr('datetime') ?? '')?.iso : undefined;
|
|
36
|
+
if (!description || !time)
|
|
37
|
+
throw new SchemaError(PROVIDER, 'Ecom Logistics returned an invalid scan');
|
|
38
|
+
// The place follows the clock and a separator the page hides from screen readers.
|
|
39
|
+
const location = clean(clocks.parent().children('span:not([aria-hidden])').first().text(), 160);
|
|
40
|
+
const staged = ecomStage(description);
|
|
41
|
+
return {
|
|
42
|
+
time,
|
|
43
|
+
...(location ? { location } : {}),
|
|
44
|
+
description,
|
|
45
|
+
...(staged ? { stage: staged.stage, stage_source: staged.source } : {}),
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
// Newest first; the page lists them so, and a stable sort keeps scans of one instant in its order.
|
|
49
|
+
return events.sort((left, right) => Date.parse(right.time) - Date.parse(left.time));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The page `shipecom.com/track/{number}`, which the server renders. Its result
|
|
53
|
+
* section names a state (`delivered`, `not_found`) and repeats the number
|
|
54
|
+
* asked. A known parcel has a status heading, a progress rail, the delivery
|
|
55
|
+
* time, the destination's postal code prefix and the tracking history. A number
|
|
56
|
+
* it does not know comes with HTTP 404, the state `not_found` and no history.
|
|
57
|
+
* Only the history is kept.
|
|
58
|
+
*/
|
|
59
|
+
export function parseEcom(html, raw) {
|
|
60
|
+
const number = normalizeEcomNumber(raw);
|
|
61
|
+
if (CHALLENGE.test(html.slice(0, 64_000)))
|
|
62
|
+
throw new ChallengeError(PROVIDER, 'Ecom Logistics answered with a browser check');
|
|
63
|
+
const $ = load(html);
|
|
64
|
+
$('script, style, noscript').remove();
|
|
65
|
+
const result = $('main section[data-track-result]');
|
|
66
|
+
if (result.length !== 1)
|
|
67
|
+
throw new SchemaError(PROVIDER, 'Ecom Logistics returned an unexpected page');
|
|
68
|
+
const shown = clean(result.find('p .track-code').first().text(), 64);
|
|
69
|
+
if (!shown)
|
|
70
|
+
throw new SchemaError(PROVIDER, 'Ecom Logistics returned a page without the number asked');
|
|
71
|
+
if (normalizeTrackingNumber(shown) !== number)
|
|
72
|
+
throw new IndeterminateError(PROVIDER, 'Ecom Logistics answered for another number');
|
|
73
|
+
const state = result.attr('data-track-result');
|
|
74
|
+
if (state === 'not_found') {
|
|
75
|
+
if ($('ol.track-history').length)
|
|
76
|
+
throw new SchemaError(PROVIDER, 'Ecom Logistics returned an unexpected page');
|
|
77
|
+
throw new NotFoundError(PROVIDER, 'Ecom Logistics does not know this number');
|
|
78
|
+
}
|
|
79
|
+
if (!state || !/^[a-z_]{1,40}$/.test(state))
|
|
80
|
+
throw new SchemaError(PROVIDER, 'Ecom Logistics returned a page without its state');
|
|
81
|
+
const events = eventsOf($);
|
|
82
|
+
const newest = events[0];
|
|
83
|
+
if (!newest)
|
|
84
|
+
throw new NoHistoryError(PROVIDER);
|
|
85
|
+
const current = events.find((event) => event.stage);
|
|
86
|
+
const stage = current?.stage;
|
|
87
|
+
return {
|
|
88
|
+
status: stage ? languageStageStatus(stage) : 'unknown',
|
|
89
|
+
...(stage ? { current_stage: stage, current_stage_source: current.stage_source } : {}),
|
|
90
|
+
last_status_text: newest.description,
|
|
91
|
+
last_update: newest.time,
|
|
92
|
+
expected_delivery: null,
|
|
93
|
+
events,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ClassifiedWording, type Stage } from '../../core/status/index.js';
|
|
2
|
+
import { type CarrierStatusMap } from '../../core/status/statusMap.js';
|
|
3
|
+
/** The stage Ecom Logistics' map gives a wording in `normalizeStatusWording` form; one it does not list has none. */
|
|
4
|
+
export declare function ecomMappedStage(wording: string): Stage | undefined;
|
|
5
|
+
/** A scan's stage: the map, else the shared wording rules, else nothing. */
|
|
6
|
+
export declare function ecomStage(wording: string): ClassifiedWording | null;
|
|
7
|
+
export declare const statusMap: CarrierStatusMap;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { classifyWording } from '../../core/status/index.js';
|
|
2
|
+
import { normalizeStatusWording } from '../../core/status/statusMap.js';
|
|
3
|
+
import statuses from './statuses.json' with { type: 'json' };
|
|
4
|
+
/** The scan wordings of Ecom Logistics' tracker, which carry no code. Another wording falls to the shared wording rules. */
|
|
5
|
+
const WORDINGS = new Map(statuses.entries
|
|
6
|
+
.map((entry) => [normalizeStatusWording(entry.wording), entry.stage]));
|
|
7
|
+
/** The stage Ecom Logistics' map gives a wording in `normalizeStatusWording` form; one it does not list has none. */
|
|
8
|
+
export function ecomMappedStage(wording) {
|
|
9
|
+
return WORDINGS.get(wording);
|
|
10
|
+
}
|
|
11
|
+
/** A scan's stage: the map, else the shared wording rules, else nothing. */
|
|
12
|
+
export function ecomStage(wording) {
|
|
13
|
+
const mapped = ecomMappedStage(normalizeStatusWording(wording));
|
|
14
|
+
if (mapped)
|
|
15
|
+
return { stage: mapped, source: 'carrier_map' };
|
|
16
|
+
const worded = classifyWording(wording, 'in_transit');
|
|
17
|
+
return worded.source === 'none' ? null : worded;
|
|
18
|
+
}
|
|
19
|
+
export const statusMap = {
|
|
20
|
+
stage: (_code, wording) => ecomMappedStage(wording),
|
|
21
|
+
gaps: [],
|
|
22
|
+
};
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"carrier": "ecom-logistics",
|
|
3
|
-
"entries": [
|
|
3
|
+
"entries": [
|
|
4
|
+
{"wording": "Picked Up", "stage": "accepted", "language": "en", "confirmedBy": "live", "note": "Collected from the sender, with the town it was collected in."},
|
|
5
|
+
{"wording": "Sorted at our facility", "stage": "in_transit", "language": "en", "confirmedBy": "live"},
|
|
6
|
+
{"wording": "First Mile Inbound", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "Arrival at the hub from the collection run."},
|
|
7
|
+
{"wording": "Received - No Discrepancy", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "Checked in at the hub against the sender's manifest."},
|
|
8
|
+
{"wording": "Linehaul Outbound", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "Left the hub for the delivery area, hours before the round."},
|
|
9
|
+
{"wording": "Outbound onto Delivery Truck", "stage": "out_for_delivery", "language": "en", "confirmedBy": "live"},
|
|
10
|
+
{"wording": "Delivered", "stage": "delivered", "language": "en", "confirmedBy": "live"}
|
|
11
|
+
]
|
|
4
12
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
{"wording": "Container picked up by YWE,LAX01 Awaiting Item.", "stage": "accepted", "language": "en", "confirmedBy": "live", "firstSeen": "2026-10-24", "note": "YWE has the container the parcel flew in, before the parcel's own scan. Other hubs' codes take LAX01's place."},
|
|
6
6
|
{"wording": "Container received", "stage": "accepted", "language": "en", "confirmedBy": "live", "firstSeen": "2026-10-24", "note": "The hub received the container, between two of its pickup notes."},
|
|
7
7
|
{"wording": "Parcel received by in LAX01.", "stage": "accepted", "language": "en", "confirmedBy": "live", "firstSeen": "2026-10-24", "note": "The parcel's first scan at the hub. Other hubs' codes take LAX01's place."},
|
|
8
|
+
{"wording": "Parcel received by NJC01.", "stage": "accepted", "language": "en", "confirmedBy": "live", "firstSeen": "2026-10-29", "note": "The same first scan at the hub, worded without \"in\". Other hubs' codes take NJC01's place."},
|
|
8
9
|
{"wording": "Parcel departed from Facility.", "stage": "in_transit", "language": "en", "confirmedBy": "live", "firstSeen": "2026-10-24"},
|
|
9
10
|
{"wording": "Parcel received by transit facility.", "stage": "in_transit", "language": "en", "confirmedBy": "live", "firstSeen": "2026-10-26", "note": "A hub on the way between YWE's own, before the delivery station."},
|
|
10
11
|
{"wording": "In processing at transit facility.", "stage": "in_transit", "language": "en", "confirmedBy": "live", "firstSeen": "2026-10-26"},
|
|
@@ -191,6 +191,7 @@ import { statusMap as eCommerceInternational } from '../../carriers/e-commerce-i
|
|
|
191
191
|
import { statusMap as earlyBird } from '../../carriers/early-bird/status.js';
|
|
192
192
|
import { statusMap as easyMail } from '../../carriers/easy-mail/status.js';
|
|
193
193
|
import { statusMap as ecmsExpress } from '../../carriers/ecms-express/status.js';
|
|
194
|
+
import { statusMap as ecomLogistics } from '../../carriers/ecom-logistics/status.js';
|
|
194
195
|
import { statusMap as econt } from '../../carriers/econt/status.js';
|
|
195
196
|
import { statusMap as ecoscooting } from '../../carriers/ecoscooting/status.js';
|
|
196
197
|
import { statusMap as ecourier } from '../../carriers/ecourier/status.js';
|
|
@@ -711,7 +712,7 @@ export const STATUS_MAPS = {
|
|
|
711
712
|
'dpd-at': dpdAt, 'dpd-be': dpdBe, 'dpd-de': dpdDe, 'dpd-fr': dpdFr, 'dpd-hu': dpdHu, 'dpd-ie': dpdIe,
|
|
712
713
|
'dpd-nl': dpdNl, 'dpd-pl': dpdPl, 'dpd-pt': dpdPt, 'dpd-ro': dpdRo, 'dpd-uk': dpdUk, 'dpd-za': dpdZa, dpex, 'dragon-star-curier': dragonStarCurier, drop2shop, dropp, dsv, dtdc, dx, dynalogic,
|
|
713
714
|
'e-commerce-international': eCommerceInternational, 'early-bird': earlyBird, 'easy-mail': easyMail,
|
|
714
|
-
'ecms-express': ecmsExpress, econt, ecoscooting, ecourier, 'egypt-post': egyptPost, ekart, elta, emile, 'emirates-post': emiratesPost, ems, 'ems-vietnam': emsVietnam, 'energia-tk': energiaTk, 'envia-colombia': enviaColombia,
|
|
715
|
+
'ecms-express': ecmsExpress, 'ecom-logistics': ecomLogistics, econt, ecoscooting, ecourier, 'egypt-post': egyptPost, ekart, elta, emile, 'emirates-post': emiratesPost, ems, 'ems-vietnam': emsVietnam, 'energia-tk': energiaTk, 'envia-colombia': enviaColombia,
|
|
715
716
|
envialia, 'eparcel-kr': eparcelKr, 'epost-global': epostGlobal, equick, estafeta, estes, 'estes-forwarding': estesForwarding, eurodis, 'euroexpress-ba': euroexpressBa, europochta, eushipments,
|
|
716
717
|
'evdemo-lojistik': evdemoLojistik, evri, 'evri-uk': evriUk, evropat, ewe, exelot,
|
|
717
718
|
'exline-kz': exlineKz, exmate, 'express-one': expressOne, 'express-one-bg': expressOneBg, 'fan-courier': fanCourier, 'far-international': farInternational, 'fargo-courier': fargoCourier, 'fargo-uz': fargoUz, fastbox, fastdrop,
|
|
@@ -516,6 +516,11 @@ export function trackingLanguageStage(description) {
|
|
|
516
516
|
// A Chinese forwarder's collection from the sender, as ParcelsApp relays it.
|
|
517
517
|
if (/^shipment pick ?up$/.test(text))
|
|
518
518
|
return 'accepted';
|
|
519
|
+
// China Post collecting an express item, as providers relay it with no carrier. For a parcel a
|
|
520
|
+
// consolidator took first it is a hand-over on the way, so it reads in transit and never moves a
|
|
521
|
+
// parcel back, as YunExpress's and PFC Express's maps read China Post's receipt of mail.
|
|
522
|
+
if (/^中国邮政 ?已收取快件$/.test(text))
|
|
523
|
+
return 'in_transit';
|
|
519
524
|
// A Chinese courier's (业务员) collection from the sender, as providers translate it.
|
|
520
525
|
if (/^(?:the )?salesperson (?:has )?(?:collects|collected|picks up|picked up) (?:the )?(?:package|parcel|shipment)$/.test(text))
|
|
521
526
|
return 'accepted';
|
package/dist/data/catalog.json
CHANGED
|
@@ -16599,7 +16599,8 @@
|
|
|
16599
16599
|
"timezone": "America/Toronto",
|
|
16600
16600
|
"tracking": {
|
|
16601
16601
|
"mode": "automatic",
|
|
16602
|
-
"adapter": "
|
|
16602
|
+
"adapter": "ecom-logistics",
|
|
16603
|
+
"recognitionRank": 576
|
|
16603
16604
|
},
|
|
16604
16605
|
"canaryUrl": "https://shipecom.com/track",
|
|
16605
16606
|
"trackingUrlTemplate": "https://shipecom.com/track/{trackingNumber}",
|
|
@@ -10684,7 +10684,8 @@ export declare const CARRIER_CATALOG: {
|
|
|
10684
10684
|
readonly timezone: "America/Toronto";
|
|
10685
10685
|
readonly tracking: {
|
|
10686
10686
|
readonly mode: "automatic";
|
|
10687
|
-
readonly adapter: "
|
|
10687
|
+
readonly adapter: "ecom-logistics";
|
|
10688
|
+
readonly recognitionRank: 576;
|
|
10688
10689
|
};
|
|
10689
10690
|
readonly canaryUrl: "https://shipecom.com/track";
|
|
10690
10691
|
readonly trackingUrlTemplate: "https://shipecom.com/track/{trackingNumber}";
|
|
@@ -16600,7 +16600,8 @@ export const CARRIER_CATALOG = {
|
|
|
16600
16600
|
"timezone": "America/Toronto",
|
|
16601
16601
|
"tracking": {
|
|
16602
16602
|
"mode": "automatic",
|
|
16603
|
-
"adapter": "
|
|
16603
|
+
"adapter": "ecom-logistics",
|
|
16604
|
+
"recognitionRank": 576
|
|
16604
16605
|
},
|
|
16605
16606
|
"canaryUrl": "https://shipecom.com/track",
|
|
16606
16607
|
"trackingUrlTemplate": "https://shipecom.com/track/{trackingNumber}",
|
|
@@ -180,6 +180,7 @@ import { adapter as eCommerceInternational } from '../carriers/e-commerce-intern
|
|
|
180
180
|
import { adapter as earlyBird } from '../carriers/early-bird/adapter.js';
|
|
181
181
|
import { adapter as easyMail } from '../carriers/easy-mail/adapter.js';
|
|
182
182
|
import { adapter as ecmsExpress } from '../carriers/ecms-express/adapter.js';
|
|
183
|
+
import { adapter as ecomLogistics } from '../carriers/ecom-logistics/adapter.js';
|
|
183
184
|
import { adapter as econt } from '../carriers/econt/adapter.js';
|
|
184
185
|
import { adapter as ecoscooting } from '../carriers/ecoscooting/adapter.js';
|
|
185
186
|
import { adapter as ecourier } from '../carriers/ecourier/adapter.js';
|
|
@@ -857,6 +858,7 @@ export const REGISTRY = {
|
|
|
857
858
|
"early-bird": earlyBird,
|
|
858
859
|
"easy-mail": easyMail,
|
|
859
860
|
"ecms-express": ecmsExpress,
|
|
861
|
+
"ecom-logistics": ecomLogistics,
|
|
860
862
|
"econt": econt,
|
|
861
863
|
"ecoscooting": ecoscooting,
|
|
862
864
|
"ecourier": ecourier,
|
|
@@ -1677,7 +1679,7 @@ export const REGISTRY = {
|
|
|
1677
1679
|
"ecms-express": "ecms-express",
|
|
1678
1680
|
"eco-express-ae": "universal",
|
|
1679
1681
|
"ecohai": "universal",
|
|
1680
|
-
"ecom-logistics": "
|
|
1682
|
+
"ecom-logistics": "ecom-logistics",
|
|
1681
1683
|
"econt": "econt",
|
|
1682
1684
|
"ecoscooting": "ecoscooting",
|
|
1683
1685
|
"ecourier": "ecourier",
|
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.905.0-main.4818",
|
|
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",
|