universal-parcel-scraper 0.7.1-main.327 → 0.7.2-main.329

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.
@@ -3,6 +3,7 @@ import makeFetchCookie from 'fetch-cookie';
3
3
  import { CookieJar } from 'tough-cookie';
4
4
  import { lookupBudget } from '../../core/adapter/index.js';
5
5
  import { NotFoundError, SchemaError } from '../../core/errors/index.js';
6
+ import { countryCode } from '../../core/time/index.js';
6
7
  import { fetchBounded, parseJsonBytes } from '../../core/transport/index.js';
7
8
  import { isRecord } from '../../core/types.js';
8
9
  import { FALLBACK_EVENT_LABELS, STAGE_STATUS, STATUS_MAP, swissPostEventStage } from './status.js';
@@ -117,9 +118,14 @@ function eventDescription(event, translations, internationalType) {
117
118
  const $ = load(`<span>${value}</span>`);
118
119
  return $('span').text().trim().slice(0, 500) || 'Tracking update';
119
120
  }
120
- /** Operational scan location only: the city and its postcode, never a street. */
121
+ /** Operational scan location: city and postcode, or an explicit country when city is absent. */
121
122
  function eventLocation(event) {
122
- return [text(event.city, 100), text(event.zip, 30)].filter(Boolean).join(' ').slice(0, 160);
123
+ const city = text(event.city, 100);
124
+ const postcode = text(event.zip, 30);
125
+ const country = countryCode(event.country);
126
+ if (!city)
127
+ return country && country !== 'ZZ' ? country : postcode;
128
+ return [city, postcode].filter(Boolean).join(' ').slice(0, 160);
123
129
  }
124
130
  /**
125
131
  * Local ordering policy: event timestamps are read only to sort. Values without
@@ -0,0 +1,2 @@
1
+ import type { SameInstantIdentityPolicy } from '../../core/catalog/eventIdentity.js';
2
+ export declare const sameInstantIdentityPolicy: SameInstantIdentityPolicy;
@@ -0,0 +1,6 @@
1
+ import { matchesLocationEnrichment } from '../../core/catalog/locationIdentity.js';
2
+ export const sameInstantIdentityPolicy = {
3
+ sourceCarrierId: 'swiss-post', storedSources: ['swiss-post'], requireProviderCode: true,
4
+ matchEachScan: true,
5
+ matches: matchesLocationEnrichment,
6
+ };
@@ -10,6 +10,8 @@ export interface SameInstantIdentityPolicy {
10
10
  readonly sourceCarrierId: string;
11
11
  readonly storedSources: readonly string[];
12
12
  readonly requireProviderCode: boolean;
13
+ /** Match distinct scans sharing an instant by their evidence, with unique matches in both directions. */
14
+ readonly matchEachScan?: boolean;
13
15
  readonly matches?: (incoming: SameInstantScan, stored: SameInstantScan) => boolean;
14
16
  }
15
17
  /** Unlisted sources cannot identify a reworded scan by its instant alone. */
@@ -1,7 +1,9 @@
1
1
  import { sameInstantIdentityPolicy as dpd } from '../../carriers/dpd/app.js';
2
2
  import { sameInstantIdentityPolicy as indiaPost } from '../../carriers/india-post/app.js';
3
3
  import { sameInstantIdentityPolicy as ups } from '../../carriers/ups/app.js';
4
- const policies = new Map([dpd, indiaPost, ups].map((policy) => [policy.sourceCarrierId, policy]));
4
+ import { sameInstantIdentityPolicy as swissPost } from '../../carriers/swiss-post/app.js';
5
+ import { sameInstantIdentityPolicy as universal } from '../../providers/shared/app.js';
6
+ const policies = new Map([dpd, indiaPost, ups, swissPost, universal].map((policy) => [policy.sourceCarrierId, policy]));
5
7
  /** Unlisted sources cannot identify a reworded scan by its instant alone. */
