universal-parcel-scraper 0.862.0-main.4623 → 0.862.2-main.4627

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.
@@ -6,7 +6,7 @@ import { ChallengeError, IndeterminateError, InvalidInputError, NotFoundError, S
6
6
  import { listsNewestFirst } from '../../core/result/order.js';
7
7
  import { cleanScalar, decodeText, fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
8
8
  import { isRecord } from '../../core/types.js';
9
- import { classifyStatus, statusKey } from './status.js';
9
+ import { classifyStatus, DELAYED, statusKey } from './status.js';
10
10
  const PROVIDER = 'Amazon Shipping';
11
11
  const DEFAULT_TIMEOUT_MS = 15_000;
12
12
  const MAX_RESPONSE_BYTES = 2_000_000;
@@ -266,7 +266,10 @@ export function parseAmazonShippingTrackingResponse(payload, zone = 'Europe/Pari
266
266
  // that had just left their pickup site and went on being scanned for another day.
267
267
  const unscannedDelivery = current.stage === 'delivered' && latestKnown !== undefined
268
268
  && !parsedEvents.some((item) => item.classified.stage === 'delivered');
269
- const active = current.status !== 'unknown' && !echoesNewest && !unscannedDelivery ? current : latestKnown?.classified;
269
+ // A delay moves nothing: a parcel whose scans show only its label keeps that stage, with the delay as its text.
270
+ const unmovedDelay = current === DELAYED && latestKnown?.classified.stage === 'registered';
271
+ const active = unmovedDelay ? { ...latestKnown.classified, description: DELAYED.description }
272
+ : current.status !== 'unknown' && !echoesNewest && !unscannedDelivery ? current : latestKnown?.classified;
270
273
  if (!active) {
271
274
  throw new SchemaError(PROVIDER, 'Amazon Shipping returned incomplete tracking details');
272
275
  }
@@ -20,6 +20,8 @@ export interface ClassifiedStatus {
20
20
  description: string;
21
21
  }
22
22
  export declare const UNKNOWN_STATUS: ClassifiedStatus;
23
+ /** A late delivery: no exception, and no movement of its own. */
24
+ export declare const DELAYED: ClassifiedStatus;
23
25
  /** Fold any provider signal into the comparison key: lower case, letters and digits only. */
24
26
  export declare function statusKey(value: unknown): string;
25
27
  /** Classify one or more provider signals; the first matching rule wins. */
@@ -3,6 +3,12 @@ export const UNKNOWN_STATUS = {
3
3
  stage: 'in_transit',
4
4
  description: 'Amazon Shipping update',
5
5
  };
6
+ /** A late delivery: no exception, and no movement of its own. */
7
+ export const DELAYED = {
8
+ status: 'in_transit',
9
+ stage: 'in_transit',
10
+ description: 'Delivery delayed',
11
+ };
6
12
  /** Fold any provider signal into the comparison key: lower case, letters and digits only. */
7
13
  export function statusKey(value) {
8
14
  if (typeof value !== 'string' && typeof value !== 'number')
@@ -48,9 +54,8 @@ export function classifyStatus(...values) {
48
54
  ])) {
49
55
  return { status: 'exception', stage: 'failed_attempt', description: 'Delivery issue' };
50
56
  }
51
- if (includesAny(key, ['delayed', 'late'])) {
52
- return { status: 'in_transit', stage: 'in_transit', description: 'Delivery delayed' };
53
- }
57
+ if (includesAny(key, ['delayed', 'late']))
58
+ return DELAYED;
54
59
  if (includesAny(key, ['delivered'])) {
55
60
  return { status: 'delivered', stage: 'delivered', description: 'Delivered' };
56
61
  }
@@ -50,6 +50,14 @@ export interface HualeiTenant {
50
50
  * tracking number it shows is the delivery carrier's.
51
51
  */
52
52
  readonly pageEcho?: true;
53
+ /**
54
+ * The tenant's own numbers. Once the delivery carrier's number has taken
55
+ * `trackingNumber`, the track interface still finds an order by its own
56
+ * number but returns it in no field: a lone record that holds none of the
57
+ * number then answers it, unless its tracking number is another of the
58
+ * tenant's own. A tracking number of another shape is the delivery carrier's.
59
+ */
60
+ readonly ownNumber?: RegExp;
53
61
  }
54
62
  /**
55
63
  * The zone of the trackers' own clock: it decides when a row written ahead is
@@ -266,16 +266,20 @@ function trackRow(raw, provider) {
266
266
  /**
267
267
  * The delivery carrier and its number: the channel's transfer number, else the
268
268
  * tracking number, which some lines fill with the delivery carrier's. Without
269
- * a channel, only the transfer number of a carrier the tenant hands to.
269
+ * a channel, only the transfer number, or the tracking number a tenant's own
270
+ * number shape marks as the delivery carrier's, of a carrier the tenant hands to.
270
271
  */
271
- function partnerOf(record, destination, tenant) {
272
+ function partnerOf(record, destination, tenant, downstream = '') {
272
273
  const { provider } = tenant;
273
274
  const label = text(record, 'channel_name', provider, 100);
274
275
  if (!label) {
275
- const number = identity(text(record, 'business_channeltransfercode', provider, 64));
276
- const detected = number ? detectCarrierMatch(number) : undefined;
277
- const carrier = detected?.confidence === 'high' ? detected.carrier : undefined;
278
- return carrier && tenant.unlabelledPartners?.includes(carrier) ? { carrier, number } : undefined;
276
+ for (const number of [identity(text(record, 'business_channeltransfercode', provider, 64)), downstream]) {
277
+ const detected = number ? detectCarrierMatch(number) : undefined;
278
+ const carrier = detected?.confidence === 'high' ? detected.carrier : undefined;
279
+ if (carrier && tenant.unlabelledPartners?.includes(carrier))
280
+ return { carrier, number };
281
+ }
282
+ return undefined;
279
283
  }
280
284
  for (const key of ['business_channeltransfercode', 'trackingNumber']) {
281
285
  const number = identity(text(record, key, provider, 64));
@@ -314,6 +318,8 @@ export function parseHualeiTrack(payload, raw, tenant, readAt = Date.now()) {
314
318
  });
315
319
  // The service matches a number in any of the record's own fields; a reference two orders share is no answer.
316
320
  const bound = records.filter((record) => IDENTITY_FIELDS.some((key) => identity(text(record, key, provider, 64)) === identity(number)));
321
+ const own = (value) => Boolean(tenant.ownNumber?.test(value));
322
+ const trackingOf = (record) => identity(text(record, 'trackingNumber', provider, 64));
317
323
  if (!bound.length) {
318
324
  // The names of the fields that still carry the number, a list such as a child parcel's included, or eight
319
325
  // characters or more of it, never their values, tell a number the platform moved from another shipment or wrote
@@ -324,14 +330,21 @@ export function parseHualeiTrack(payload, raw, tenant, readAt = Date.now()) {
324
330
  .map(([key]) => key.slice(0, 40))))].slice(0, 4);
325
331
  const whole = fields((value) => value.includes(identity(number)), true);
326
332
  const part = whole.length ? [] : fields((value) => value.length >= 8 && identity(number).includes(value));
327
- const shapes = whole.length || part.length ? [] : [...new Set(records.flatMap((record) => IDENTITY_FIELDS
328
- .map((key) => [key, text(record, key, provider, 64)])
329
- .filter(([, value]) => value)
330
- .map(([key, value]) => `${key} ${numberShape(value)}`)))].slice(0, 4);
331
- throw new IndeterminateError(provider, `${provider} answered with another shipment${whole.length ? `; its ${whole.join(', ')} holds the number`
332
- : part.length ? `; its ${part.join(', ')} holds part of the number` : shapes.length ? `; its numbers read ${shapes.join(', ')}` : ''}`);
333
+ // A tenant's own number the record holds nowhere is the one the interface found it by, once the delivery
334
+ // carrier's number has taken its tracking number.
335
+ if (own(number) && records.length === 1 && !whole.length && !part.length && !own(trackingOf(records[0]))) {
336
+ bound.push(records[0]);
337
+ }
338
+ else {
339
+ const shapes = whole.length || part.length ? [] : [...new Set(records.flatMap((record) => IDENTITY_FIELDS
340
+ .map((key) => [key, text(record, key, provider, 64)])
341
+ .filter(([, value]) => value)
342
+ .map(([key, value]) => `${key} ${numberShape(value)}`)))].slice(0, 4);
343
+ throw new IndeterminateError(provider, `${provider} answered with another shipment${whole.length ? `; its ${whole.join(', ')} holds the number`
344
+ : part.length ? `; its ${part.join(', ')} holds part of the number` : shapes.length ? `; its numbers read ${shapes.join(', ')}` : ''}`);
345
+ }
333
346
  }
334
- if (new Set(bound.map((record) => identity(text(record, 'trackingNumber', provider, 64)))).size > 1) {
347
+ if (new Set(bound.map(trackingOf)).size > 1) {
335
348
  throw new IndeterminateError(provider, `${provider} holds several shipments under the number`);
336
349
  }
337
350
  const record = bound[0];
@@ -339,9 +352,12 @@ export function parseHualeiTrack(payload, raw, tenant, readAt = Date.now()) {
339
352
  if (!Array.isArray(details) || details.length > MAX_ROWS)
340
353
  throw new SchemaError(provider, `${provider} returned an invalid history`);
341
354
  const destination = text(record, 'consigneeCountry', provider, 8).toUpperCase();
355
+ const tracking = trackingOf(record);
356
+ // On a tenant that names its own numbers, a tracking number of another shape is the delivery carrier's.
357
+ const downstream = tenant.ownNumber && tracking && !own(tracking) ? tracking : '';
342
358
  return result(details.map((row) => trackRow(row, provider)), {
343
- number, destination, partner: partnerOf(record, destination, tenant),
344
- tracking: identity(text(record, 'trackingNumber', provider, 64)),
359
+ number, destination, partner: partnerOf(record, destination, tenant, downstream),
360
+ tracking: downstream ? '' : tracking, ...(downstream ? { downstream } : {}),
345
361
  service: text(record, 'productKindName', provider, 100) || undefined,
346
362
  }, tenant, readAt);
347
363
  }
@@ -2,9 +2,12 @@ import { normalizeHualeiNumber, parseHualeiPage, parseHualeiTrack } from '../gzy
2
2
  import { isJrtUpsRow, jrtEnglish, jrtStage } from './status.js';
3
3
  export const PROVIDER = 'JRT';
4
4
  export const JRT_ORIGIN = 'http://106.53.64.22:8082';
5
+ // Once JRT hands a parcel over, the record's tracking number is the delivering carrier's, as
6
+ // DHL's into Europe, and JRT's own number, which still finds the order, is in no field.
5
7
  export const JRT = {
6
8
  carrier: 'jrt', provider: PROVIDER, origin: JRT_ORIGIN, stage: jrtStage, english: jrtEnglish,
7
9
  clock: ({ content, location }) => (isJrtUpsRow(content, location) ? 'wall' : 'platform'),
10
+ ownNumber: /^JRT\d{10}[A-Z]{2}$/, unlabelledPartners: ['dhl'],
8
11
  };
9
12
  export const normalizeJrtNumber = (raw) => normalizeHualeiNumber(raw, PROVIDER);
10
13
  /** The track interface's reply, or null when it holds no row for the number. */
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.862.0-main.4623"
5
+ "version": "0.862.2-main.4627"
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.862.0-main.4623",
3
+ "version": "0.862.2-main.4627",
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",