universal-parcel-scraper 0.44.0-main.412 → 0.44.0-main.414
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/india-post/adapter.js +138 -20
- package/dist/carriers/india-post/app.js +3 -1
- package/dist/core/catalog/eventIdentity.d.ts +8 -0
- package/dist/providers/seventeentrack/adapter.js +1 -1
- package/dist/providers/seventeentrack/events.d.ts +5 -2
- package/dist/providers/seventeentrack/events.js +23 -3
- package/package.json +1 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { load } from 'cheerio';
|
|
2
2
|
import makeFetchCookie from 'fetch-cookie';
|
|
3
|
+
import { DateTime } from 'luxon';
|
|
3
4
|
import { CookieJar } from 'tough-cookie';
|
|
4
5
|
import { lookupBudget } from '../../core/adapter/index.js';
|
|
5
6
|
import { ChallengeError, IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
|
|
6
7
|
import { eventPoint } from '../../core/result/index.js';
|
|
7
8
|
import { isValidS10TrackingNumber } from '../../core/detection/index.js';
|
|
8
|
-
import { isoTime, mislabeledLocalTime } from '../../core/time/index.js';
|
|
9
|
+
import { countryCode, countryTimeZone, isoTime, mislabeledLocalTime, timeZoneCountry, } from '../../core/time/index.js';
|
|
9
10
|
import { cleanScalar, decodeText, fetchBounded, parseJsonBytes, UpstreamHttpError, } from '../../core/transport/index.js';
|
|
10
11
|
import { isRecord } from '../../core/types.js';
|
|
11
12
|
import { classifyIndiaPostEvent } from './status.js';
|
|
@@ -22,6 +23,8 @@ const DEFAULT_MAX_POLL_ATTEMPTS = 10;
|
|
|
22
23
|
// kept an 11-day-old "Item Booked" while it reached export customs.
|
|
23
24
|
const REFRESH_AFTER_MS = 30 * 60_000;
|
|
24
25
|
const USER_TIMEZONE = 'Europe/Zurich';
|
|
26
|
+
// The zone of every Indian office, and the one MySpeedPost labels all clocks in.
|
|
27
|
+
const INDIA_ZONE = 'Asia/Kolkata';
|
|
25
28
|
const MAX_RESPONSE_BYTES = 2_000_000;
|
|
26
29
|
const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '
|
|
27
30
|
+ 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36';
|
|
@@ -119,12 +122,20 @@ const AIRPORT_PLACES = {
|
|
|
119
122
|
function officeAirport(office) {
|
|
120
123
|
return /^Office - ([A-Z]{3})\b/.exec(office)?.[1];
|
|
121
124
|
}
|
|
122
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* A flight's number and route, from a remark naming the office's airport as
|
|
127
|
+
* the origin. Flight remarks have a narrow format; unrelated remarks can
|
|
128
|
+
* contain recipient details.
|
|
129
|
+
*/
|
|
130
|
+
function flightRoute(remarks, office) {
|
|
131
|
+
const match = /^Flight No:\s*([A-Z0-9]{2}\d{1,4}[A-Z]?)\s*\(From ([A-Z]{3}) To ([A-Z]{3})\)$/.exec(clean(remarks, 500));
|
|
132
|
+
return match && match[2] === officeAirport(office) ? match : null;
|
|
133
|
+
}
|
|
123
134
|
function flightDetails(providerCode, remarks, office) {
|
|
124
135
|
if (providerCode !== TAKE_OFF_CODE && providerCode !== 'MailArrived')
|
|
125
136
|
return null;
|
|
126
|
-
const match =
|
|
127
|
-
if (!match
|
|
137
|
+
const match = flightRoute(remarks, office);
|
|
138
|
+
if (!match)
|
|
128
139
|
return null;
|
|
129
140
|
const airport = (code) => AIRPORT_PLACES[code] ? `${AIRPORT_PLACES[code].name} (${code})` : code;
|
|
130
141
|
const departed = providerCode === TAKE_OFF_CODE;
|
|
@@ -166,6 +177,88 @@ function officePoint(rawEvent, office) {
|
|
|
166
177
|
return null;
|
|
167
178
|
return point;
|
|
168
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* The country on MySpeedPost's Destination card: a heading, then label and
|
|
182
|
+
* value pairs, of which only "Country" is read. The card is absent when
|
|
183
|
+
* MySpeedPost has no booking details.
|
|
184
|
+
*/
|
|
185
|
+
function destinationOf($) {
|
|
186
|
+
const heading = $('h3').filter((_, element) => clean($(element).text(), 40) === 'Destination').first();
|
|
187
|
+
for (const label of heading.closest('[x-data]').find('p').toArray()) {
|
|
188
|
+
if (clean($(label).text(), 40).toLowerCase() !== 'country')
|
|
189
|
+
continue;
|
|
190
|
+
// A longer value is not a country name, and a cut one would be wrong.
|
|
191
|
+
const name = clean($(label).next('p').text(), 81);
|
|
192
|
+
return name.length <= 80 && /^\p{L}[\p{L} .,'()&-]*$/u.test(name) ? { code: countryCode(name), name } : null;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
// UPU's marks for a scan by the destination's post ("Item received at office
|
|
197
|
+
// of exchange (Inb)") and by the origin's ("Transfer to OOE (Otb)"). Another
|
|
198
|
+
// reply for the same item can carry bare codes ("ITEM_RECEIVE") instead.
|
|
199
|
+
const INBOUND = /\(Inb\)/i;
|
|
200
|
+
const OUTBOUND = /\(Otb\)/i;
|
|
201
|
+
// India Post's code for a transfer to the office of exchange, in either reply.
|
|
202
|
+
const TRANSFER_CODE = 'ItemTransfered';
|
|
203
|
+
// The codes of a bag's arrival, with the flight's route in the remark.
|
|
204
|
+
const ARRIVAL_CODES = new Set(['MailArrived', 'BagUnloaded']);
|
|
205
|
+
/** The airport a flight relayed on an arrival row lands at. */
|
|
206
|
+
function arrivalAirport(row) {
|
|
207
|
+
return ARRIVAL_CODES.has(clean(row.event_type, 100)) ? flightRoute(row.remarks, clean(row.office, 120))?.[3] ?? null : null;
|
|
208
|
+
}
|
|
209
|
+
function airportCountry(airport) {
|
|
210
|
+
const place = AIRPORT_PLACES[airport];
|
|
211
|
+
return place ? countryCode(place.country) : timeZoneCountry(AIRPORT_ZONES[airport] ?? '');
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Rows the destination's post scanned abroad, which India Post relays with
|
|
215
|
+
* the office's wall clock labelled as India's. They start with an inbound
|
|
216
|
+
* mark or with a flight's arrival in the destination country, whichever is
|
|
217
|
+
* labelled first, so a reply in bare codes picks the same rows as one in
|
|
218
|
+
* prose. Flight rows keep their own reading. An outbound mark, a transfer to
|
|
219
|
+
* the office of exchange or an arrival in another country after that starts
|
|
220
|
+
* a way back, which may be in India again, so it and what follows keep
|
|
221
|
+
* India's clock. Take-offs are not compared: their labels are airport clocks.
|
|
222
|
+
*/
|
|
223
|
+
function scannedAbroad(events, destination) {
|
|
224
|
+
const abroad = new Set();
|
|
225
|
+
if (!destination || destination.code === 'IN')
|
|
226
|
+
return abroad;
|
|
227
|
+
const marked = (row, mark) => mark.test(clean(row.event)) || mark.test(clean(row.remarks));
|
|
228
|
+
const code = (row) => clean(row.event_type, 100);
|
|
229
|
+
const labelled = events.map((row) => isoTime(row.tracked_at, INDIA_ZONE, 100)?.timestamp ?? Number.NaN);
|
|
230
|
+
// The country each arrival row lands in: '' where the airport's is unknown.
|
|
231
|
+
const landed = events.map((row) => {
|
|
232
|
+
const airport = arrivalAirport(row);
|
|
233
|
+
return airport === null ? null : airportCountry(airport) ?? '';
|
|
234
|
+
});
|
|
235
|
+
const entered = Math.min(...events.flatMap((row, index) => (marked(row, INBOUND) || (destination.code !== null && landed[index] === destination.code) ? [labelled[index]] : [])));
|
|
236
|
+
const wayBack = Math.min(...events.flatMap((row, index) => (labelled[index] > entered && (marked(row, OUTBOUND) || code(row) === TRANSFER_CODE || (landed[index] && landed[index] !== destination.code))
|
|
237
|
+
? [labelled[index]] : [])));
|
|
238
|
+
events.forEach((row, index) => {
|
|
239
|
+
if (labelled[index] >= entered && labelled[index] < wayBack && landed[index] === null
|
|
240
|
+
&& code(row) !== TAKE_OFF_CODE)
|
|
241
|
+
abroad.add(index);
|
|
242
|
+
});
|
|
243
|
+
return abroad;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* A clock relayed from abroad, read in the destination's zone when the
|
|
247
|
+
* country keeps one. Elsewhere it stays an offset-less wall time: India's
|
|
248
|
+
* label would place it hours off, and the office's zone is unknown.
|
|
249
|
+
*/
|
|
250
|
+
function foreignClock(trackedAt, zone) {
|
|
251
|
+
const labelled = isoTime(trackedAt, INDIA_ZONE, 100);
|
|
252
|
+
if (!labelled)
|
|
253
|
+
return null;
|
|
254
|
+
const wall = DateTime.fromMillis(labelled.timestamp, { zone: INDIA_ZONE })
|
|
255
|
+
.toISO({ includeOffset: false, suppressMilliseconds: true });
|
|
256
|
+
if (!wall)
|
|
257
|
+
return null;
|
|
258
|
+
const time = zone ? isoTime(wall, zone) : null;
|
|
259
|
+
// The local wall time still orders the row among India's labels.
|
|
260
|
+
return time ? { time } : zone ? null : { local: wall, timestamp: labelled.timestamp };
|
|
261
|
+
}
|
|
169
262
|
export function parseIndiaPostTrackingHtml(html, trackingNumber) {
|
|
170
263
|
const requested = normalizeIndiaPostTrackingNumber(trackingNumber);
|
|
171
264
|
const $ = load(html);
|
|
@@ -188,19 +281,26 @@ export function parseIndiaPostTrackingHtml(html, trackingNumber) {
|
|
|
188
281
|
}
|
|
189
282
|
const parsed = [];
|
|
190
283
|
const seen = new Set();
|
|
191
|
-
|
|
284
|
+
const destination = destinationOf($);
|
|
285
|
+
const rows = events.slice(0, 500);
|
|
286
|
+
const abroad = scannedAbroad(rows, destination);
|
|
287
|
+
const destinationZone = destination ? countryTimeZone(destination.code) : null;
|
|
288
|
+
rows.forEach((rawEvent, index) => {
|
|
192
289
|
const office = clean(rawEvent.office, 120);
|
|
193
290
|
const providerCode = clean(rawEvent.event_type, 100);
|
|
194
291
|
// tracked_at is ISO; offset-less values are read as Asia/Kolkata, the zone
|
|
195
|
-
// every India Post office stamps. A take-off is read on its airport's clock
|
|
292
|
+
// every India Post office stamps. A take-off is read on its airport's clock,
|
|
293
|
+
// and a scan abroad on the destination's.
|
|
196
294
|
const zone = takeOffZone(providerCode, office, clean(rawEvent.tracked_at, 100));
|
|
197
|
-
const
|
|
295
|
+
const foreign = abroad.has(index) ? foreignClock(rawEvent.tracked_at, destinationZone) : null;
|
|
296
|
+
const time = foreign ? ('time' in foreign ? foreign.time : null) : zone
|
|
198
297
|
? mislabeledLocalTime(rawEvent.tracked_at, zone, 100)
|
|
199
|
-
: isoTime(rawEvent.tracked_at,
|
|
298
|
+
: isoTime(rawEvent.tracked_at, INDIA_ZONE, 100);
|
|
299
|
+
const local = foreign && 'local' in foreign ? foreign : null;
|
|
200
300
|
const takeOff = providerCode === TAKE_OFF_CODE;
|
|
201
301
|
const flight = flightDetails(providerCode, rawEvent.remarks, office);
|
|
202
302
|
const description = flight?.description ?? (takeOff ? 'Aircraft Departure' : eventText(clean(rawEvent.event)));
|
|
203
|
-
if (!time || !description)
|
|
303
|
+
if (!(time || local) || !description)
|
|
204
304
|
return;
|
|
205
305
|
const pincode = /^\d{6}$/.test(clean(rawEvent.pincode, 6))
|
|
206
306
|
? clean(rawEvent.pincode, 6)
|
|
@@ -208,7 +308,7 @@ export function parseIndiaPostTrackingHtml(html, trackingNumber) {
|
|
|
208
308
|
const airportCode = flight?.airport ?? (takeOff ? officeAirport(office) : undefined);
|
|
209
309
|
const point = airportCode ? null : officePoint(rawEvent, office);
|
|
210
310
|
const airport = airportCode ? AIRPORT_PLACES[airportCode] : undefined;
|
|
211
|
-
const identity = JSON.stringify([time
|
|
311
|
+
const identity = JSON.stringify([time?.iso ?? local?.local, description, office, pincode, providerCode]);
|
|
212
312
|
if (seen.has(identity))
|
|
213
313
|
return;
|
|
214
314
|
seen.add(identity);
|
|
@@ -218,7 +318,7 @@ export function parseIndiaPostTrackingHtml(html, trackingNumber) {
|
|
|
218
318
|
// contact numbers and address blocks travel on the row and are
|
|
219
319
|
// deliberately never retained.
|
|
220
320
|
event: {
|
|
221
|
-
time: time.iso,
|
|
321
|
+
...(time ? { time: time.iso } : { local_time: local.local }),
|
|
222
322
|
location: airport ? `${airport.name} (${airportCode}), ${airport.country}`
|
|
223
323
|
: flight && !takeOff ? `${flight.airport} Airport` : [office, pincode].filter(Boolean).join(' '),
|
|
224
324
|
description,
|
|
@@ -227,7 +327,7 @@ export function parseIndiaPostTrackingHtml(html, trackingNumber) {
|
|
|
227
327
|
...(point ? { point } : {}),
|
|
228
328
|
},
|
|
229
329
|
classified,
|
|
230
|
-
timestamp: time.timestamp,
|
|
330
|
+
timestamp: time?.timestamp ?? local.timestamp,
|
|
231
331
|
index,
|
|
232
332
|
});
|
|
233
333
|
});
|
|
@@ -244,16 +344,36 @@ export function parseIndiaPostTrackingHtml(html, trackingNumber) {
|
|
|
244
344
|
current_stage: classified.stage,
|
|
245
345
|
last_status_text: latest.event.description,
|
|
246
346
|
last_update: latest.event.time ?? null,
|
|
347
|
+
...(typeof latest.event.local_time === 'string' ? { last_update_local: latest.event.local_time } : {}),
|
|
247
348
|
expected_delivery: null,
|
|
248
|
-
timezone:
|
|
349
|
+
timezone: INDIA_ZONE,
|
|
249
350
|
// When MySpeedPost last asked India Post; an old value means stale history.
|
|
250
351
|
...(syncedAt ? { source_synced_at: syncedAt.iso } : {}),
|
|
352
|
+
// Only the country: a S10 item goes on to that country's post, which the
|
|
353
|
+
// host may confirm with its own lookup.
|
|
354
|
+
...(destination?.code ? { destination_country: destination.code }
|
|
355
|
+
: destination ? { destination_country_name: destination.name } : {}),
|
|
251
356
|
events: parsed.slice(0, 100).map((item) => item.event),
|
|
252
357
|
};
|
|
253
358
|
}
|
|
254
359
|
function enumValue(value) {
|
|
255
360
|
return Array.isArray(value) && typeof value[0] === 'string' ? value[0] : '';
|
|
256
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* A component state other than New, Processing or Completed. One lookup met
|
|
364
|
+
* one in October 2026 without its name being recorded. It says nothing about
|
|
365
|
+
* the shipment, so a completed history the HTML still carries is used, and
|
|
366
|
+
* otherwise the lookup is inconclusive.
|
|
367
|
+
*/
|
|
368
|
+
function otherState(status, html, trackingNumber) {
|
|
369
|
+
try {
|
|
370
|
+
return parseIndiaPostTrackingHtml(html, trackingNumber);
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
const state = /^[A-Za-z_]{1,40}$/.test(status) ? status : 'unrecognised';
|
|
374
|
+
throw new IndeterminateError('India Post', `India Post tracking is in the ${state} state`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
257
377
|
function parseTrackSnapshot(snapshot, trackingNumber) {
|
|
258
378
|
let parsed;
|
|
259
379
|
try {
|
|
@@ -447,9 +567,8 @@ export class IndiaPostTracker {
|
|
|
447
567
|
return cached;
|
|
448
568
|
}
|
|
449
569
|
}
|
|
450
|
-
if (!['New', 'Processing'].includes(initial.status))
|
|
451
|
-
|
|
452
|
-
}
|
|
570
|
+
if (!['New', 'Processing'].includes(initial.status))
|
|
571
|
+
return otherState(initial.status, page.html, normalized);
|
|
453
572
|
const token = csrfToken(page.html);
|
|
454
573
|
return initial.status === 'Processing'
|
|
455
574
|
? this.complete(fetcher, budget, normalized, pageUrl, token, initial.snapshot, [{
|
|
@@ -474,15 +593,14 @@ export class IndiaPostTracker {
|
|
|
474
593
|
const names = dispatchNames(update.effects);
|
|
475
594
|
if (names.has('consignment_not_found'))
|
|
476
595
|
throw new NotFoundError('India Post');
|
|
596
|
+
const html = typeof update.effects.html === 'string' ? update.effects.html : '';
|
|
477
597
|
if (update.component.status === 'Completed') {
|
|
478
|
-
const html = typeof update.effects.html === 'string' ? update.effects.html : '';
|
|
479
598
|
if (!html)
|
|
480
599
|
throw new SchemaError('India Post', 'India Post returned an empty completed response');
|
|
481
600
|
return parseIndiaPostTrackingHtml(html, normalized);
|
|
482
601
|
}
|
|
483
|
-
if (update.component.status !== 'Processing')
|
|
484
|
-
|
|
485
|
-
}
|
|
602
|
+
if (update.component.status !== 'Processing')
|
|
603
|
+
return otherState(update.component.status, html, normalized);
|
|
486
604
|
if (attempt === this.maxPollAttempts)
|
|
487
605
|
break;
|
|
488
606
|
await pause(this.pollIntervalMs, budget.signal);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// MySpeedPost changes a scan's wording while its instant and event_type stay the same.
|
|
2
|
+
// Scans abroad were read on India's clock until October 2026 and now show the
|
|
3
|
+
// destination's offset, unless a reply lacks what places them abroad.
|
|
2
4
|
export const sameInstantIdentityPolicy = {
|
|
3
|
-
sourceCarrierId: 'india-post', storedSources: ['india-post'], requireProviderCode: true,
|
|
5
|
+
sourceCarrierId: 'india-post', storedSources: ['india-post'], requireProviderCode: true, relabelledFrom: 'Asia/Kolkata',
|
|
4
6
|
};
|
|
@@ -13,6 +13,14 @@ export interface SameInstantIdentityPolicy {
|
|
|
13
13
|
/** Match distinct scans sharing an instant by their evidence, with unique matches in both directions. */
|
|
14
14
|
readonly matchEachScan?: boolean;
|
|
15
15
|
readonly matches?: (incoming: SameInstantScan, stored: SameInstantScan) => boolean;
|
|
16
|
+
/**
|
|
17
|
+
* The zone this source once read every wall clock in. A scan showing a wall
|
|
18
|
+
* clock under another offset takes over the stored row whose instant shows
|
|
19
|
+
* that wall clock in this zone, and the reverse, when the provider code
|
|
20
|
+
* (required) and location agree, no event of the batch carries that row,
|
|
21
|
+
* and the match is unique both ways. Wording may differ.
|
|
22
|
+
*/
|
|
23
|
+
readonly relabelledFrom?: string;
|
|
16
24
|
}
|
|
17
25
|
/** Unlisted sources cannot identify a reworded scan by its instant alone. */
|
|
18
26
|
export declare function sameInstantIdentityPolicy(sourceCarrierId: string, options?: {
|
|
@@ -137,7 +137,7 @@ export function parse17TrackResponse(payload, trackingNumber, postcode = null) {
|
|
|
137
137
|
const placed = wall && zone ? DateTime.fromISO(wall, { zone }) : null;
|
|
138
138
|
let parsed;
|
|
139
139
|
try {
|
|
140
|
-
parsed = seventeenTrackEvent(placed?.isValid ?
|
|
140
|
+
parsed = seventeenTrackEvent(raw, operator, placed?.isValid ? placed.toUTC().toISO() ?? undefined : undefined);
|
|
141
141
|
}
|
|
142
142
|
catch (cause) {
|
|
143
143
|
throw new SchemaError(SOURCE, '17TRACK returned an invalid tracking event', { cause });
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { CarrierEvent } from '../../core/result/index.js';
|
|
2
2
|
import { type JsonObject } from '../../core/types.js';
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Keep scan codes/operator and the origin of 17TRACK's converted timestamp.
|
|
5
|
+
* `placed` is the instant a caller read from the facility's own clock instead.
|
|
6
|
+
*/
|
|
7
|
+
export declare function seventeenTrackEvent(raw: JsonObject, operator: JsonObject, placed?: string): CarrierEvent | null;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DateTime } from 'luxon';
|
|
2
|
+
import { EXPLICIT_OFFSET_PATTERN } from '../../core/time/index.js';
|
|
1
3
|
import { isRecord } from '../../core/types.js';
|
|
2
4
|
import { event, text } from '../shared/result.js';
|
|
3
5
|
// Official v2 status vocabulary, plus TransportArrived/Departed observed in
|
|
@@ -18,11 +20,29 @@ const SUB_STAGES = {
|
|
|
18
20
|
Exception_Rejected: 'exception', Exception_Delayed: 'exception', Exception_Lost: 'exception',
|
|
19
21
|
Exception_Destroyed: 'exception', Exception_Cancel: 'exception',
|
|
20
22
|
};
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
+
/**
|
|
24
|
+
* 17TRACK's two readings of a scan usually name one instant. When they
|
|
25
|
+
* disagree, `time_iso` is the clock and offset 17TRACK shows: on India Post's
|
|
26
|
+
* legs `time_utc` lands 30 minutes after it, on scans India Post itself dates.
|
|
27
|
+
*/
|
|
28
|
+
function scanTime(raw) {
|
|
29
|
+
const { time_utc: utc, time_iso: iso } = raw;
|
|
30
|
+
if (typeof utc === 'string' && typeof iso === 'string' && EXPLICIT_OFFSET_PATTERN.test(iso)) {
|
|
31
|
+
const shown = DateTime.fromISO(iso, { setZone: true });
|
|
32
|
+
const converted = DateTime.fromISO(utc, { setZone: true });
|
|
33
|
+
if (shown.isValid && converted.isValid && shown.toMillis() !== converted.toMillis())
|
|
34
|
+
return iso;
|
|
35
|
+
}
|
|
36
|
+
return utc ?? iso;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Keep scan codes/operator and the origin of 17TRACK's converted timestamp.
|
|
40
|
+
* `placed` is the instant a caller read from the facility's own clock instead.
|
|
41
|
+
*/
|
|
42
|
+
export function seventeenTrackEvent(raw, operator, placed) {
|
|
23
43
|
// Some completed responses contain undated explanatory rows alongside real
|
|
24
44
|
// scans. Skip only absent dates; malformed nonempty timestamps still fail.
|
|
25
|
-
const time =
|
|
45
|
+
const time = placed ?? scanTime(raw);
|
|
26
46
|
if (time === null || time === undefined)
|
|
27
47
|
return null;
|
|
28
48
|
const code = text(raw.sub_status);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-parcel-scraper",
|
|
3
|
-
"version": "0.44.0-main.
|
|
3
|
+
"version": "0.44.0-main.414",
|
|
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",
|