universal-parcel-scraper 0.55.0-main.706 → 0.55.0-main.708
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.
|
@@ -15,6 +15,8 @@ const PROVIDER = 'Swiss Post Cargo';
|
|
|
15
15
|
const ZONE = 'Europe/Zurich';
|
|
16
16
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
17
17
|
const MAX_RESPONSE_BYTES = 2_000_000;
|
|
18
|
+
/** The delivery photo and signature rows: proof of a delivery, not scans of their own. */
|
|
19
|
+
const DELIVERY_PROOF_CODES = new Set(['IMG', 'SIG']);
|
|
18
20
|
/**
|
|
19
21
|
* The endpoint sends Swiss wall-clock ISO-8601 without an offset
|
|
20
22
|
* (`2026-08-30T12:30:00.777`). It is read in Europe/Zurich, never in the
|
|
@@ -99,6 +101,7 @@ export function parseSwissPostCargoResponse(payload, rawTrackingNumber, now = Da
|
|
|
99
101
|
for (const shipment of shipments.slice(0, 100)) {
|
|
100
102
|
if (!Array.isArray(shipment.History))
|
|
101
103
|
continue;
|
|
104
|
+
const rows = [];
|
|
102
105
|
for (const candidate of shipment.History.slice(0, 500)) {
|
|
103
106
|
if (!isRecord(candidate))
|
|
104
107
|
continue;
|
|
@@ -106,13 +109,22 @@ export function parseSwissPostCargoResponse(payload, rawTrackingNumber, now = Da
|
|
|
106
109
|
const description = cleanScalar(candidate.Description);
|
|
107
110
|
if (!time || !description)
|
|
108
111
|
continue;
|
|
109
|
-
const location = cleanScalar(candidate.City, 120);
|
|
110
112
|
const code = cleanScalar(candidate.Status, 32).toLocaleUpperCase('en-US');
|
|
113
|
+
rows.push({
|
|
114
|
+
time, description, code, location: cleanScalar(candidate.City, 120),
|
|
115
|
+
classified: statusFor(code, description), proof: DELIVERY_PROOF_CODES.has(code),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
// A barcode's photo and signature are part of its delivery scan: folded
|
|
119
|
+
// into it, they never stand on top of it.
|
|
120
|
+
const deliveryScan = rows.some((row) => !row.proof && row.classified.stage === 'delivered');
|
|
121
|
+
for (const { time, description, code, location, classified, proof } of rows) {
|
|
122
|
+
if (proof && deliveryScan)
|
|
123
|
+
continue;
|
|
111
124
|
const identity = JSON.stringify([time.iso, location, description, code]);
|
|
112
125
|
if (seen.has(identity))
|
|
113
126
|
continue;
|
|
114
127
|
seen.add(identity);
|
|
115
|
-
const classified = statusFor(code, description);
|
|
116
128
|
parsedEvents.push({
|
|
117
129
|
event: {
|
|
118
130
|
time: time.iso,
|
|
@@ -123,12 +135,16 @@ export function parseSwissPostCargoResponse(payload, rawTrackingNumber, now = Da
|
|
|
123
135
|
},
|
|
124
136
|
status: classified.status,
|
|
125
137
|
timestamp: time.timestamp,
|
|
138
|
+
proof,
|
|
126
139
|
index,
|
|
127
140
|
});
|
|
128
141
|
index += 1;
|
|
129
142
|
}
|
|
130
143
|
}
|
|
131
|
-
|
|
144
|
+
// Ties keep the carrier's order, except that a photo or signature never
|
|
145
|
+
// passes a scan made at the same instant.
|
|
146
|
+
parsedEvents.sort((left, right) => right.timestamp - left.timestamp
|
|
147
|
+
|| Number(left.proof) - Number(right.proof) || left.index - right.index);
|
|
132
148
|
const events = parsedEvents.slice(0, 100);
|
|
133
149
|
if (events.length === 0) {
|
|
134
150
|
throw new SchemaError(PROVIDER, 'Swiss Post Cargo returned no usable tracking events');
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
{ "code": "DLV", "stage": "delivered", "confirmedBy": "fixture", "firstSeen": "2026-08-30", "note": "Delivery scan; appears with the wording 'Delivered'." },
|
|
5
5
|
{ "code": "POD", "wording": "DELIVERED SCANNED", "language": "en", "stage": "delivered", "confirmedBy": "live", "firstSeen": "2026-08-30", "note": "Proof-of-delivery scan; seen live with the delivery wording." },
|
|
6
6
|
{ "code": "P40", "stage": "delivered", "confirmedBy": "prior-art", "firstSeen": "2026-08-30", "note": "Proof-of-delivery family carried over from the adapter's original map; not re-confirmed against a capture." },
|
|
7
|
-
{ "code": "IMG", "wording": "IMAGE", "language": "en", "stage": "delivered", "confirmedBy": "live", "firstSeen": "2026-08-30", "note": "A delivery picture: live histories show it just before the delivery scan.
|
|
8
|
-
{ "code": "SIG", "wording": "SIGNATURE", "language": "en", "stage": "delivered", "confirmedBy": "live", "firstSeen": "2026-08-30", "note": "Signed for, at the instant of the delivery scan; the signature itself is never retained." },
|
|
7
|
+
{ "code": "IMG", "wording": "IMAGE", "language": "en", "stage": "delivered", "confirmedBy": "live", "firstSeen": "2026-08-30", "note": "A delivery picture: live histories show it at or just before the delivery scan's instant, listed ahead of it. Folded into the barcode's delivery scan; the picture itself is never retained." },
|
|
8
|
+
{ "code": "SIG", "wording": "SIGNATURE", "language": "en", "stage": "delivered", "confirmedBy": "live", "firstSeen": "2026-08-30", "note": "Signed for, at the instant of the delivery scan. Folded into the barcode's delivery scan; the signature itself is never retained." },
|
|
9
9
|
{ "code": "SCA", "wording": "Loaded for Delivery", "language": "en", "stage": "out_for_delivery", "confirmedBy": "live", "firstSeen": "2026-08-30", "note": "Delivery-round scan." },
|
|
10
10
|
{ "code": "NTF", "wording": "Data entry POST", "language": "en", "stage": "registered", "confirmedBy": "live", "firstSeen": "2026-08-30", "note": "Shipment announced." },
|
|
11
11
|
{ "code": "RFS", "wording": "Received warehouse POST", "language": "en", "stage": "accepted", "confirmedBy": "live", "firstSeen": "2026-08-30", "note": "Accompanies the wording 'Shipment accepted'." },
|
|
@@ -107,6 +107,32 @@ const HOLD = String.raw `\b(?:on hold|held|hold|holding|retenu(?:e|s|es)?|en att
|
|
|
107
107
|
const RECIPIENT_HOLD = new RegExp(`${HOLD}${SENTENCE}*?${RECIPIENT_REQUEST}|${RECIPIENT_REQUEST}${SENTENCE}*?${HOLD}`);
|
|
108
108
|
/** Where a held parcel waits for the recipient to collect it. */
|
|
109
109
|
const COUNTER = /post office|postal outlet|pick ?up|collection|counter|retail (?:location|outlet)|bureau de poste|point (?:de )?retrait|point relais|filiale|abholung|ufficio postale|ritiro|oficina|recogida|levantamento|agencia/;
|
|
110
|
+
/** The recipient's side by its role, not a service desk or an agent acting for it. */
|
|
111
|
+
const RECIPIENT = String.raw `(?:recipient|addressee|consignee|customer|receiver)s?\b(?!'| (?:service|care|support)\b)`;
|
|
112
|
+
/**
|
|
113
|
+
* Where only a recipient collects a parcel. Parcel shops, service points and
|
|
114
|
+
* access points also take in the drop-offs a courier then collects ("Picked
|
|
115
|
+
* up at parcelshop", "collected from the ServicePoint"), so a collection there
|
|
116
|
+
* has to name the recipient.
|
|
117
|
+
*/
|
|
118
|
+
const COLLECTION_PLACE = String.raw `(?:pick ?up (?:point|station|parcel ?shop|locker)|collection point|(?:parcel )?lockers?|packstation|(?:post office |postal )?counter)\b`;
|
|
119
|
+
const COLLECTED = String.raw `(?:picked up|collected)`;
|
|
120
|
+
/**
|
|
121
|
+
* A parcel collected from its pickup point: by the recipient, the consignee or
|
|
122
|
+
* the customer, or from a pickup point, locker or counter, in each language.
|
|
123
|
+
*/
|
|
124
|
+
const RECIPIENT_COLLECTED = new RegExp([
|
|
125
|
+
String.raw `\b${COLLECTED}(?: [\w'-]+){0,6}? by (?:the |a |an |its |your )?${RECIPIENT}|\b${RECIPIENT} (?:has |have )?(?:already )?${COLLECTED}\b|\b${COLLECTED} (?:from|at|in) (?:the |a |an |its |your |our )?(?:[\w-]+ ){0,2}?${COLLECTION_PLACE}`,
|
|
126
|
+
String.raw `\bretire(?:e|s|es)?\b${SENTENCE}*? par (?:le |la |l'|son |sa )?(?:destinataire|cliente?)\b|\b(?:destinataire|cliente?) a retire\b|\bretire(?:e|s|es)? (?:au |en |dans (?:le |la |l'|un |une |votre |son )|a la |a l')(?:point (?:de )?retrait|point relais|relais|consigne|locker|bureau de poste)\b`,
|
|
127
|
+
String.raw `\b(?:vom|von (?:der|dem)|durch (?:den|die)) (?:empfanger(?:in)?|kunden|kundin|adressaten|adressatin)\b${SENTENCE}*?\babgeholt\b|\b(?:empfanger(?:in)?|kunde|kundin) hat\b${SENTENCE}*?\babgeholt\b|\b(?:in|aus) (?:der|dem|einer|einem|ihrer|ihrem) (?:[\w-]+ )?(?:filiale|postfiliale|packstation|paketstation|abholstation|postamt)\b${SENTENCE}*?\babgeholt\b`,
|
|
128
|
+
String.raw `\britirat[oaie]\b${SENTENCE}*? dal(?:la)? (?:destinatari[oa]|cliente)\b|\b(?:destinatari[oa]|cliente) ha ritirato\b|\britirat[oaie] (?:presso|in|al|nel|nella) (?:il |la |lo |l')?(?:punto (?:di )?ritiro|locker|ufficio postale)\b`,
|
|
129
|
+
String.raw `\b(?:recogid|retirad)[oa]s? por (?:el |la )?destinatari[oa]\b|\bdestinatari[oa] (?:ya )?ha (?:recogido|retirado)\b|\b(?:levantad|retirad)[oa]s? pel[oa] destinatari[oa]\b|\bodebran[aeoy] przez (?:odbiorce|adresata)\b`,
|
|
130
|
+
].join('|'));
|
|
131
|
+
const COLLECT_VERB = String.raw `(?:picked up|collected|retire|abgeholt|ritirat|recogid|retirad|levantad|odebran)`;
|
|
132
|
+
/** A collection negated, still to come or made a condition, in each language. */
|
|
133
|
+
const COLLECTION_PENDING = new RegExp(String.raw `(?:\b(?:not|never|nothing|non|pas|jamais|nicht|nao|nie)\b|n't)${SENTENCE}*?${COLLECT_VERB}|\bno (?:(?:ha|han|fue|sido|se) )*(?:recogid|retirad)|\b(?:be|being|once|when|until|till|if|unless|before|etre|sera|seront|serait|essere|sara|saranno|verra|verranno)(?: [\w']+){0,3}? ${COLLECT_VERB}|\babgeholt (?:werden|wird)\b|\b(?:wird|werden|kann|konnen|muss|soll)\b${SENTENCE}*?\babgeholt\b`);
|
|
134
|
+
/** A courier, a driver or the sender's side collecting it, in each language. */
|
|
135
|
+
const COLLECTED_BY_CARRIER = new RegExp(String.raw `\b(?:by|from|par|de|du|vom|von|beim|dal|dalla|dallo|por|del|pelo|przez) (?:the |a |an |our |your |le |la |l'|den |der |dem |il |lo |el |o )?(?:[\w-]+ )?(?:couriers?|drivers?|carriers?|senders?|shippers?|sellers?|merchants?|consignors?|chauffeur|livreur|transporteur|coursier|expediteur|\w*(?:fahrer|kurier|zusteller|boten?)|absender|corriere|autista|vettore|mittente|repartidor|mensajero|transportista|remitente|motorista|estafeta|remetente|kuriera|nadawcy)\b(?!')`);
|
|
110
136
|
/**
|
|
111
137
|
* A shipment, order or label cancelled, and nothing else in the scan, in each
|
|
112
138
|
* language and Turkish ("İptal Edildi"). A cancelled return, pickup or hold,
|
|
@@ -193,6 +219,11 @@ export function trackingLanguageStage(description) {
|
|
|
193
219
|
// admisión", a parcel the sender dropped in a CityPaq locker.
|
|
194
220
|
if (/\bpara (?:su )?(?:admision|admissao)\b|\b(?:deposited|dropped off)\b(?: [\w']+){0,8}? for (?:admission|acceptance)\b/.test(text))
|
|
195
221
|
return 'registered';
|
|
222
|
+
// The recipient collecting the parcel ends the delivery, ahead of the drop-off
|
|
223
|
+
// at the pickup point the scan may also name; the courier's or the sender's
|
|
224
|
+
// collection is an acceptance below.
|
|
225
|
+
if (RECIPIENT_COLLECTED.test(text) && !COLLECTION_PENDING.test(text) && !COLLECTED_BY_CARRIER.test(text))
|
|
226
|
+
return 'delivered';
|
|
196
227
|
// "Delivered to" a pickup point or an intermediary is not delivery to the
|
|
197
228
|
// recipient; a parcel already waiting at its pickup point outranks the
|
|
198
229
|
// "sera remis" (will be handed over) that follows in La Poste's sentence.
|
|
@@ -266,7 +297,7 @@ export function trackingLanguageStage(description) {
|
|
|
266
297
|
// A delivery still to come ("una vez entregado", "que se entregue", "quando
|
|
267
298
|
// for entregue") is not one; Spanish "entregue" is also a subjunctive.
|
|
268
299
|
if (!/\b(?:una vez|cuando|hasta que|antes de que|para que|en cuanto|tan pronto como|uma vez|quando|assim que|ate que|logo que)\b[^.;:]*\bentreg/.test(text)
|
|
269
|
-
&& /(?<!\b(?:sea|sean|fuera|fuese|ser|seja|sejam|fosse|for|forem|que|se) )\b(?:entregad[oa]s?|entregues?)\b|\bentrega (?:realizada|efectuada|efetuada|completada|concluida|finalizada)\b
|
|
300
|
+
&& /(?<!\b(?:sea|sean|fuera|fuese|ser|seja|sejam|fosse|for|forem|que|se) )\b(?:entregad[oa]s?|entregues?)\b|\bentrega (?:realizada|efectuada|efetuada|completada|concluida|finalizada)\b/.test(text))
|
|
270
301
|
return 'delivered';
|
|
271
302
|
// A handover to another network, a carrier or a courier set apart, takes the parcel on.
|
|
272
303
|
if (HANDED_TO_NETWORK.test(text))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-parcel-scraper",
|
|
3
|
-
"version": "0.55.0-main.
|
|
3
|
+
"version": "0.55.0-main.708",
|
|
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",
|