universal-parcel-scraper 0.1007.1-main.5444 → 0.1007.2-main.5446

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.
@@ -4,7 +4,7 @@ import { isValidS10TrackingNumber } from '../../core/detection/index.js';
4
4
  import { countryTimeZone, explicitOffsetTime, isoTime } from '../../core/time/index.js';
5
5
  import { clean, decodeText, escapeRegExp, fetchBounded, parseJsonBytes, } from '../../core/transport/index.js';
6
6
  import { isRecord } from '../../core/types.js';
7
- import { agencyIntake, cttSiteZone, isCttExpressPlace, portugalZone } from './clock.js';
7
+ import { agencyIntake, cttSiteZone, isCttExpressPlace, officeCountry, portugalZone } from './clock.js';
8
8
  import { classifyCttStatus } from './status.js';
9
9
  // Protocol provenance:
10
10
  // - Prior art (full session mechanics + vocabulary, verified step by step
@@ -86,6 +86,9 @@ function sessionFromSetCookie(response) {
86
86
  const DESTINATION_EVENT = /^(?:EM[D-I]|ED[A-H])$/;
87
87
  /** UPU events an origin post records: from posting to dispatch from its office of exchange. */
88
88
  const ORIGIN_EVENT = /^(?:EM[A-C]|EX[A-DX])$/;
89
+ /** Stages a scan from the return's arrival back at the origin takes as part of the trip back. */
90
+ const RETURN_TRIP = new Set(['accepted', 'in_transit', 'out_for_delivery', 'ready_for_pickup', 'delivered']);
91
+ const RETURNED = { status: 'exception', stage: 'returned' };
89
92
  const WALL_CLOCK = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.\d{1,3})?(?:Z|\+00:?00)?$/i;
90
93
  /** A scan's wall clock read as UTC, only to compare it with another's. */
