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
|
|
100
|
-
*
|
|
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,
|
|
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 =
|
|
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
|
}
|
package/dist/server/openapi.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-parcel-scraper",
|
|
3
|
-
"version": "0.1007.
|
|
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",
|