6
8
  export function sameInstantIdentityPolicy(sourceCarrierId, options = {}) {
7
9
  const policy = policies.get(sourceCarrierId);
@@ -0,0 +1,3 @@
1
+ import type { SameInstantScan } from './eventIdentity.js';
2
+ /** Adding a place must keep the scan's wording and known stage; conflicting places do not match. */
3
+ export declare function matchesLocationEnrichment(incoming: SameInstantScan, stored: SameInstantScan): boolean;
@@ -0,0 +1,10 @@
1
+ const normalized = (value) => value.replace(/\s+/g, ' ').trim().toLocaleLowerCase('en-US');
2
+ /** Adding a place must keep the scan's wording and known stage; conflicting places do not match. */
3
+ export function matchesLocationEnrichment(incoming, stored) {
4
+ const wording = normalized(incoming.description);
5
+ const location = normalized(incoming.location);
6
+ const savedLocation = normalized(stored.location);
7
+ return Boolean(location && wording) && !['', 'unknown'].includes(incoming.stage)
8
+ && incoming.stage === stored.stage && wording === normalized(stored.description)
9
+ && (!savedLocation || location === savedLocation);
10
+ }
@@ -63,6 +63,11 @@ export function trackingPlace(location) {
63
63
  if (country)
64
64
  return { country, place: kept ? text.slice(0, fields[kept].index).replace(/[\s,;|(]+$/u, '') : '' };
65
65
  if (fields.length === 1 && text === fields[0][0]) {
66
+ // Aggregators can name a DHL country operation instead of a town.
67
+ const operation = /^DHL\s+(.+)$/i.exec(text);
68
+ const operationCountry = operation ? namedCountry(operation[1], true) : null;
69
+ if (operationCountry)
70
+ return { country: operationCountry, place: '' };
66
71
  const words = text.split(/\s+/);
67
72
  for (let count = Math.min(words.length - 1, 4); count > 0; count -= 1) {
68
73
  const code = namedCountry(words.slice(0, count).join(' '), true);
@@ -221,6 +221,10 @@ function parseHistory(payload, trackingNumber, timezone = null) {
221
221
  continue;
222
222
  }
223
223
  if (parsed) {
224
+ const location = typeof raw.location === 'string' ? raw.location.replace(/\s+/g, ' ').trim().slice(0, 200) : '';
225
+ // Some Chronopost rows put the service label in the location field.
226
+ if (location && !/^Type de livraison\s*:/i.test(location))
227
+ parsed.location = location;
224
228
  events.push(parsed);
225
229
  scanCarriers.add(name);
226
230
  }
@@ -0,0 +1,2 @@
1
+ import type { SameInstantIdentityPolicy } from '../../core/catalog/eventIdentity.js';
2
+ export declare const sameInstantIdentityPolicy: SameInstantIdentityPolicy;
@@ -0,0 +1,6 @@
1
+ import { matchesLocationEnrichment } from '../../core/catalog/locationIdentity.js';
2
+ export const sameInstantIdentityPolicy = {
3
+ sourceCarrierId: 'unknown', storedSources: ['unknown'], requireProviderCode: false,
4
+ matchEachScan: true,
5
+ matches: matchesLocationEnrichment,
6
+ };
@@ -163,7 +163,14 @@ function moment(value) {
163
163
  export function result(events, source, preserveOrder = false) {
164
164
  // Wall times sort alongside UTC instants only approximately; providers that
165
165
  // omit offsets everywhere pass preserveOrder instead.
166
- const unique = [...new Map(events.map((e) => [`${moment(e)}|${e.description}`, e])).values()]
166
+ const scans = new Map();
167
+ for (const event of events) {
168
+ const key = `${moment(event)}|${event.description}`;
169
+ const earlier = scans.get(key);
170
+ // A carrier's copy can lack the place another copy of the same scan reported.
171
+ scans.set(key, !event.location && earlier?.location ? { ...event, location: earlier.location } : event);
172
+ }
173
+ const unique = [...scans.values()]
167
174
  .sort((a, b) => preserveOrder ? 0 : moment(b).localeCompare(moment(a))).slice(0, 100);
168
175
  // A well-formed reply with no scan left after notices and private details proves nothing about the parcel.
169
176
  if (!unique.length)
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.7.1"
5
+ "version": "0.7.2"
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.7.1-main.327",
3
+ "version": "0.7.2-main.329",
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",