91
94
  function wallMillis(value) {
@@ -96,20 +99,22 @@ function wallMillis(value) {
96
99
  * A scan's clock. CTT's Z is no offset: the digits are the wall clock where
97
100
  * the scan happened (see clock.ts). That is the partner post's country
98
101
  * (`partner`) for its scans: the destination's on a parcel bound abroad, the
99
- * origin's on mail from abroad. A location naming an office or a site of
100
- * CTT's overrides it; Portugal's clock applies otherwise. In a country with
102
+ * origin's on mail from abroad. A location naming an office of exchange in
103
+ * Portugal or one of the parcel's `countries`, or a site of CTT's, overrides
104
+ * it: a parcel returned to its origin arrives at the origin's office. Portugal's
105
+ * clock applies otherwise. In a country with
101
106
  * several zones the scan keeps CTT's Z, the last guess: hours off, but the
102
107
  * reply keeps its instant. Only a wall clock sent with no Z stays a wall time
103
108
  * there. Any other offset is taken as sent.
104
109
  */
105
- function scanClock(value, location, partner, spanish) {
110
+ function scanClock(value, location, partner, countries, spanish) {
106
111
  const raw = typeof value === 'string' ? value.trim() : '';
107
112
  const wall = WALL_CLOCK.exec(raw)?.[1];
108
113
  if (!wall) {
109
114
  const time = explicitOffsetTime(raw);
110
115
  return time ? { time: time.iso, timestamp: time.timestamp } : null;
111
116
  }
112
- const zone = cttSiteZone(location, partner ? [partner] : []) ?? (spanish ? 'Europe/Madrid' : null)
117
+ const zone = cttSiteZone(location, countries) ?? (spanish ? 'Europe/Madrid' : null)
113
118
  ?? (partner ? countryTimeZone(partner) : portugalZone(location));
114
119
  const placed = zone ? isoTime(wall, zone) : null;
115
120
  if (placed)
@@ -161,7 +166,7 @@ export function parseCttTrackingResponse(payload, trackingNumber) {
161
166
  const partner = abroad && DESTINATION_EVENT.test(code) ? abroad : origin && ORIGIN_EVENT.test(code) ? origin : null;
162
167
  const at = wallMillis(rawEvent.DateTime);
163
168
  const spanish = !location && agencyIntake(at, agencyScans);
164
- const clock = scanClock(rawEvent.DateTime, location, partner, spanish);
169
+ const clock = scanClock(rawEvent.DateTime, location, partner, [abroad, origin], spanish);
165
170
  if (!clock || !description)
166
171
  return;
167
172
  const identity = `${clock.time ?? clock.local_time}\u0000${description}\u0000${Number.isSafeInteger(stateId) ? stateId : ''}`;
@@ -187,9 +192,21 @@ export function parseCttTrackingResponse(payload, trackingNumber) {
187
192
  classified: classified ?? undefined,
188
193
  timestamp: clock.timestamp,
189
194
  index,
195
+ // Mail from abroad arriving at an office of exchange of the country it
196
+ // came from is a return back at its origin.
197
+ back: Boolean(origin && !abroad && DESTINATION_EVENT.test(code) && officeCountry(location) === origin),
190
198
  });
191
199
  });
192
200
  parsed.sort((left, right) => right.timestamp - left.timestamp || left.index - right.index);
201
+ // From that arrival on, the scans are the trip back to the sender: a movement
202
+ // or a delivery among them is the return, not a delivery to the recipient.
203
+ const arrivedBack = parsed.map((item) => item.back).lastIndexOf(true);
204
+ for (const item of parsed.slice(0, arrivedBack + 1)) {
205
+ const trip = RETURN_TRIP.has(item.classified?.stage ?? '');
206
+ if (trip)
207
+ item.classified = RETURNED;
208
+ item.event = { ...item.event, ...(trip ? { stage: 'returned' } : {}), provider_leg: 'return' };
209
+ }
193
210
  const events = parsed.slice(0, MAX_EVENTS_TO_RETURN).map(({ event }) => event);
194
211
  const latest = parsed.find((item) => item.classified);
195
212
  if (!latest) {
@@ -17,6 +17,8 @@ export declare function isCttExpressPlace(location: string): boolean;
17
17
  * read as UTC, only to compare them. It is on Madrid's clock, as the agency's scan is.
18
18
  */
19
19
  export declare function agencyIntake(at: number, agencyScans: readonly number[]): boolean;
20
+ /** The country of the office of exchange leading a location, if one does. */
21
+ export declare function officeCountry(location: string): string | null;
20
22
  /**
21
23
  * The clock a CTT scan's location itself names: the country of the office of
22
24
  * exchange leading it, when that is Portugal or one of `countries` (the
@@ -88,6 +88,10 @@ const AGENCY_INTAKE_MS = 60_000;
88
88
  export function agencyIntake(at, agencyScans) {
89
89
  return agencyScans.some((agency) => agency - at >= 0 && agency - at <= AGENCY_INTAKE_MS);
90
90
  }
91
+ /** The country of the office of exchange leading a location, if one does. */
92
+ export function officeCountry(location) {
93
+ return OFFICE.exec(location)?.[1] ?? null;
94
+ }
91
95
  /**
92
96
  * The clock a CTT scan's location itself names: the country of the office of
93
97
  * exchange leading it, when that is Portugal or one of `countries` (the
@@ -97,7 +101,7 @@ export function agencyIntake(at, agencyScans) {
97
101
  * the same clocks.
98
102
  */
99
103
  export function cttSiteZone(location, countries) {
100
- const office = OFFICE.exec(location)?.[1];
104
+ const office = officeCountry(location);
101
105
  if (office === 'PT' || (office && countries.includes(office))) {
102
106
  return office === 'PT' ? portugalZone(location) : countryTimeZone(office);
103
107
  }
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.1007.1-main.5444"
5
+ "version": "0.1007.2-main.5446"
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.1007.1-main.5444",
3
+ "version": "0.1007.2-main.5446",
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",