universal-parcel-scraper 0.969.3-main.5259 → 0.969.5-main.5263
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 +1 -1
- package/dist/carriers/first-line/identity.d.ts +2 -0
- package/dist/carriers/first-line/identity.js +7 -0
- package/dist/carriers/first-line/parser.js +2 -5
- package/dist/carriers/first-line/place.d.ts +2 -0
- package/dist/carriers/first-line/place.js +7 -0
- package/dist/carriers/first-line/statuses.json +1 -0
- package/dist/carriers/js-express/identity.js +20 -2
- package/dist/carriers/yanwen/status.js +12 -3
- package/dist/carriers/yanwen/statuses.json +32 -0
- package/dist/carriers/yto/wording.d.ts +6 -0
- package/dist/carriers/yto/wording.js +21 -0
- package/dist/core/catalog/eventIdentity.js +2 -1
- package/dist/providers/seventeentrack/adapter.js +6 -1
- package/dist/providers/shared/app.js +14 -2
- package/dist/server/openapi.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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,933 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.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { matchesRereadLocation } from '../../core/catalog/locationIdentity.js';
|
|
2
|
+
import { firstLinePlace } from './place.js';
|
|
3
|
+
// A row stored with GOFO's town and state said twice is taken over by the scan that names them once.
|
|
4
|
+
export const sameInstantIdentityPolicy = {
|
|
5
|
+
sourceCarrierId: 'first-line', storedSources: ['first-line'], requireProviderCode: false, matchEachScan: true,
|
|
6
|
+
matches: (incoming, stored) => matchesRereadLocation(incoming, stored, firstLinePlace),
|
|
7
|
+
};
|
|
@@ -7,6 +7,7 @@ import { normalizeStatusWording } from '../../core/status/statusMap.js';
|
|
|
7
7
|
import { firstOccurrence } from '../../core/time/index.js';
|
|
8
8
|
import { textFromHtml } from '../../core/transport/index.js';
|
|
9
9
|
import { FIRST_LINE_CHINA_ZONE, firstLineInChina } from './clock.js';
|
|
10
|
+
import { firstLinePlace } from './place.js';
|
|
10
11
|
import { firstLineStage } from './status.js';
|
|
11
12
|
export const PROVIDER = 'FIRST LINE';
|
|
12
13
|
// A long history is a few kilobytes; the tracking page is about twelve.
|
|
@@ -43,10 +44,6 @@ export function parseFirstLinePage(html) {
|
|
|
43
44
|
throw new ChallengeError(PROVIDER, 'FIRST LINE answered its tracking page with a browser check');
|
|
44
45
|
throw new SchemaError(PROVIDER, 'FIRST LINE’s tracking page holds no lookup address');
|
|
45
46
|
}
|
|
46
|
-
/** A place as the tracker writes it, without a trailing postcode. */
|
|
47
|
-
function placeOf(location) {
|
|
48
|
-
return location.replace(/\s+\d{4,6}(?:-\d{4})?$/, '').trim();
|
|
49
|
-
}
|
|
50
47
|
function stageOf(description) {
|
|
51
48
|
const mapped = firstLineStage(null, normalizeStatusWording(description));
|
|
52
49
|
if (mapped)
|
|
@@ -70,7 +67,7 @@ function projectRow(row, readAt) {
|
|
|
70
67
|
const description = textFromHtml(match[4]);
|
|
71
68
|
if (!description)
|
|
72
69
|
return null;
|
|
73
|
-
const location =
|
|
70
|
+
const location = firstLinePlace(textFromHtml(match[3], 200));
|
|
74
71
|
const staged = stageOf(description);
|
|
75
72
|
// A row in China or Hong Kong is on Beijing's clock; any other keeps its wall clock.
|
|
76
73
|
const inChina = location ? firstLineInChina(location) : ORDER_DATA.test(description);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// A US partner's place as the tracker relays GOFO's: its hub's "Town, ST", then the town again and the state,
|
|
2
|
+
// "Melrose Park, IL, Melrose Park IL", or the state alone, "Melrose Park, IL IL".
|
|
3
|
+
const REPEATED_TOWN = /^(\S[^,]{0,80}), ([A-Z]{2})(?:,? \1)? \2$/;
|
|
4
|
+
/** A place as the tracker writes it, without a trailing postcode or a US town and state said twice. */
|
|
5
|
+
export function firstLinePlace(location) {
|
|
6
|
+
return location.replace(/\s+\d{4,6}(?:-\d{4})?$/, '').trim().replace(REPEATED_TOWN, '$1, $2');
|
|
7
|
+
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
{"wording": "Customs broker picked it up", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-11-03", "note": "The customs broker collecting the parcel after its import release, before the hand-over to the delivery carrier: it is no longer held in customs."},
|
|
19
19
|
{"wording": "The package has been transferred to the service provider", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-11-03", "note": "The hand-over to the delivery carrier after the broker's collection."},
|
|
20
20
|
{"wording": "Arrived at GOFO Regional Hub", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-11-04", "note": "The delivering partner GOFO's sorting hub."},
|
|
21
|
+
{"wording": "Loaded on Vehicle", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-11-05", "note": "GOFO loading the parcel on the truck at its sorting hub, minutes before the hub's departure scan: not the delivery round."},
|
|
21
22
|
{"wording": "Departed GOFO Regional Hub", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-11-04", "note": "Leaving GOFO's sorting hub for its delivery station."},
|
|
22
23
|
{"wording": "Arrived at GOFO Delivery Station", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-11-04", "note": "GOFO's last-mile station, before its delivery round."}
|
|
23
24
|
]
|
|
@@ -14,11 +14,29 @@ export const sameInstantIdentityPolicy = {
|
|
|
14
14
|
...translated,
|
|
15
15
|
// A row stored with a label that names no place, without a letter as the parser reads it, is
|
|
16
16
|
// taken over by the row without it. So is one stored at the tracker's "Array", whatever stage
|
|
17
|
-
// the map has given its wording since,
|
|
17
|
+
// the map has given its wording since, an iMile delivery stored with its receiver type, and
|
|
18
|
+
// China Post's scan from the feed that keeps minutes, less than a minute from the other's.
|
|
18
19
|
matches: (incoming, stored) => translated.matches(incoming, stored)
|
|
19
20
|
|| matchesDroppedLocation(incoming, stored, (location) => !/\p{L}/u.test(location))
|
|
20
|
-
|| listPlace(incoming, stored) || receiverCut(incoming, stored),
|
|
21
|
+
|| listPlace(incoming, stored) || receiverCut(incoming, stored) || officeCoded(incoming, stored),
|
|
22
|
+
matchWithinMinute: true,
|
|
21
23
|
};
|
|
24
|
+
// China Post's domestic scans, as the tracker relays them, come from two feeds: one dates them to the
|
|
25
|
+
// minute and places them in the town ("郑州市"), the other to the second, with the office's code after
|
|
26
|
+
// the town ("郑州市 45004400"). The first can word an arrival for the customer, the parcel a 快件 and the
|
|
27
|
+
// centre's slogan after it, where the other has 邮件 and the centre alone.
|
|
28
|
+
const OFFICE_CODE = /^(\S.*?)\s+\d{6,10}$/;
|
|
29
|
+
const POST_SCAN = /^(?:快件|邮件)(到达|离开)(【[^】]{1,80}】)/;
|
|
30
|
+
const postScan = (wording) => POST_SCAN.exec(normalized(wording))?.slice(1).join('');
|
|
31
|
+
function officeCoded(incoming, stored) {
|
|
32
|
+
const [place, saved] = [incoming.location.trim(), stored.location.trim()];
|
|
33
|
+
const [coded, savedCoded] = [OFFICE_CODE.exec(place), OFFICE_CODE.exec(saved)];
|
|
34
|
+
if (!coded === !savedCoded || (coded?.[1] ?? place) !== (savedCoded?.[1] ?? saved))
|
|
35
|
+
return false;
|
|
36
|
+
const scan = postScan(incoming.description);
|
|
37
|
+
return (sameWording(incoming.description, stored.description) || (scan !== undefined && scan === postScan(stored.description)))
|
|
38
|
+
&& (!incoming.providerCode || !stored.providerCode || incoming.providerCode === stored.providerCode);
|
|
39
|
+
}
|
|
22
40
|
function listPlace(incoming, stored) {
|
|
23
41
|
return stored.location.trim() === LIST_PLACE && !incoming.location.trim() && sameWording(incoming.description, stored.description);
|
|
24
42
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { languageStageStatus } from '../../core/status/index.js';
|
|
2
2
|
import { normalizeStatusWording } from '../../core/status/statusMap.js';
|
|
3
3
|
import { chinaPostRelayedStage } from '../china-post/status.js';
|
|
4
|
+
import { jAndTAuMappedStage } from '../j-and-t-au/status.js';
|
|
4
5
|
const WORDING = new Map([
|
|
5
6
|
['Delivered.', { status: 'delivered', stage: 'delivered' }],
|
|
6
7
|
['Shipment out for delivery.', { status: 'out_for_delivery', stage: 'out_for_delivery' }],
|
|
@@ -136,10 +137,18 @@ const DELIVERY_ICON = 'LM40';
|
|
|
136
137
|
const withoutPrefix = (description) => description.replace(/^【[^】]*】\s*/, '').replace(/^[a-z]{2}\.(?=\S)/i, '').trim();
|
|
137
138
|
// A partner's postal event can end with its country's code: "Arrival at Destination Post (Country: TR)".
|
|
138
139
|
const bare = (description) => withoutPrefix(description).replace(/\s*\(Country(?: code)?: [a-z]{2}\)$/i, '');
|
|
140
|
+
// J&T Express Australia's scans after Yanwen's batch handover at the destination port, relayed with its customer
|
|
141
|
+
// service line: read as J&T's own map reads them, but its pickup is Yanwen's handover, not the parcel's acceptance.
|
|
142
|
+
const HOTLINE = /[\s.]*customer service\s*[::]\s*\d[\d\s]*$/i;
|
|
143
|
+
function jAndTAuRelayedStage(wording) {
|
|
144
|
+
const stage = jAndTAuMappedStage(wording.replace(HOTLINE, '.'));
|
|
145
|
+
return stage === 'accepted' ? 'in_transit' : stage;
|
|
146
|
+
}
|
|
139
147
|
// China Post's domestic scans, which Yanwen relays in Chinese with the centre in 【】, read as China Post's own
|
|
140
|
-
// map reads them: "邮件离开【…】,正在发往下一站" in transit, the security check's "【…】退回" an exception.
|
|
148
|
+
// map reads them: "邮件离开【…】,正在发往下一站" in transit, the security check's "【…】退回" an exception. Then J&T Australia's.
|
|
149
|
+
const relayedStage = (normalized) => chinaPostRelayedStage(normalized) ?? jAndTAuRelayedStage(normalized);
|
|
141
150
|
function relayedStatus(description) {
|
|
142
|
-
const stage =
|
|
151
|
+
const stage = relayedStage(normalizeStatusWording(description));
|
|
143
152
|
return stage ? { status: languageStageStatus(stage), stage } : undefined;
|
|
144
153
|
}
|
|
145
154
|
export function yanwenStatus(description, code) {
|
|
@@ -157,7 +166,7 @@ const NOTICES = new Map([
|
|
|
157
166
|
export const yanwenNotice = (description) => NOTICES.has(normalizeStatusWording(withoutPrefix(description)));
|
|
158
167
|
/** What the map says about one scan by its milestone icon, the only code it keeps, else its wording. */
|
|
159
168
|
export const statusMap = {
|
|
160
|
-
stage: (code, wording) => (code === DELIVERY_ICON ? 'delivered' : STAGE_BY_WORDING.get(bare(wording)) ??
|
|
169
|
+
stage: (code, wording) => (code === DELIVERY_ICON ? 'delivered' : STAGE_BY_WORDING.get(bare(wording)) ?? relayedStage(wording)),
|
|
161
170
|
gaps: [
|
|
162
171
|
{ wording: 'ua.', note: "A partner's scan in Ukraine that Yanwen relays as its country's code alone, without words: it names no stage." },
|
|
163
172
|
...[...NOTICES].map(([wording, note]) => ({ wording, note })),
|
|
@@ -675,6 +675,38 @@
|
|
|
675
675
|
"firstSeen": "2026-11-03",
|
|
676
676
|
"note": "China Post's domestic scan, which Yanwen relays in Chinese with the centre in 【】; read as China Post's map reads it."
|
|
677
677
|
},
|
|
678
|
+
{
|
|
679
|
+
"wording": "The parcel has been successfully picked up and is being prepared for transportation.",
|
|
680
|
+
"language": "en",
|
|
681
|
+
"stage": "in_transit",
|
|
682
|
+
"confirmedBy": "live",
|
|
683
|
+
"firstSeen": "2026-11-05",
|
|
684
|
+
"note": "J&T Express Australia's pickup from Yanwen's batch handover at the destination port, relayed with J&T's customer service line after it: a handover, not the parcel's acceptance, which J&T's own history reads it as."
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
"wording": "The parcel is dispatched from the transit center to the next destination",
|
|
688
|
+
"language": "en",
|
|
689
|
+
"stage": "in_transit",
|
|
690
|
+
"confirmedBy": "live",
|
|
691
|
+
"firstSeen": "2026-11-05",
|
|
692
|
+
"note": "J&T Express Australia's scan, which Yanwen relays after its batch handover at the destination port; read as J&T's map reads it."
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
"wording": "The parcel arrives at the distribution center and is prepared for transport to the next destination.",
|
|
696
|
+
"language": "en",
|
|
697
|
+
"stage": "in_transit",
|
|
698
|
+
"confirmedBy": "live",
|
|
699
|
+
"firstSeen": "2026-11-05",
|
|
700
|
+
"note": "J&T Express Australia's scan, which Yanwen relays after its batch handover at the destination port; read as J&T's map reads it."
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
"wording": "Your parcel is out for delivery.",
|
|
704
|
+
"language": "en",
|
|
705
|
+
"stage": "out_for_delivery",
|
|
706
|
+
"confirmedBy": "live",
|
|
707
|
+
"firstSeen": "2026-11-05",
|
|
708
|
+
"note": "J&T Express Australia's scan, which Yanwen relays with J&T's courtesy and customer service line after it; read as J&T's map reads it."
|
|
709
|
+
},
|
|
678
710
|
{
|
|
679
711
|
"wording": "送交出口海关",
|
|
680
712
|
"language": "zh",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A YTO scan's expanded description without the courier's name and phone and
|
|
3
|
+
* YTO's hotline, as the adapter keeps neither: its clauses up to the first that
|
|
4
|
+
* names a contact, without a bracket that gives one. Empty when the first does.
|
|
5
|
+
*/
|
|
6
|
+
export declare function ytoRelayedWording(description: string): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// The website's expanded description, which 17TRACK relays in place of the short label: the
|
|
2
|
+
// scan's own clause, then who collected or delivers the parcel with their phone,
|
|
3
|
+
// "揽收人: <name>(<phone>)", and YTO's hotline in brackets, "【物流问题请致电(专属热线:<number>)更快解决】".
|
|
4
|
+
// A clause or bracket that gives a courier, a signer, a phone, a hotline or a complaint line, as
|
|
5
|
+
// relayed Chinese post scans are read.
|
|
6
|
+
const CONTACT = /电话|手机|致电|联系|电联|热线|投诉|呼叫|姓名|司机|(?:揽收|揽件|揽投|收件|取件|派件|派送|收派|配送|投递|签收|快递|业务)[人员]|(?<![A-Z\d])\d{5,}/i;
|
|
7
|
+
/**
|
|
8
|
+
* A YTO scan's expanded description without the courier's name and phone and
|
|
9
|
+
* YTO's hotline, as the adapter keeps neither: its clauses up to the first that
|
|
10
|
+
* names a contact, without a bracket that gives one. Empty when the first does.
|
|
11
|
+
*/
|
|
12
|
+
export function ytoRelayedWording(description) {
|
|
13
|
+
const text = description.replace(/【([^【】]*)】/gu, (whole, inner) => (CONTACT.test(inner) ? '' : whole));
|
|
14
|
+
const kept = [];
|
|
15
|
+
for (const clause of text.split(/(?<=[,,。;;!!??])/u)) {
|
|
16
|
+
if (CONTACT.test(clause))
|
|
17
|
+
break;
|
|
18
|
+
kept.push(clause);
|
|
19
|
+
}
|
|
20
|
+
return kept.join('').replace(/\s+/gu, ' ').trim().replace(/[\s,,;;::]+$/u, '');
|
|
21
|
+
}
|
|
@@ -24,6 +24,7 @@ import { sameInstantIdentityPolicy as dpdPoland } from '../../carriers/dpd-pl/id
|
|
|
24
24
|
import { sameInstantIdentityPolicy as dpdRomania } from '../../carriers/dpd-ro/identity.js';
|
|
25
25
|
import { sameInstantIdentityPolicy as dpdUk } from '../../carriers/dpd-uk/identity.js';
|
|
26
26
|
import { sameInstantIdentityPolicy as fedex } from '../../carriers/fedex/identity.js';
|
|
27
|
+
import { sameInstantIdentityPolicy as firstLine } from '../../carriers/first-line/identity.js';
|
|
27
28
|
import { sameInstantIdentityPolicy as gofoFrance } from '../../carriers/gofo-fr/identity.js';
|
|
28
29
|
import { sameInstantIdentityPolicy as glsItaly } from '../../carriers/gls-it/identity.js';
|
|
29
30
|
import { sameInstantIdentityPolicy as gofoItaly } from '../../carriers/gofo-it/identity.js';
|
|
@@ -75,7 +76,7 @@ import { sameInstantIdentityPolicy as zto } from '../../carriers/zto/identity.js
|
|
|
75
76
|
import { sameInstantIdentityPolicy as ztoInternational } from '../../carriers/zto-international/identity.js';
|
|
76
77
|
import { sameInstantIdentityPolicy as universal } from '../../providers/shared/app.js';
|
|
77
78
|
import { CARRIER_DEFINITIONS } from './definitions.js';
|
|
78
|
-
const policies = new Map([aliexpress, allegroOne, amazonShipping, australiaPost, austrianPost, brt, canadaPost, chinaPost, chronopost, ciblex, correosExpress, correosSpain, ctt, cttyex, dhl, dhlEcommerce, dhlEcommercePl, dhlExpress, dhlink, dpd, dpdGermany, dpdFrance, dpdPoland, dpdRomania, dpdUk, fedex, glsItaly, gofoFrance, gofoItaly, gzyl, hermesGermany, huayaExpress, imile, indiaPost, inpost, jAndTAu, jcex, jdLogistics, jrt, jsExpress, laPoste, lbcExpress, linhan, mayiyinfu, mondialRelay, mrw, nacex, nzPost, packeta, parcelforce, pitneyBowes, pocztaPolska, posMalaysia, postlogistics, postnord, postaSlovenije, posteItaliane, posti, royalMail, sfExpress, shouwu, speedpak, speedy, spxBr, sunYou, thhy, ups, usps, swissPost, vintedGo, yanwen, yilongLogistics, yto, yunExpress, zto, ztoInternational, universal].map((policy) => [policy.sourceCarrierId, policy]));
|
|
79
|
+
const policies = new Map([aliexpress, allegroOne, amazonShipping, australiaPost, austrianPost, brt, canadaPost, chinaPost, chronopost, ciblex, correosExpress, correosSpain, ctt, cttyex, dhl, dhlEcommerce, dhlEcommercePl, dhlExpress, dhlink, dpd, dpdGermany, dpdFrance, dpdPoland, dpdRomania, dpdUk, fedex, firstLine, glsItaly, gofoFrance, gofoItaly, gzyl, hermesGermany, huayaExpress, imile, indiaPost, inpost, jAndTAu, jcex, jdLogistics, jrt, jsExpress, laPoste, lbcExpress, linhan, mayiyinfu, mondialRelay, mrw, nacex, nzPost, packeta, parcelforce, pitneyBowes, pocztaPolska, posMalaysia, postlogistics, postnord, postaSlovenije, posteItaliane, posti, royalMail, sfExpress, shouwu, speedpak, speedy, spxBr, sunYou, thhy, ups, usps, swissPost, vintedGo, yanwen, yilongLogistics, yto, yunExpress, zto, ztoInternational, universal].map((policy) => [policy.sourceCarrierId, policy]));
|
|
79
80
|
const wording = (scan) => scan.description.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
80
81
|
/** One scan read twice: the same wording and place, and codes that agree when both carry one. */
|
|
81
82
|
function sameScan(incoming, stored) {
|
|
@@ -24,6 +24,7 @@ import { HYWL_CHINA_ZONE, hywlInChina } from '../../carriers/hywl/clock.js';
|
|
|
24
24
|
import { placedInIndia } from '../../carriers/shiprocket/clock.js';
|
|
25
25
|
import { swissPostDestinationZone } from '../../carriers/swiss-post/clock.js';
|
|
26
26
|
import { uspsRowZone, uspsScanZone } from '../../carriers/usps/clock.js';
|
|
27
|
+
import { ytoRelayedWording } from '../../carriers/yto/wording.js';
|
|
27
28
|
import { universalCarrierHints } from '../shared/hints.js';
|
|
28
29
|
import { capturedBodies, captureFailure, loadCapture } from '../shared/capture.js';
|
|
29
30
|
import { facilityZone, LatestScan, relaysFacilityClock } from '../shared/facilityClock.js';
|
|
@@ -404,6 +405,8 @@ const CHINA_POST = 3011;
|
|
|
404
405
|
const CHINA_POST_LABELS = new Set([CHINA_POST, 192049]);
|
|
405
406
|
/** Legs whose English wording ends with the place in brackets: Pengyou (捧友, 191878 in 17TRACK's carrier list). */
|
|
406
407
|
const BRACKETED_PLACES = new Set([191878]);
|
|
408
|
+
/** 17TRACK's carrier 190157, YTO Express, whose scans it relays in the website's expanded wording. */
|
|
409
|
+
const YTO = 190157;
|
|
407
410
|
/** China Post's mirror of the origin post's outward exchange office: "arrived at" or "left the exchange office abroad". */
|
|
408
411
|
const ORIGIN_EXCHANGE = /^(?:到达|离开)境外出口互换局/;
|
|
409
412
|
/**
|
|
@@ -608,8 +611,10 @@ export function parse17TrackResponse(payload, trackingNumber, postcode = null) {
|
|
|
608
611
|
: daiPost ? daiPostRelayZone(text(raw.description), text(raw.location)) : facilityZone(text(raw.location))));
|
|
609
612
|
const placed = wall && zone ? firstOccurrence(DateTime.fromISO(wall, { zone })) : null;
|
|
610
613
|
let parsed;
|
|
614
|
+
// YTO's wording without the courier's name and phone and its hotline, as YTO's own result keeps neither.
|
|
615
|
+
const scan = reported.key === YTO ? { ...raw, description: ytoRelayedWording(text(raw.description)) } : raw;
|
|
611
616
|
try {
|
|
612
|
-
parsed = seventeenTrackEvent(
|
|
617
|
+
parsed = seventeenTrackEvent(scan, operator, placed?.isValid ? placed.toUTC().toISO() ?? undefined : undefined);
|
|
613
618
|
}
|
|
614
619
|
catch (cause) {
|
|
615
620
|
throw new SchemaError(SOURCE, '17TRACK returned an invalid tracking event', { cause });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { matchesLocationEnrichment, matchesRereadLocation } from '../../core/catalog/locationIdentity.js';
|
|
2
2
|
import { chinaPostEnglish, chinaPostLabelPlace } from '../../carriers/china-post/status.js';
|
|
3
|
+
import { ytoRelayedWording } from '../../carriers/yto/wording.js';
|
|
3
4
|
import { place, trailingPlace } from './result.js';
|
|
4
5
|
const normalized = (value) => value.replace(/\s+/g, ' ').trim().toLocaleLowerCase('en-US');
|
|
5
6
|
/**
|
|
@@ -27,14 +28,25 @@ function matchesTrailingPlace(incoming, stored) {
|
|
|
27
28
|
&& normalized(trailing.place) === normalized(incoming.location)
|
|
28
29
|
&& (!incoming.providerCode || !stored.providerCode || incoming.providerCode === stored.providerCode);
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* A YTO scan stored with the courier's name and phone or YTO's hotline, before
|
|
33
|
+
* 17TRACK's copy left them out: the stored wording without them, at the same
|
|
34
|
+
* location, with provider codes that agree.
|
|
35
|
+
*/
|
|
36
|
+
function matchesYtoContacts(incoming, stored) {
|
|
37
|
+
const wording = ytoRelayedWording(stored.description);
|
|
38
|
+
return wording !== '' && normalized(wording) !== normalized(stored.description) && normalized(wording) === normalized(incoming.description)
|
|
39
|
+
&& normalized(incoming.location) === normalized(stored.location)
|
|
40
|
+
&& (!incoming.providerCode || !stored.providerCode || incoming.providerCode === stored.providerCode);
|
|
41
|
+
}
|
|
30
42
|
// A copy may add a place, or drop a label it was once stored with that names
|
|
31
43
|
// no place (Posti's "abroad", a feed's system user) or the words before a
|
|
32
44
|
// depot's town, show a China Post label in English, or take the place from
|
|
33
|
-
// after its wording.
|
|
45
|
+
// after its wording, or leave out a YTO courier's contact.
|
|
34
46
|
export const sameInstantIdentityPolicy = {
|
|
35
47
|
sourceCarrierId: 'unknown', storedSources: ['unknown'], requireProviderCode: false,
|
|
36
48
|
matchEachScan: true,
|
|
37
49
|
matches: (incoming, stored) => matchesLocationEnrichment(incoming, stored)
|
|
38
50
|
|| matchesRereadLocation(incoming, stored, place) || matchesChinaPostEnglish(incoming, stored)
|
|
39
|
-
|| matchesTrailingPlace(incoming, stored),
|
|
51
|
+
|| matchesTrailingPlace(incoming, stored) || matchesYtoContacts(incoming, stored),
|
|
40
52
|
};
|
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.969.
|
|
3
|
+
"version": "0.969.5-main.5263",
|
|
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",
|