universal-parcel-scraper 0.993.0-main.5345 → 0.993.2-main.5350

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.
@@ -5,3 +5,5 @@ export declare const DHLINK_RECORD_ZONE = "Asia/Shanghai";
5
5
  * gives that instant.
6
6
  */
7
7
  export declare function usClockCopy(wall: string, timestamp: number): boolean;
8
+ /** The US zone, the state's first, on whose clock `wall` strikes at `timestamp`; null when none does. */
9
+ export declare function usClockZone(wall: string, timestamp: number, state?: string | null): string | null;
@@ -10,5 +10,9 @@ const US_ZONES = ['America/New_York', 'America/Chicago', 'America/Denver', 'Amer
10
10
  * gives that instant.
11
11
  */
12
12
  export function usClockCopy(wall, timestamp) {
13
- return US_ZONES.some((zone) => isoTime(wall, zone)?.timestamp === timestamp);
13
+ return usClockZone(wall, timestamp) !== null;
14
+ }
15
+ /** The US zone, the state's first, on whose clock `wall` strikes at `timestamp`; null when none does. */
16
+ export function usClockZone(wall, timestamp, state) {
17
+ return [...(state ? [state] : []), ...US_ZONES].find((zone) => isoTime(wall, zone)?.timestamp === timestamp) ?? null;
14
18
  }
@@ -7,7 +7,7 @@ import { countryCode, firstOccurrence, isoTime, usStateTimeZone } from '../../co
7
7
  import { cleanScalar } from '../../core/transport/text.js';
8
8
  import { isRecord } from '../../core/types.js';
9
9
  import { countryZones } from '../../places/zones.js';
10
- import { DHLINK_RECORD_ZONE as RECORD_ZONE, usClockCopy } from './clock.js';
10
+ import { DHLINK_RECORD_ZONE as RECORD_ZONE, usClockCopy, usClockZone } from './clock.js';
11
11
  import { dhlinkEnglish, dhlinkStage, FLIGHT_BOOKED } from './status.js';
12
12
  export const PROVIDER = 'DHLink';
13
13
  // A long history is a few dozen kilobytes.
@@ -179,6 +179,10 @@ function projectRow(row, readAt) {
179
179
  return { stamp: local, event, rank, china, state, hub: usStateTimeZone(hub) !== null,
180
180
  recorded: Math.min(recordedAt(row) ?? readAt, readAt) };
181
181
  }
182
+ /** A relayed wording without the town and state a US partner can write before it ("Sampletown IL, "). */
183
+ function unplaced(description) {
184
+ return (description ?? '').replace(/^[A-Z][A-Za-z .'-]{0,40} [A-Z]{2}, /, '');
185
+ }
182
186
  function outranks(left, right) {
183
187
  const index = left.findIndex((value, position) => value !== right[position]);
184
188
  return index >= 0 && left[index] > right[index];
@@ -259,6 +263,29 @@ export function parseDhlink(payload, raw, readAt = Date.now()) {
259
263
  const zone = scanZone(scan, destination, last?.leg.beijingClock === true && !(scan.hub && last.leg.hubClockPickup));
260
264
  return { scan, zone, time: zone ? isoTime(scan.stamp, zone) : null };
261
265
  });
266
+ // A US partner's scan can come twice, as UniUni's do: on its hub's wall clock, often after the
267
+ // hub's town, and on UTC. The UTC copy's clock is the other's on a US reading, to the second,
268
+ // before DHLink recorded either: one scan, on that US clock.
269
+ const utcCopies = new Set();
270
+ if (destination === 'US') {
271
+ for (const local of dated) {
272
+ if (utcCopies.has(local))
273
+ continue;
274
+ const wording = unplaced(local.scan.event.description);
275
+ const copy = dated.find((other) => {
276
+ if (other === local || utcCopies.has(other) || unplaced(other.scan.event.description) !== wording)
277
+ return false;
278
+ const instant = isoTime(other.scan.stamp, 'UTC')?.timestamp;
279
+ return instant !== undefined && instant <= Math.min(local.scan.recorded, other.scan.recorded) + CLOCK_SKEW_MS
280
+ && usClockZone(local.scan.stamp, instant, local.scan.state) !== null;
281
+ });
282
+ if (!copy)
283
+ continue;
284
+ const zone = usClockZone(local.scan.stamp, isoTime(copy.scan.stamp, 'UTC').timestamp, local.scan.state);
285
+ Object.assign(local, { zone, time: isoTime(local.scan.stamp, zone) });
286
+ utcCopies.add(copy);
287
+ }
288
+ }
262
289
  // Beside DHLink's own record of a partner's scan on Beijing time, the partner's copy can come
263
290
  // on its US wall clock: the same wording, at the instant of DHLink's row on a US reading.
264
291
  // Without a partner known to be relayed on Beijing time, DHLink's row must be read on it.
@@ -266,7 +293,7 @@ export function parseDhlink(payload, raw, readAt = Date.now()) {
266
293
  && other.scan.event.description === scan.event.description && usClockCopy(scan.stamp, other.time.timestamp))) : [];
267
294
  // A place naming only the destination country is the parcel's, not the scan's: a Canada line's
268
295
  // airline hand-over in China reads "Canada".
269
- const kept = dated.filter((entry) => !copies.includes(entry));
296
+ const kept = dated.filter((entry) => !copies.includes(entry) && !utcCopies.has(entry));
270
297
  const events = kept
271
298
  .map(({ scan, time }) => {
272
299
  const { location, ...event } = scan.event;
@@ -207,9 +207,12 @@ const WALL_CLOCKS = new Set(['usps', 'canadapost', 'gofoexpress', 'speedpak', 'e
207
207
  * USPS's own rows read "Shipping Label Created, USPS Awaiting Item". Its date
208
208
  * is the instant: read on its place's clock, a label came eight hours after
209
209
  * ParcelsApp first returned it, and Juxi Express's rows of this shape fall
210
- * between 17TRACK's instants of the same warehouse's steps as labeled.
210
+ * between 17TRACK's instants of the same warehouse's steps as labeled. Only a
211
+ * label or a shipping partner's step is such a row: USPS's own scans can come
212
+ * with their place after them too ("USPS picked up item, EXAMPLE CITY, KY
213
+ * 12345"), and USPS's own tracker dates them on their place's clock.
211
214
  */
212
- const USPS_PARTNER_ROW = /,[^,]+,\s*[A-Z]{2}\s+\d{5}(?:-\d{4})?$/;
215
+ const USPS_PARTNER_ROW = /^(?:shipping label created|[^,]*\bshipping partner\b[^,]*),[^,]+,\s*[A-Z]{2}\s+\d{5}(?:-\d{4})?$/i;
213
216
  const uspsPartnerRow = (name, status) => nameKey(name) === 'usps' && USPS_PARTNER_ROW.test(status.trim());
214
217
  // Known clocks that are the destination or origin country's (`knownClock`).
215
218
  const DESTINATION = 'destination';
@@ -16,7 +16,7 @@ import { runSteps } from '../../core/runner/index.js';
16
16
  import { countryTimeZone, firstOccurrence } from '../../core/time/index.js';
17
17
  import { isRecord } from '../../core/types.js';
18
18
  import { trackingPlace } from '../../places/trackingLocation.js';
19
- import { chinaPostEnglish, chinaPostLabelPlace } from '../../carriers/china-post/status.js';
19
+ import { chinaPostDestinationScan, chinaPostEnglish, chinaPostLabelPlace } from '../../carriers/china-post/status.js';
20
20
  import { agencyIntake, cttSiteZone, isCttExpressAgency } from '../../carriers/ctt/clock.js';
21
21
  import { correosOriginZone } from '../../carriers/correos-spain/clock.js';
22
22
  import { daiPostRelayZone } from '../../carriers/dai-post/clock.js';
@@ -553,6 +553,10 @@ export function parse17TrackResponse(payload, trackingNumber, postcode = null) {
553
553
  ? nzPostAbroad(provider.events, number, nzPostDestination(shipment, tracking.providers)) : new Map();
554
554
  const origin = reported.key === CHINA_POST ? originZone(number) : reported.key === JAPAN_POST ? japanPostOriginZone(number) : null;
555
555
  const outward = reported.key === JAPAN_POST ? OUTWARD_SCAN : ORIGIN_EXCHANGE;
556
+ // China Post's copies of the destination post's own scans keep that post's wall clock, as China Post's adapter
557
+ // reads them, and 17TRACK labels them with China's offset: Correos's arrival at its exchange office at 09:25
558
+ // Madrid time showed in China Post's leg at 09:25 +08:00, seven hours before Correos's own row of it.
559
+ const inbound = reported.key === CHINA_POST ? countryTimeZone(recipientCountry(shipment)) : null;
556
560
  const salvador = reported.key === CORREO_EL_SALVADOR;
557
561
  const sender = salvador && isValidS10TrackingNumber(number) ? countryTimeZone(number.slice(-2)) : null;
558
562
  const bound = reported.key === SWISS_POST ? recipientCountry(shipment) : null;
@@ -581,6 +585,7 @@ export function parse17TrackResponse(payload, trackingNumber, postcode = null) {
581
585
  if (undatedScan(raw))
582
586
  undated++;
583
587
  const exchange = origin !== null && outward.test(text(raw.description).trim());
588
+ const arrived = inbound !== null && chinaPostDestinationScan(text(raw.description));
584
589
  const own = !salvador ? null : OUTBOUND.test(text(raw.description)) ? sender : 'UTC';
585
590
  // The destination post's scan Swiss Post relays, its wall clock under Swiss Post's offset, or Huiyi Logistics' row in China under "+00:00".
586
591
  const relayed = bound ? swissPostDestinationZone(text(raw.description), text(raw.location), bound)
@@ -597,15 +602,15 @@ export function parse17TrackResponse(payload, trackingNumber, postcode = null) {
597
602
  const site = !ctt ? null : cttSiteZone(text(raw.location), countries)
598
603
  ?? (!text(raw.location) && agencyIntake(wallMillis(raw), agencyScans) ? 'Europe/Madrid' : null);
599
604
  // A facility's clock, CTT's, A1's UTC, NZ Post's, Poland Post's, FIC's partner's, Correos de El Salvador's, Correos de Chile's, Shiprocket's, DAI Post's, Quick Rabbit's or an origin post's, on which 17TRACK puts one offset for the whole parcel.
600
- const read = facility || site || relaysFacilityClock(name) || utc || nz || polish || handed || exchange || own || chile || shiprocket || daiPost || quickRabbit
601
- || sent !== undefined;
605
+ const read = facility || site || relaysFacilityClock(name) || utc || nz || polish || handed || exchange || arrived || own || chile || shiprocket || daiPost
606
+ || quickRabbit || sent !== undefined;
602
607
  // Any other row 17TRACK labelled with its zone's offset on the day it looks, not on the scan's.
603
608
  const season = relayed || hywl || read || poland ? null : seasonZone(raw, labelled, now);
604
609
  const wall = relayed || hywl ? rawClock(raw) : read || season ? facilityWall(raw) : null;
605
610
  // A label its mailer wrote on UTC, as USPS's adapter reads it.
606
611
  const scanned = facility && wall ? uspsRowZone(wall, facility, text(raw.description), now.toMillis()) : facility;
607
612
  const zone = !wall ? null : season ?? (hywl ? relayed : relayed ?? scanned ?? site ?? own ?? (sent !== undefined ? sent : abroad.has(index) ? abroad.get(index) ?? null
608
- : exchange ? origin : utc ? 'UTC' : polish ? polish : handed ? handed
613
+ : exchange ? origin : arrived ? inbound : utc ? 'UTC' : polish ? polish : handed ? handed
609
614
  : nz ? 'Pacific/Auckland' : quickRabbit ? 'Europe/Madrid' : chile ? facilityZone(text(raw.location)) ?? 'America/Santiago'
610
615
  : shiprocket ? (placedInIndia(text(raw.location)) ? 'Asia/Kolkata' : null)
611
616
  : daiPost ? daiPostRelayZone(text(raw.description), text(raw.location)) : facilityZone(text(raw.location))));
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.993.0-main.5345"
5
+ "version": "0.993.2-main.5350"
6
6
  },
7
7
  "paths": {
8
8
  "/health": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-parcel-scraper",
3
- "version": "0.993.0-main.5345",
3
+ "version": "0.993.2-main.5350",
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",