universal-parcel-scraper 0.241.0 → 0.241.1-main.1513

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/README.md CHANGED
@@ -155,7 +155,7 @@ createTracker({ providers: ['ParcelsApp', 'Ship24', '17TRACK', 'Postal Ninja', '
155
155
  <!-- GENERATED:stages -->
156
156
  <img src="docs/assets/stages.svg" alt="DHL: Die Sendung wurde in das Zustellfahrzeug geladen.; Mondial Relay: En cours de livraison; Correios Brazil: Objeto saiu para entrega ao destinatário; Correos Express: EN REPARTO; Yamato Transport: 配達中; La Poste / Colissimo: DISTOU. All are filed under out_for_delivery." width="760">
157
157
 
158
- The carrier folders record 10,813 such statuses, each filed under one stage.
158
+ The carrier folders record 10,817 such statuses, each filed under one stage.
159
159
  <!-- /GENERATED:stages -->
160
160
 
161
161
  Wording nobody recorded yet goes through a classifier that reads seven European languages.
@@ -21798,7 +21798,7 @@ for (let e of [
21798
21798
  ]) {
21799
21799
  let t = e === "en" ? ss : new Intl.DisplayNames([e], { type: "region" });
21800
21800
  for (let n of cs) {
21801
- let r = t.of(n), i = e === "de" ? [r, r.replace(/[äöüÄÖÜ]/g, (e) => `${e.normalize("NFD")[0]}e`)] : [r];
21801
+ let r = t.of(n), i = e === "de" ? [r, r.replace(/[äöüÄÖÜ]/g, (e) => `${e.normalize("NFD")[0]}e`)] : e === "en" ? [r, r.replace(/ & /g, " and ").replace(/^St\. /, "Saint ")] : [r];
21802
21802
  for (let t of i.map(ls)) !us.has(t) && (e === "en" || !Jo(t)) && us.set(t, n);
21803
21803
  }
21804
21804
  }
@@ -2,6 +2,7 @@ import { DateTime } from 'luxon';
2
2
  import { normalizeTrackingNumber } from '../../core/detection/index.js';
3
3
  import { IndeterminateError, InvalidInputError, NoHistoryError, NotFoundError, RateLimitedError, SchemaError } from '../../core/errors/index.js';
4
4
  import { languageStageStatus } from '../../core/status/index.js';
5
+ import { countryTimeZone, isoTime } from '../../core/time/index.js';
5
6
  import { clean } from '../../core/transport/index.js';
6
7
  import { isRecord } from '../../core/types.js';
7
8
  import { imileStage } from './status.js';
@@ -71,8 +72,7 @@ export function imileWording(raw) {
71
72
  }
72
73
  /**
73
74
  * A scan's clock: a wall clock to the second with no zone. The page says it
74
- * is the recipient country's time, and several of iMile's countries have
75
- * more than one zone, so it stays local.
75
+ * is the recipient country's time, which places it once that country is known.
76
76
  */
77
77
  function scanClock(row) {
78
78
  const stamp = text(row, 'time', 32);
@@ -160,15 +160,23 @@ export function parseImile(payload, raw) {
160
160
  const stage = current?.stage;
161
161
  const code = text(shipment, 'country', 8).toUpperCase();
162
162
  const country = Object.hasOwn(COUNTRIES, code) ? COUNTRIES[code] : undefined;
163
+ // Every scan shows the recipient country's clock: a country of one zone
164
+ // places them all, and one of several leaves them local.
165
+ const zone = country ? countryTimeZone(country) : null;
166
+ const placed = zone ? newestFirst.map(({ local_time, ...event }) => {
167
+ const time = isoTime(local_time, zone)?.iso;
168
+ return time ? { time, ...event } : { local_time, ...event };
169
+ }) : newestFirst;
170
+ const latest = placed[0];
163
171
  return {
164
172
  status: stage ? languageStageStatus(stage) : 'unknown',
165
173
  ...(stage ? { current_stage: stage, current_stage_source: current.stage_source } : {}),
166
174
  last_status_text: newest.description,
167
- last_update: null,
168
- last_update_local: newest.local_time,
175
+ last_update: latest.time ?? null,
176
+ ...(latest.time ? {} : { last_update_local: newest.local_time }),
169
177
  expected_delivery: null,
170
178
  ...(country ? { destination_country: country } : {}),
171
- events: newestFirst.slice(0, MAX_EVENTS),
179
+ events: placed.slice(0, MAX_EVENTS),
172
180
  ...(newestFirst.length > MAX_EVENTS ? { history_truncated: true } : {}),
173
181
  };
174
182
  }
@@ -45,6 +45,7 @@ const PHRASES = new Map([
45
45
  ['airplane clearance finish,ready to ship to', 'in_transit'],
46
46
  // The driver is named before the parcel leaves the centre; the misspelling is iMile's.
47
47
  ['your order has assigned delivery drvier', 'in_transit'],
48
+ ['dispatch task assigned to da', 'in_transit'],
48
49
  ['our delivery associate is out for delivery', 'out_for_delivery'],
49
50
  ['delivery attempt finished,return to local logistics company warehouse', 'failed_attempt'],
50
51
  // Drivers' reasons, each followed by the attempt's end and the return to the centre.
@@ -53,6 +54,9 @@ const PHRASES = new Map([
53
54
  ['route not completed', 'failed_attempt'],
54
55
  ['user not at home', 'failed_attempt'],
55
56
  ['wrong address', 'failed_attempt'],
57
+ ['<bad address>', 'failed_attempt'],
58
+ // A recipient iMile cannot reach, on the delivery run before the return starts.
59
+ ['need to return', 'exception'],
56
60
  ['lost order', null],
57
61
  ['inventory count scan', null],
58
62
  ['your order has been delivered successfully', 'delivered'],
@@ -66,6 +70,8 @@ const PHRASES = new Map([
66
70
  * flight and a scheduled delivery day are no movement.
67
71
  */
68
72
  const OPENINGS = [
73
+ // The origin airport's warehouse took the parcel in; the flight it books follows.
74
+ [/^origin port warehouse receive\b/, 'in_transit'],
69
75
  [/^flight schedule etd\b/, null],
70
76
  [/^shipment is scheduled to\b/, null],
71
77
  [/^the driver failed to deliver\b/, 'failed_attempt'],
@@ -38,6 +38,10 @@
38
38
  {"code": "1003", "wording": "wrong address", "stage": "failed_attempt", "language": "en", "confirmedBy": "live", "note": "A driver's reason, followed by the attempt's end."},
39
39
  {"code": "1003", "wording": "Lost order", "language": "en", "confirmedBy": "live", "unmapped": true, "note": "A driver's reason during a delivery run, followed by the attempt's end; the parcel it was seen on went out again and was delivered, so it does not establish a loss."},
40
40
  {"code": "1003", "wording": "Delivery attempt finished,return to Local logistics company warehouse.", "stage": "failed_attempt", "language": "en", "confirmedBy": "live", "note": "The parcel goes back to the station after an attempt."},
41
+ {"wording": "Origin port warehouse receive,Flight Schedule ETD【MIA】<2026-01-13 07:05:00>, ETA【BOG】<2026-01-13 09:50:00>", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "The origin airport's warehouse took the parcel in, with the flight booked for it; logged without a code."},
42
+ {"code": "1003", "wording": "【Sample DS】Dispatch task assigned to DA.", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "A delivery associate named before the run."},
43
+ {"code": "1003", "wording": "<Bad Address>", "stage": "failed_attempt", "language": "en", "confirmedBy": "live", "note": "A driver's reason during a delivery run; the parcel went out again after it."},
44
+ {"code": "1003", "wording": "【Contact Error】need to return.", "stage": "exception", "language": "en", "confirmedBy": "live", "note": "The recipient could not be reached; the return starts after it."},
41
45
  {"code": "2060", "wording": "Your order has been delivered successfully.", "stage": "delivered", "language": "en", "confirmedBy": "live", "note": "Some countries add who took the parcel, which the adapter drops."},
42
46
  {"code": "2060", "wording": "【DS CCA】Your package was received.", "stage": "delivered", "language": "en", "confirmedBy": "live", "note": "The Brazilian wording; the reply names who took the parcel and adds iMile's support contacts, which the adapter drops."},
43
47
  {"code": "1008", "wording": "Order delivery failed, returning to sender.", "stage": "returned", "language": "en", "confirmedBy": "live", "note": "Return Handling Process; the reply adds a support line, which the adapter drops."},
@@ -192,7 +192,10 @@ for (const language of ['en', 'de', 'fr', 'it', 'es', 'nl']) {
192
192
  for (const code of REGION_CODES) {
193
193
  const name = names.of(code);
194
194
  // German written without umlauts puts an e after the vowel: "Oesterreich".
195
- const spellings = language === 'de' ? [name, name.replace(/[äöüÄÖÜ]/g, (vowel) => `${vowel.normalize('NFD')[0]}e`)] : [name];
195
+ // English names Intl abbreviates are also written out: "Bosnia and
196
+ // Herzegovina", "Saint Lucia".
197
+ const spellings = language === 'de' ? [name, name.replace(/[äöüÄÖÜ]/g, (vowel) => `${vowel.normalize('NFD')[0]}e`)]
198
+ : language === 'en' ? [name, name.replace(/ & /g, ' and ').replace(/^St\. /, 'Saint ')] : [name];
196
199
  for (const key of spellings.map(regionKey)) {
197
200
  // A name a large town shares names the town as often as the country.
198
201
  if (!REGION_BY_NAME.has(key) && (language === 'en' || !townName(key)))
@@ -157,6 +157,27 @@ function providerWordingStage(description) {
157
157
  // A return on its way back to the sender, as other unfinished returns.
158
158
  if (/^the shipment is handled by a return consolidator[.!]?$/i.test(value))
159
159
  return 'exception';
160
+ // Relay's notices on Vinted parcels, from the label to the parcel in its hands.
161
+ if (/^awaiting your vinted parcel[.!]?$|^expecting your parcel[.!]?$/i.test(value))
162
+ return 'registered';
163
+ if (/^parcel received by relay[.!]?$|^received your parcel[.!]?$/i.test(value))
164
+ return 'accepted';
165
+ // SF Express's pickup notice, which goes on to name the courier.
166
+ if (/^sf express has received your package\b/i.test(value))
167
+ return 'accepted';
168
+ // iMile's scans as aggregators relay them, its 【】 place names cut or dropped.
169
+ // A wrong address sends the parcel out again; a recipient who cannot be
170
+ // reached closes the delivery and starts the return.
171
+ if (/^order submit+ed[.!]?$/i.test(value))
172
+ return 'registered';
173
+ if (/^we have received the shipment from the sender[.!]?$/i.test(value))
174
+ return 'accepted';
175
+ if (/^(?:\S+ )?cent(?:er|re) (?:received|parcel shipped to \S+)[.!]?$|^origin port warehouse receive\b|^the flight has taken off[.!]?$|^(?:\S+ )?dispatch task assigned to da[.!]?$/i.test(value))
176
+ return 'in_transit';
177
+ if (/^<bad address>$/i.test(value))
178
+ return 'failed_attempt';
179
+ if (/^contact error,? need to return[.!]?$|^start returning\b|^\S*devolucion\S* order closed[.!]?$/i.test(value))
180
+ return 'exception';
160
181
  if (/^warehouse receipt[.!]?$|^(?:package |parcel )?received by (?:the )?warehouse[.!]?$|^(?:package|parcel) (?:bulk )?received(?: and scanned)? at (?:the )?warehouse[.!]?$|^we['’]ve got it[.!]?$|^received at evri[.!]?$|^acceptance, sent to [a-z ]+$|^china post has received the mail\b|^envio recogido en origen[.!]?$|^first parcel scan along poste italiane['’]s logistic network\b|已收寄|^仓库收货$|^rfid confirmed pick-?up$|^usps in possession of item\b/i.test(value))
161
182
  return 'accepted';
162
183
  if (/^goods leave the operation cent(?:er|re)[.!]?$|^the shipment has left (?:the )?origin country[.!]?$|^(?:delivery\. )?your order has been shipped from (?:the distribution|\d\) [a-z ]{1,40}) cent(?:er|re)[.!]?$|^nuevo reparto(?:\. concertacion entrega flexible)?[.!]?$|^new delivery, flexible delivery arrangement[.!]?$|^inventory in a warehouse[.!]?$|^hand over to airline[.!]?$|^in lavorazione[.!]?$|^uscita centro postale[.!]?$|^przesyłka w oddziale \w{2,20}[.!]?$|^(?:package |parcel )?(?:left|leaves?) the warehouse[.!]?$|^we['’]re processing your parcel at our hub\b|^warehouse\. (?:inbound warehouse|delivery in last mile facilities|ready to hand over to last-mile supplier)[.!]?$|^hand over to last mile[.!]?$|^at destination country\/region[.!]?$|^(?:beladung|entladung)$|^entrance to the terminal from the driver$|^outbou?n?d from pickup terminal to the line ?h[ao]u?l$|^inbound from l?ine ?h[ao]u?l to (?:the )?terminal$|^[\w ]+ warehouse, .{1,80}? processing,? (?:order has been (?:in|out)bounded|assembling operation)$|^received in site[- ]|^exported[.!]?$|^(?:the )?plane has landed[.!]?$|^received by (?:the )?airline[.!]?$|^flight delay(?:ed)?[.!](?:$| the flight scheduled\b)|^wi\s*[–-]\s*intermediate warehouse\. (?:container received|package received at intermediate warehouse)[.!]?$|^delivery branch\. received at distribution branch[.!]?$|^receiving destination mail\. received by distributor[.!]?$|^container reception[.!]?$|(?:邮件|快件)(?:到达|离开)处理中心|航空公司接收|已交承运商运输|已完成分拣|^出口海关\/放行|^启运|^干线中转/i.test(value))
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.241.0"
5
+ "version": "0.241.1"
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.241.0",
3
+ "version": "0.241.1-main.1513",
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",