universal-parcel-scraper 0.49.0-main.523 → 0.49.0-main.526
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/dist/carriers/aliexpress/adapter.js +27 -3
- package/dist/carriers/aliexpress/app.d.ts +2 -0
- package/dist/carriers/aliexpress/app.js +14 -0
- package/dist/carriers/aliexpress/carrier.json +3 -0
- package/dist/carriers/la-poste/adapter.d.ts +8 -0
- package/dist/carriers/la-poste/adapter.js +91 -4
- package/dist/carriers/swiss-post/adapter.d.ts +13 -0
- package/dist/carriers/swiss-post/adapter.js +48 -3
- package/dist/carriers/swiss-post/carrier.json +3 -0
- package/dist/core/catalog/eventIdentity.js +2 -1
- package/package.json +1 -1
|
@@ -76,6 +76,28 @@ function scanTime(scan) {
|
|
|
76
76
|
const offset = zone[1] ? `${zone[1]}${zone[2].padStart(2, '0')}:${zone[3] ?? '00'}` : 'Z';
|
|
77
77
|
return explicitOffsetTime(`${wall.replace(' ', 'T')}${offset}`)?.iso ?? wall;
|
|
78
78
|
}
|
|
79
|
+
/** The carriers a module names, which a bracket can name too without being a place. */
|
|
80
|
+
function carrierNames(trackingModule, scans) {
|
|
81
|
+
const named = [trackingModule.destCpInfo, ...[trackingModule.latestTrace, trackingModule.globalCombinedLogisticsTraceDTO, ...scans]
|
|
82
|
+
.map((scan) => record(scan).currentStatusCpInfo)];
|
|
83
|
+
return new Set(named.map((info) => text(record(info).cpName).trim().toLocaleLowerCase('en-US')).filter(Boolean));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Cainiao has no place field: it writes a scan's town in front of its standard
|
|
87
|
+
* wording, "[Bordeaux] Out for delivery", in every language. A bracket that
|
|
88
|
+
* reads as a place name becomes the location and the wording keeps the rest.
|
|
89
|
+
* Codes, capitals-only names and the module's own carrier names stay in the text.
|
|
90
|
+
*/
|
|
91
|
+
function placedWording(wording, carriers) {
|
|
92
|
+
const match = /^\s*\[([^[\]]{1,80})\]\s*(\S[\s\S]*)$/.exec(wording);
|
|
93
|
+
const place = match?.[1].trim() ?? '';
|
|
94
|
+
const letters = place.match(/\p{L}/gu)?.length ?? 0;
|
|
95
|
+
if (!match || letters < 3 || !/^\p{L}[\p{L}\p{M} .'’-]*$/u.test(place)
|
|
96
|
+
|| (/\p{Lu}/u.test(place) && !/\p{Ll}/u.test(place)) || carriers.has(place.toLocaleLowerCase('en-US'))) {
|
|
97
|
+
return { description: wording, location: '' };
|
|
98
|
+
}
|
|
99
|
+
return { description: match[2].trim(), location: place };
|
|
100
|
+
}
|
|
79
101
|
const BEIJING_OFFSET_MS = 8 * 3_600_000;
|
|
80
102
|
/**
|
|
81
103
|
* Whether a zone-less scan is Cainiao's own record on the Beijing clock. Its
|
|
@@ -139,13 +161,15 @@ export function parseCainiaoTrackingResponse(value, trackingNumber) {
|
|
|
139
161
|
?? (rawStatus ? 'in_transit' : details.length === 0 ? 'pending' : 'unknown'));
|
|
140
162
|
const latestAction = cainiaoActionCode(latest.actionCode);
|
|
141
163
|
const actionStage = cainiaoStageByStatus(latestAction);
|
|
164
|
+
const carriers = carrierNames(trackingModule, details);
|
|
142
165
|
const events = details.slice(0, MAX_EVENTS_TO_RETURN).map((event) => {
|
|
143
166
|
const code = cainiaoActionCode(event.actionCode);
|
|
144
167
|
const mapped = code ? CAINIAO_ACTION_STATUS.get(code) : undefined;
|
|
168
|
+
const { description, location } = placedWording(text(event.standerdDesc) || text(event.desc), carriers);
|
|
145
169
|
return {
|
|
146
170
|
time: scanTime(event),
|
|
147
|
-
location
|
|
148
|
-
description
|
|
171
|
+
location,
|
|
172
|
+
description,
|
|
149
173
|
...(mapped ? { stage: actionStage[mapped] ?? 'in_transit' } : {}),
|
|
150
174
|
};
|
|
151
175
|
});
|
|
@@ -162,7 +186,7 @@ export function parseCainiaoTrackingResponse(value, trackingNumber) {
|
|
|
162
186
|
return {
|
|
163
187
|
status,
|
|
164
188
|
...(actionStage[status] || status === 'in_transit' ? { current_stage: actionStage[status] ?? 'in_transit' } : {}),
|
|
165
|
-
last_status_text: text(latest.standerdDesc) || text(latest.desc) || rawStatus,
|
|
189
|
+
last_status_text: placedWording(text(latest.standerdDesc) || text(latest.desc), carriers).description || rawStatus,
|
|
166
190
|
last_update: scanTime(latest) || null,
|
|
167
191
|
expected_delivery: status === 'delivered' ? null : expected,
|
|
168
192
|
...(status === 'delivered' || expectedFrom == null || expectedFrom === expected
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const normalized = (value) => value.replace(/\s+/g, ' ').trim().toLocaleLowerCase('en-US');
|
|
2
|
+
// A scan stored while its town was still part of the wording, "[Bordeaux] Out for
|
|
3
|
+
// delivery" without a location, is the same scan as "Out for delivery" in Bordeaux.
|
|
4
|
+
export const sameInstantIdentityPolicy = {
|
|
5
|
+
sourceCarrierId: 'aliexpress', storedSources: ['aliexpress'], requireProviderCode: false,
|
|
6
|
+
matchEachScan: true,
|
|
7
|
+
matches(incoming, stored) {
|
|
8
|
+
const wording = normalized(incoming.description);
|
|
9
|
+
const location = normalized(incoming.location);
|
|
10
|
+
return Boolean(wording && location) && !['', 'unknown'].includes(incoming.stage)
|
|
11
|
+
&& incoming.stage === stored.stage && !normalized(stored.location)
|
|
12
|
+
&& normalized(stored.description) === `[${location}] ${wording}`;
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"capabilities": [
|
|
30
30
|
"history",
|
|
31
|
+
"location",
|
|
31
32
|
"eta",
|
|
32
33
|
"eta_window",
|
|
33
34
|
"delivered_at"
|
|
@@ -38,11 +39,13 @@
|
|
|
38
39
|
"shows": [
|
|
39
40
|
"status",
|
|
40
41
|
"history",
|
|
42
|
+
"location",
|
|
41
43
|
"eta"
|
|
42
44
|
],
|
|
43
45
|
"scraped": [
|
|
44
46
|
"status",
|
|
45
47
|
"history",
|
|
48
|
+
"location",
|
|
46
49
|
"eta"
|
|
47
50
|
],
|
|
48
51
|
"discarded": [],
|
|
@@ -25,7 +25,15 @@ export declare class LaPosteTracker {
|
|
|
25
25
|
private readonly fetcher?;
|
|
26
26
|
private readonly recorder?;
|
|
27
27
|
private readonly userAgent;
|
|
28
|
+
/** The locator's page for a point is large and its address does not change. */
|
|
29
|
+
private readonly addresses;
|
|
28
30
|
constructor(options?: LaPosteTrackerOptions);
|
|
29
31
|
fetch(trackingNumber: string, context?: TrackingContext): Promise<CarrierResult>;
|
|
32
|
+
/**
|
|
33
|
+
* The point's street and town, or nothing: the parcel is found, and its pickup point keeps its
|
|
34
|
+
* name without them. The request gets at most half of what is left, so the result still has
|
|
35
|
+
* time to return.
|
|
36
|
+
*/
|
|
37
|
+
private pointAddress;
|
|
30
38
|
}
|
|
31
39
|
export declare const adapter: AdapterFactory;
|
|
@@ -4,7 +4,7 @@ import { dpdParcelNumber } from '../../core/detection/dpd.js';
|
|
|
4
4
|
import { CarrierError, InvalidInputError, SchemaError, UpstreamHttpError } from '../../core/errors/index.js';
|
|
5
5
|
import { runSteps } from '../../core/runner/index.js';
|
|
6
6
|
import { isoTime } from '../../core/time/index.js';
|
|
7
|
-
import { clean, fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
|
|
7
|
+
import { clean, decodeText, fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
|
|
8
8
|
import { isRecord } from '../../core/types.js';
|
|
9
9
|
import { eventStage, eventStatus } from './status.js';
|
|
10
10
|
// Protocol provenance:
|
|
@@ -20,10 +20,19 @@ import { eventStage, eventStatus } from './status.js';
|
|
|
20
20
|
// - Production HTTP 403s carried La Poste's "Site indisponible - Incident en
|
|
21
21
|
// cours" page and immediately following checks succeeded, so a 403 is
|
|
22
22
|
// retried up to three times inside the original deadline (see README.md).
|
|
23
|
+
// - The tracking page links the point holding a parcel to La Poste's locator,
|
|
24
|
+
// `localiser.laposte.fr/{idPoint}`, which redirects to the point's page. That
|
|
25
|
+
// page carries the point's record, address included, as JSON in
|
|
26
|
+
// `Yext["profile"]`; `meta.id` repeats the point's id.
|
|
23
27
|
const TRACKING_API = 'https://www.laposte.fr/ssu/sun/back/suivi-unifie';
|
|
24
28
|
const TRACKING_PAGE = 'https://www.laposte.fr/outils/suivre-vos-envois';
|
|
29
|
+
const LOCATOR = 'https://localiser.laposte.fr';
|
|
25
30
|
const TIMEZONE = 'Europe/Paris';
|
|
26
31
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
32
|
+
/** The locator answers from a cache within a fraction of a second. */
|
|
33
|
+
const ADDRESS_TIMEOUT_MS = 3_000;
|
|
34
|
+
/** Points whose address a tracker remembers: a waiting parcel is looked up again and again. */
|
|
35
|
+
const MAX_REMEMBERED_POINTS = 500;
|
|
27
36
|
const MAX_RESPONSE_BYTES = 2_000_000;
|
|
28
37
|
const MAX_EVENTS_TO_RETURN = 100;
|
|
29
38
|
/** The provider's "unknown shipment" return code. */
|
|
@@ -116,7 +125,36 @@ export function laPosteTrackingApiUrl(trackingNumber) {
|
|
|
116
125
|
url.searchParams.set('lang', 'fr');
|
|
117
126
|
return url.toString();
|
|
118
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* The street and town of the point a locator page describes, a line each, if it is the
|
|
130
|
+
* requested one. Its phone, opening hours, services and coordinates are not read.
|
|
131
|
+
*/
|
|
132
|
+
function pointAddress(html, id) {
|
|
133
|
+
const marker = 'Yext["profile"] = ';
|
|
134
|
+
const start = html.indexOf(marker);
|
|
135
|
+
const end = start < 0 ? -1 : html.indexOf('; return Yext;', start);
|
|
136
|
+
if (end < 0)
|
|
137
|
+
return '';
|
|
138
|
+
let profile;
|
|
139
|
+
try {
|
|
140
|
+
profile = JSON.parse(html.slice(start + marker.length, end));
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return '';
|
|
144
|
+
}
|
|
145
|
+
if (!isRecord(profile) || !isRecord(profile.meta) || clean(profile.meta.id, 40) !== id || !isRecord(profile.address))
|
|
146
|
+
return '';
|
|
147
|
+
const street = clean(profile.address.line1, 120);
|
|
148
|
+
const city = clean(profile.address.city, 80);
|
|
149
|
+
if (!street || !city)
|
|
150
|
+
return '';
|
|
151
|
+
return `${street}\n${[clean(profile.address.postalCode, 16), city].filter(Boolean).join(' ')}`;
|
|
152
|
+
}
|
|
119
153
|
export function parseLaPosteTrackingResponse(payload, trackingNumber) {
|
|
154
|
+
return parseShipment(payload, trackingNumber).result;
|
|
155
|
+
}
|
|
156
|
+
/** `point` is the locator id of the pickup point the result names, else empty. */
|
|
157
|
+
function parseShipment(payload, trackingNumber) {
|
|
120
158
|
const requested = normalizeLaPosteTrackingNumber(trackingNumber);
|
|
121
159
|
if (!Array.isArray(payload) || payload.length === 0) {
|
|
122
160
|
throw new SchemaError('La Poste', 'La Poste returned an invalid tracking response');
|
|
@@ -180,13 +218,15 @@ export function parseLaPosteTrackingResponse(payload, trackingNumber) {
|
|
|
180
218
|
const shipped = clean(shipment.idShip, 64).toLocaleUpperCase('en-US');
|
|
181
219
|
// The merchant La Poste shows on the tracking page. Recipient blocks stay unread.
|
|
182
220
|
const sender = clean(context.merchantName, 200);
|
|
183
|
-
// The post office, locker or shop holding the parcel
|
|
221
|
+
// The post office, locker or shop holding the parcel: its name, and its id
|
|
222
|
+
// on La Poste's locator, which gives its address.
|
|
184
223
|
const removal = isRecord(context.removalPoint) ? context.removalPoint : {};
|
|
185
224
|
const pickupPoint = latest?.stage === 'ready_for_pickup' && clean(removal.type, 20) ? clean(removal.name, 200) : '';
|
|
225
|
+
const point = clean(removal.idPoint, 40);
|
|
186
226
|
const partner = isRecord(context.partner) ? context.partner : {};
|
|
187
227
|
const deliveryCarrier = carrierIdFromPartner(clean(partner.name, 80), clean(partner.url, 2048));
|
|
188
228
|
const deliveryNumber = clean(partner.reference, 64).toUpperCase();
|
|
189
|
-
|
|
229
|
+
const result = {
|
|
190
230
|
status: eventStatus(latestGroup, latestCode, latestLabel, events.length > 0),
|
|
191
231
|
// The status vocabulary has no pickup or customs value; without the stage
|
|
192
232
|
// the sync would re-read the sentence and fall back to "out for delivery".
|
|
@@ -207,12 +247,15 @@ export function parseLaPosteTrackingResponse(payload, trackingNumber) {
|
|
|
207
247
|
} : {}),
|
|
208
248
|
events,
|
|
209
249
|
};
|
|
250
|
+
return { result, point: pickupPoint && /^[A-Za-z0-9]{1,20}$/.test(point) ? point : '' };
|
|
210
251
|
}
|
|
211
252
|
export class LaPosteTracker {
|
|
212
253
|
timeoutMs;
|
|
213
254
|
fetcher;
|
|
214
255
|
recorder;
|
|
215
256
|
userAgent;
|
|
257
|
+
/** The locator's page for a point is large and its address does not change. */
|
|
258
|
+
addresses = new Map();
|
|
216
259
|
constructor(options = {}) {
|
|
217
260
|
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
218
261
|
if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) {
|
|
@@ -241,7 +284,14 @@ export class LaPosteTracker {
|
|
|
241
284
|
maxBytes: MAX_RESPONSE_BYTES,
|
|
242
285
|
fetcher: this.fetcher,
|
|
243
286
|
});
|
|
244
|
-
|
|
287
|
+
const { result, point } = parseShipment(parseJsonBytes(bytes, 'La Poste'), normalized);
|
|
288
|
+
// The feed names the point holding the parcel without its address, which the locator adds.
|
|
289
|
+
if (point) {
|
|
290
|
+
const address = await this.pointAddress(point, signal, deadline - performance.now());
|
|
291
|
+
if (address)
|
|
292
|
+
result.pickup_point = `${result.pickup_point}\n${address}`;
|
|
293
|
+
}
|
|
294
|
+
return result;
|
|
245
295
|
};
|
|
246
296
|
// La Poste's edge answers single lookups with an HTTP 403 "Site indisponible
|
|
247
297
|
// - Incident en cours" page while another parcel, or the same one a moment
|
|
@@ -268,6 +318,43 @@ export class LaPosteTracker {
|
|
|
268
318
|
{ ...retry },
|
|
269
319
|
]);
|
|
270
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* The point's street and town, or nothing: the parcel is found, and its pickup point keeps its
|
|
323
|
+
* name without them. The request gets at most half of what is left, so the result still has
|
|
324
|
+
* time to return.
|
|
325
|
+
*/
|
|
326
|
+
async pointAddress(id, signal, remainingMs) {
|
|
327
|
+
const known = this.addresses.get(id);
|
|
328
|
+
if (known)
|
|
329
|
+
return known;
|
|
330
|
+
const timeoutMs = Math.floor(Math.min(ADDRESS_TIMEOUT_MS, remainingMs / 2));
|
|
331
|
+
if (timeoutMs < 1)
|
|
332
|
+
return '';
|
|
333
|
+
try {
|
|
334
|
+
const { bytes } = await fetchBounded(`${LOCATOR}/${encodeURIComponent(id)}`, {
|
|
335
|
+
signal,
|
|
336
|
+
headers: { Accept: 'text/html', 'Accept-Language': 'fr-FR,fr;q=0.9', 'User-Agent': this.userAgent },
|
|
337
|
+
}, {
|
|
338
|
+
provider: 'La Poste locator',
|
|
339
|
+
timeoutMs,
|
|
340
|
+
maxBytes: MAX_RESPONSE_BYTES,
|
|
341
|
+
// The point's id redirects to its page, named after its town.
|
|
342
|
+
redirect: 'follow',
|
|
343
|
+
fetcher: this.fetcher,
|
|
344
|
+
});
|
|
345
|
+
const address = pointAddress(decodeText(bytes), id);
|
|
346
|
+
if (address) {
|
|
347
|
+
if (this.addresses.size >= MAX_REMEMBERED_POINTS)
|
|
348
|
+
this.addresses.delete(this.addresses.keys().next().value);
|
|
349
|
+
this.addresses.set(id, address);
|
|
350
|
+
}
|
|
351
|
+
return address;
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
signal.throwIfAborted();
|
|
355
|
+
return '';
|
|
356
|
+
}
|
|
357
|
+
}
|
|
271
358
|
}
|
|
272
359
|
export const adapter = (environment) => {
|
|
273
360
|
const tracker = new LaPosteTracker({
|
|
@@ -6,6 +6,17 @@ export interface SwissPostOptions {
|
|
|
6
6
|
fetcher?: typeof fetch;
|
|
7
7
|
}
|
|
8
8
|
export declare function swissPostExpectedDelivery(item: JsonObject): string | null;
|
|
9
|
+
/**
|
|
10
|
+
* The site number of the office or My Post 24 terminal holding the parcel, from its pickup
|
|
11
|
+
* notice, else empty. The tracker reads the arrival office first, as its own page does.
|
|
12
|
+
*/
|
|
13
|
+
export declare function swissPostPickupOffice(item: JsonObject): string;
|
|
14
|
+
/**
|
|
15
|
+
* The site record's name, then its street and its town on their own lines, as the pickup
|
|
16
|
+
* notice shows them, if it is the requested site. A street that repeats the name, as a
|
|
17
|
+
* terminal's can, is left out; without a town the name stands alone.
|
|
18
|
+
*/
|
|
19
|
+
export declare function swissPostPickupPoint(office: unknown, site: string): string;
|
|
9
20
|
export declare function parseSwissPostShipment(item: JsonObject, rawEvents: unknown[], translations?: Record<string, string>): CarrierResult;
|
|
10
21
|
export declare class SwissPostTracker {
|
|
11
22
|
#private;
|
|
@@ -14,5 +25,7 @@ export declare class SwissPostTracker {
|
|
|
14
25
|
/** Cached for the process: the table is large, static, and shared by every lookup. */
|
|
15
26
|
loadTranslations(fetcher: typeof fetch, budget?: LookupBudget): Promise<Record<string, string>>;
|
|
16
27
|
fetch(trackingNumber: string, context?: TrackingContext): Promise<CarrierResult>;
|
|
28
|
+
/** The pickup point, or nothing: the parcel is found without it. Only the caller's cancellation ends the lookup. */
|
|
29
|
+
private pickupPoint;
|
|
17
30
|
}
|
|
18
31
|
export declare const adapter: AdapterFactory;
|
|
@@ -14,8 +14,8 @@ const API_BASE = 'https://service.post.ch/ekp-web/api';
|
|
|
14
14
|
const TRANSLATIONS_URL = 'https://service.post.ch/ekp-web/core/rest/translations/en/shipment-text-messages';
|
|
15
15
|
const PROVIDER = 'Swiss Post';
|
|
16
16
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
17
|
-
/** The user, search, result, event and
|
|
18
|
-
const DEFAULT_BUDGET_MS =
|
|
17
|
+
/** The user, search, result, event, translation and pickup office calls, each at its own bound. */
|
|
18
|
+
const DEFAULT_BUDGET_MS = 6 * REQUEST_TIMEOUT_MS;
|
|
19
19
|
/** A request's own timer can fire a few milliseconds before the budget's clock runs out. */
|
|
20
20
|
const BUDGET_SLACK_MS = 5;
|
|
21
21
|
const HEADERS = {
|
|
@@ -146,6 +146,33 @@ function eventLocation(event) {
|
|
|
146
146
|
return country && country !== 'ZZ' ? country : postcode;
|
|
147
147
|
return [city, postcode].filter(Boolean).join(' ').slice(0, 160);
|
|
148
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* The site number of the office or My Post 24 terminal holding the parcel, from its pickup
|
|
151
|
+
* notice, else empty. The tracker reads the arrival office first, as its own page does.
|
|
152
|
+
*/
|
|
153
|
+
export function swissPostPickupOffice(item) {
|
|
154
|
+
const avis = isRecord(item.avis) ? item.avis : {};
|
|
155
|
+
const office = text(avis.arrivalPostOfficeZip, 20) || text(avis.deliveryPostOfficeZip, 20);
|
|
156
|
+
return /^\d{6}$/.test(office) ? office : '';
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The site record's name, then its street and its town on their own lines, as the pickup
|
|
160
|
+
* notice shows them, if it is the requested site. A street that repeats the name, as a
|
|
161
|
+
* terminal's can, is left out; without a town the name stands alone.
|
|
162
|
+
*/
|
|
163
|
+
export function swissPostPickupPoint(office, site) {
|
|
164
|
+
if (!isRecord(office) || text(office.zip, 20) !== site)
|
|
165
|
+
return '';
|
|
166
|
+
const name = text(office.description, 120);
|
|
167
|
+
if (!name)
|
|
168
|
+
return '';
|
|
169
|
+
const postcode = text(office.zip4, 10);
|
|
170
|
+
const town = text(office.city, 80);
|
|
171
|
+
if (!/^\d{4}$/.test(postcode) || !town)
|
|
172
|
+
return name;
|
|
173
|
+
const street = [text(office.street, 120), text(office.streetNumber, 20)].filter(Boolean).join(' ');
|
|
174
|
+
return [name, street !== name && street, `${postcode} ${town}`].filter(Boolean).join('\n');
|
|
175
|
+
}
|
|
149
176
|
/**
|
|
150
177
|
* Local ordering policy: event timestamps are read only to sort. Values without
|
|
151
178
|
* an offset are treated as UTC for that comparison, which keeps the ordering
|
|
@@ -332,7 +359,25 @@ export class SwissPostTracker {
|
|
|
332
359
|
}
|
|
333
360
|
}
|
|
334
361
|
const translations = events.length > 0 ? await this.loadTranslations(fetcher, budget) : {};
|
|
335
|
-
|
|
362
|
+
const result = parseSwissPostShipment(item, events, translations);
|
|
363
|
+
// The pickup notice names the site holding the parcel; that site's record gives its name and address.
|
|
364
|
+
const office = result.current_stage === 'ready_for_pickup' ? swissPostPickupOffice(item) : '';
|
|
365
|
+
if (office) {
|
|
366
|
+
const point = await this.pickupPoint(fetcher, headers, office, budget, context.signal);
|
|
367
|
+
if (point)
|
|
368
|
+
result.pickup_point = point;
|
|
369
|
+
}
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
/** The pickup point, or nothing: the parcel is found without it. Only the caller's cancellation ends the lookup. */
|
|
373
|
+
async pickupPoint(fetcher, headers, office, budget, signal) {
|
|
374
|
+
try {
|
|
375
|
+
return swissPostPickupPoint(await this.readJson(fetcher, `${API_BASE}/autocomplete/postoffice/id/${encodeURIComponent(office)}`, { headers }, budget), office);
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
signal?.throwIfAborted();
|
|
379
|
+
return '';
|
|
380
|
+
}
|
|
336
381
|
}
|
|
337
382
|
}
|
|
338
383
|
export const adapter = (environment) => {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"history",
|
|
30
30
|
"location",
|
|
31
31
|
"eta",
|
|
32
|
+
"pickup_point",
|
|
32
33
|
"provider_code",
|
|
33
34
|
"weight",
|
|
34
35
|
"dimensions",
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
"location",
|
|
44
45
|
"eta",
|
|
45
46
|
"eta_window",
|
|
47
|
+
"pickup_point",
|
|
46
48
|
"weight",
|
|
47
49
|
"dimensions",
|
|
48
50
|
"delivered_at",
|
|
@@ -57,6 +59,7 @@
|
|
|
57
59
|
"history",
|
|
58
60
|
"location",
|
|
59
61
|
"eta",
|
|
62
|
+
"pickup_point",
|
|
60
63
|
"provider_code",
|
|
61
64
|
"weight",
|
|
62
65
|
"dimensions",
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { sameInstantIdentityPolicy as aliexpress } from '../../carriers/aliexpress/app.js';
|
|
1
2
|
import { sameInstantIdentityPolicy as australiaPost } from '../../carriers/australia-post/app.js';
|
|
2
3
|
import { sameInstantIdentityPolicy as dpd } from '../../carriers/dpd/app.js';
|
|
3
4
|
import { sameInstantIdentityPolicy as indiaPost } from '../../carriers/india-post/app.js';
|
|
4
5
|
import { sameInstantIdentityPolicy as ups } from '../../carriers/ups/app.js';
|
|
5
6
|
import { sameInstantIdentityPolicy as swissPost } from '../../carriers/swiss-post/app.js';
|
|
6
7
|
import { sameInstantIdentityPolicy as universal } from '../../providers/shared/app.js';
|
|
7
|
-
const policies = new Map([australiaPost, dpd, indiaPost, ups, swissPost, universal].map((policy) => [policy.sourceCarrierId, policy]));
|
|
8
|
+
const policies = new Map([aliexpress, australiaPost, dpd, indiaPost, ups, swissPost, universal].map((policy) => [policy.sourceCarrierId, policy]));
|
|
8
9
|
/** Unlisted sources cannot identify a reworded scan by its instant alone. */
|
|
9
10
|
export function sameInstantIdentityPolicy(sourceCarrierId, options = {}) {
|
|
10
11
|
const policy = policies.get(sourceCarrierId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-parcel-scraper",
|
|
3
|
-
"version": "0.49.0-main.
|
|
3
|
+
"version": "0.49.0-main.526",
|
|
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",
|