universal-parcel-scraper 0.69.2-main.888 → 0.69.3-main.891

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.
@@ -0,0 +1,2 @@
1
+ import type { SameInstantIdentityPolicy } from '../../core/catalog/eventIdentity.js';
2
+ export declare const sameInstantIdentityPolicy: SameInstantIdentityPolicy;
@@ -0,0 +1,7 @@
1
+ // Scans relayed from other posts kept Canada Post's -05:00 or -04:00 until
2
+ // October 2026, and those in a country with one clock now show its offset.
3
+ // The old label is read as -05:00, the one Canada Post gives them in winter and
4
+ // often in summer; a row stored under -04:00 is not recognized.
5
+ export const sameInstantIdentityPolicy = {
6
+ sourceCarrierId: 'canada-post', storedSources: ['canada-post'], requireProviderCode: true, relabelledFrom: 'Etc/GMT+5',
7
+ };
@@ -81,8 +81,9 @@ const EXCHANGE_SCANS = new Set(['4000', '4100', '4202', '4310', '4311', '4330',
81
81
  * Post's own page, a Korean one is on Korea Post's clock, a Montana delivery
82
82
  * office's "05:05 -05:00" is 05:05 MDT on USPS's page, and the Toronto
83
83
  * exchange office's "01:11 -05:00" fell in summer time (checked 2026-10-10).
84
- * Such a scan is read on its country's single clock; elsewhere it stays a
85
- * wall clock. Null for a scan whose offset stands.
84
+ * Such a scan is read on its country's single clock; elsewhere it keeps
85
+ * Canada Post's offset, the last guess: hours off, but the reply keeps its
86
+ * instant. Null for a scan whose offset stands.
86
87
  */
87
88
  function relayedClock(code, country, region) {
88
89
  const abroad = country !== '' && country !== 'CA';
@@ -104,12 +105,12 @@ function eventClock(value, relayed = null) {
104
105
  const placed = relayed?.zone ? isoTime(local, relayed.zone) : null;
105
106
  if (placed)
106
107
  return { time: placed.iso };
107
- if (!relayed && /^(?:Z|[+-](?:(?:0\d|1[0-3]):[0-5]\d|14:00))$/i.test(offset)) {
108
+ if (/^(?:Z|[+-](?:(?:0\d|1[0-3]):[0-5]\d|14:00))$/i.test(offset)) {
108
109
  const parsed = explicitOffsetTime(`${local}${offset}`);
109
110
  if (parsed)
110
111
  return { time: parsed.iso };
111
112
  }
112
- else if (relayed || !offset) {
113
+ else if (!offset) {
113
114
  // UTC validates the calendar only; no timezone is attached to local digits.
114
115
  const parsed = DateTime.fromISO(local, { zone: 'UTC' });
115
116
  const valid = parsed.isValid ? parsed.toISO({ includeOffset: false, suppressMilliseconds: true }) : null;
@@ -184,14 +185,17 @@ export function parseCanadaPostTrackingResponse(payload, trackingNumber) {
184
185
  const location = isRecord(raw.locationAddr)
185
186
  ? [textField(raw.locationAddr, 'city', 160), region].filter(Boolean).join(', ') : '';
186
187
  const stage = type === 'Signature' || code === '20' ? null : canadaPostScanStage(code, description) ?? canadaPostStage(description);
187
- const event = { ...eventClock(raw.datetime, relayedClock(code, country, region)), description: stage === 'delivered' ? 'Delivered' : description,
188
+ const relayed = relayedClock(code, country, region);
189
+ const clock = eventClock(raw.datetime, relayed);
190
+ const event = { ...clock, description: stage === 'delivered' ? 'Delivered' : description,
188
191
  provider_code: code, ...(stage ? { stage } : {}), ...(location ? { location } : {}) };
189
192
  return { event, type, region, country, sourceDate: isRecord(raw.datetime) ? calendarDate(raw.datetime.date) : null,
190
- returnCue: returnStarted(description) || type === 'RtsLabelProc' };
193
+ returnCue: returnStarted(description) || type === 'RtsLabelProc', guessed: relayed?.zone === null && 'time' in clock };
191
194
  });
192
195
  // Native detail history is newest first. Compare instants only when every
193
- // row resolves; an unresolved latest row must not fall behind old scans.
194
- if (rows.every(row => row.event.time))
196
+ // row resolves, and none only by a guess; an unresolved latest row must not
197
+ // fall behind old scans, nor a guessed one behind a scan placed abroad.
198
+ if (rows.every(row => row.event.time && !row.guessed))
195
199
  rows.sort((a, b) => Date.parse(b.event.time) - Date.parse(a.event.time));
196
200
  let returning = false;
197
201
  for (const row of [...rows].reverse()) {
@@ -89,9 +89,10 @@ const WALL_CLOCK = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.\d{1,3})?(?:Z|\+0
89
89
  * A scan's clock. CTT's Z is no offset: the digits are the wall clock where
90
90
  * the scan happened (see clock.ts). That is the destination's, for a
91
91
  * partner post's scan of a parcel bound abroad (`abroad`), unless the
92
- * location names an office or a site of CTT's; Portugal's otherwise. A
93
- * destination with several zones leaves the scan a wall time. Any other
94
- * offset is taken as sent.
92
+ * location names an office or a site of CTT's; Portugal's otherwise. In a
93
+ * destination with several zones the scan keeps CTT's Z, the last guess:
94
+ * hours off, but the reply keeps its instant. Only a wall clock sent with no
95
+ * Z stays a wall time there. Any other offset is taken as sent.
95
96
  */
96
97
  function scanClock(value, location, abroad) {
97
98
  const raw = typeof value === 'string' ? value.trim() : '';
@@ -104,6 +105,9 @@ function scanClock(value, location, abroad) {
104
105
  const placed = zone ? isoTime(wall, zone) : null;
105
106
  if (placed)
106
107
  return { time: placed.iso, timestamp: placed.timestamp };
108
+ const labelled = explicitOffsetTime(raw);
109
+ if (labelled)
110
+ return { time: labelled.iso, timestamp: labelled.timestamp };
107
111
  const read = Date.parse(`${wall}Z`);
108
112
  return Number.isFinite(read) ? { local_time: wall, timestamp: read } : null;
109
113
  }
@@ -0,0 +1,2 @@
1
+ import type { SameInstantIdentityPolicy } from '../../core/catalog/eventIdentity.js';
2
+ export declare const sameInstantIdentityPolicy: SameInstantIdentityPolicy;
@@ -0,0 +1,6 @@
1
+ // Scan times were read as the UTC their Z says until October 2026, and now show
2
+ // the clock of the place the scan happened: Lisbon's, the Azores' or the
3
+ // destination's. Lisbon's winter scans keep their instants under +00:00.
4
+ export const sameInstantIdentityPolicy = {
5
+ sourceCarrierId: 'ctt', storedSources: ['ctt'], requireProviderCode: true, relabelledFrom: 'UTC',
6
+ };
@@ -14,7 +14,9 @@ function matches(incoming, stored) {
14
14
  && wording === normalized(stored.description)
15
15
  && (location === savedLocation || (!location && isNzPostPlaceholderLocation(savedLocation)));
16
16
  }
17
+ // Scans abroad were read on Auckland's clock until October 2026 and now show
18
+ // the offset of the country they happened in, unless nothing places them.
17
19
  export const sameInstantIdentityPolicy = {
18
20
  sourceCarrierId: 'nz-post', storedSources: ['nz-post'], requireProviderCode: true,
19
- matchEachScan: true, matches,
21
+ matchEachScan: true, matches, relabelledFrom: 'Pacific/Auckland',
20
22
  };
@@ -53,7 +53,8 @@ function abroadWall(clocked) {
53
53
  * an airline's included ("China"), else the nearest later one's. All of them
54
54
  * are the destination post's or the flight's to it; a transit hub's country
55
55
  * can be wrong, but Hong Kong, Singapore and China keep one clock. Null when
56
- * that country has several clocks or none is named: the scan stays a wall time.
56
+ * that country has several clocks or none is named: the scan keeps NZ Post's
57
+ * Auckland reading, the last guess.
57
58
  */
58
59
  function abroadZone(scans, position) {
59
60
  const country = scans[position].country ?? scans.slice(0, position).reverse().find(other => other.country)?.country
@@ -119,23 +120,27 @@ export function parseNzPost(payload, number) {
119
120
  return { ...clocked, description, provider_code: code,
120
121
  ...(location ? { location } : {}), ...(mapped ? { stage: mapped.stage } : {}) };
121
122
  });
123
+ // A scan nothing places keeps NZ Post's Auckland reading, the last guess:
124
+ // hours off, but the reply keeps its instant.
125
+ let guessed = false;
122
126
  afterDeparture.forEach((scan, position) => {
123
127
  // An airline's scan only lends its country: its clock was not checked.
124
128
  if (!scan.wall)
125
129
  return;
126
130
  const zone = abroadZone(afterDeparture, position);
127
131
  const placed = zone ? isoTime(scan.wall, zone) : null;
128
- const event = { ...projected[scan.index], ...placed ? { time: placed.iso } : { local_time: scan.wall } };
129
- if (!placed)
130
- delete event.time;
131
- projected[scan.index] = event;
132
+ if (placed)
133
+ projected[scan.index] = { ...projected[scan.index], time: placed.iso };
134
+ else
135
+ guessed = true;
132
136
  });
133
137
  if (!projected.length)
134
138
  throw new IndeterminateError('nz-post', 'NZ Post returned no parcel history');
135
139
  // The consumer client treats the last source row as current. Preserve that
136
- // order if any clock is unresolved instead of promoting an older dated row.
140
+ // order if any clock is unresolved, or only guessed, instead of promoting an
141
+ // older dated row: a guessed clock can fall hours behind one placed abroad.
137
142
  projected.reverse();
138
- if (projected.every(event => event.time))
143
+ if (!guessed && projected.every(event => event.time))
139
144
  projected.sort((a, b) => Date.parse(b.time) - Date.parse(a.time));
140
145
  const seen = new Set();
141
146
  const events = projected.filter(event => {
@@ -1,6 +1,10 @@
1
1
  import { matchesLocationEnrichment } from '../../core/catalog/locationIdentity.js';
2
+ // The destination post's scans of a parcel sent abroad came with Swiss Post's
3
+ // Zurich offset until October 2026, and now show the destination's when it
4
+ // keeps one clock.
2
5
  export const sameInstantIdentityPolicy = {
3
6
  sourceCarrierId: 'swiss-post', storedSources: ['swiss-post'], requireProviderCode: true,
4
7
  matchEachScan: true,
5
8
  matches: matchesLocationEnrichment,
9
+ relabelledFrom: 'Europe/Zurich',
6
10
  };
@@ -2,9 +2,11 @@ import { sameInstantIdentityPolicy as aliexpress } from '../../carriers/aliexpre
2
2
  import { sameInstantIdentityPolicy as amazonShipping } from '../../carriers/amazon-shipping/app.js';
3
3
  import { sameInstantIdentityPolicy as australiaPost } from '../../carriers/australia-post/app.js';
4
4
  import { sameInstantIdentityPolicy as austrianPost } from '../../carriers/austrian-post/identity.js';
5
+ import { sameInstantIdentityPolicy as canadaPost } from '../../carriers/canada-post/app.js';
5
6
  import { sameInstantIdentityPolicy as chronopost } from '../../carriers/chronopost/identity.js';
6
7
  import { sameInstantIdentityPolicy as ciblex } from '../../carriers/ciblex/identity.js';
7
8
  import { sameInstantIdentityPolicy as correosExpress } from '../../carriers/correos-express/identity.js';
9
+ import { sameInstantIdentityPolicy as ctt } from '../../carriers/ctt/app.js';
8
10
  import { sameInstantIdentityPolicy as dpd } from '../../carriers/dpd/app.js';
9
11
  import { sameInstantIdentityPolicy as dpdFrance } from '../../carriers/dpd-fr/identity.js';
10
12
  import { sameInstantIdentityPolicy as hermesGermany } from '../../carriers/hermes-de/identity.js';
@@ -23,7 +25,7 @@ import { sameInstantIdentityPolicy as ups } from '../../carriers/ups/app.js';
23
25
  import { sameInstantIdentityPolicy as swissPost } from '../../carriers/swiss-post/app.js';
24
26
  import { sameInstantIdentityPolicy as yanwen } from '../../carriers/yanwen/app.js';
25
27
  import { sameInstantIdentityPolicy as universal } from '../../providers/shared/app.js';
26
- const policies = new Map([aliexpress, amazonShipping, australiaPost, austrianPost, chronopost, ciblex, correosExpress, dpd, dpdFrance, hermesGermany, indiaPost, jdLogistics, lbcExpress, mondialRelay, mrw, nzPost, posMalaysia, posteItaliane, posti, speedpak, sunYou, ups, swissPost, yanwen, universal].map((policy) => [policy.sourceCarrierId, policy]));
28
+ const policies = new Map([aliexpress, amazonShipping, australiaPost, austrianPost, canadaPost, chronopost, ciblex, correosExpress, ctt, dpd, dpdFrance, hermesGermany, indiaPost, jdLogistics, lbcExpress, mondialRelay, mrw, nzPost, posMalaysia, posteItaliane, posti, speedpak, sunYou, ups, swissPost, yanwen, universal].map((policy) => [policy.sourceCarrierId, policy]));
27
29
  /** Unlisted sources cannot identify a reworded scan by its instant alone. */
28
30
  export function sameInstantIdentityPolicy(sourceCarrierId, options = {}) {
29
31
  const policy = policies.get(sourceCarrierId);
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.69.2"
5
+ "version": "0.69.3"
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.69.2-main.888",
3
+ "version": "0.69.3-main.891",
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",