universal-parcel-scraper 0.5.0-main.321 → 0.5.0-main.323
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 +1 -1
- package/dist/browser/scraper.js +3 -1
- package/dist/carriers/dpd/adapter.d.ts +1 -1
- package/dist/carriers/dpd/adapter.js +8 -3
- package/dist/carriers/india-post/status.js +1 -0
- package/dist/carriers/india-post/statuses.json +2 -1
- package/dist/carriers/la-poste/status.js +4 -0
- package/dist/carriers/la-poste/statuses.json +9 -0
- package/dist/carriers/posti/status.js +5 -1
- package/dist/carriers/posti/statuses.json +9 -1
- package/dist/carriers/spring-gds/status.d.ts +2 -2
- package/dist/carriers/spring-gds/status.js +8 -0
- package/dist/carriers/spring-gds/statuses.json +24 -0
- package/dist/carriers/tnt/status.js +1 -0
- package/dist/carriers/tnt/statuses.json +8 -0
- package/dist/core/status/language.js +7 -1
- package/dist/providers/shared/result.js +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -149,7 +149,7 @@ createTracker({ providers: ['ParcelsApp', 'Ship24', '17TRACK', 'Postal Ninja', '
|
|
|
149
149
|
<!-- GENERATED:stages -->
|
|
150
150
|
<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">
|
|
151
151
|
|
|
152
|
-
The carrier folders record 1,
|
|
152
|
+
The carrier folders record 1,689 such statuses, each filed under one stage.
|
|
153
153
|
<!-- /GENERATED:stages -->
|
|
154
154
|
|
|
155
155
|
Wording nobody recorded yet goes through a classifier that reads seven European languages.
|
package/dist/browser/scraper.js
CHANGED
|
@@ -3526,10 +3526,12 @@ function st(e) {
|
|
|
3526
3526
|
if (/ready for (?:pickup|collection)|ready to collect|available for (?:pickup|collection)|awaiting (?:customer |recipient |your )?(?:pickup|collection)|pret.*(?:retrait|retire)|disponible.*(?:retrait|collecte)|vous attend (?:au|en|dans|chez|a)\b|mise? a disposition (?:au|en|dans|a)\b.*(?:point (?:de )?retrait|relais|consigne|locker|bureau de poste|agence)|jours? restants? pour (?:le )?retirer|abholbereit|zur abholung bereit|pront[oa] per il ritiro|disponibile per il ritiro|(?:deposited|depose|hinterlegt|depositat[oa]).*mypost24/.test(t) || /(?<!\b(?:no|nao|ya no|ja nao) (?:(?:esta|estara|se encuentra|se encontra|e|era|foi) )?)\bdisponi(?:ble|vel) (?:para (?:su |ser )?(?:recoger|recogida|recogid[oa]|retirar|retiro|retirada|retirad[oa]|levantamento|levantar|levantad[oa]|recolha|recolher)|(?:en|no|na|num|numa|em) (?:el |la |un |una |su |tu |nuestr[oa] )?(?:punto|oficina|tienda|locker|taquilla|consigna|citypaq|parcel ?shop|buzon inteligente|agencia|delegacion|sucursal|ponto|loja|cacifo|estacao|balcao|payshop|posto|unidade))\b|\blist[oa] para (?:su |ser )?(?:recoger|recogida|recogid[oa]|retirar|retiro)\b|\bpront[oa] (?:para|a) (?:ser )?(?:levantamento|levantar|levantad[oa]|recolha|recolher|retirada|retirar)\b|\b(?:ya )?(?:puede|puedes|podra|podras) (?:pasar a )?recoger(?:lo|la)?\b|\b(?:aguarda|aguardando|a aguardar) (?:o |a )?(?:levantamento|retirada)\b|\besperando (?:su|tu) recogida\b|\b(?:pendiente de|esperando|a la espera de) (?:ser )?recogid[oa] por (?:el |la )?destinatari[oa]\b/.test(t)) return "ready_for_pickup";
|
|
3527
3527
|
if (/label (?:created|printed)|(?:created|generated) a (?:new )?(?:shipment |shipping )?label|etiquette.*(?:cree|imprime)|cree une etiquette|versandetikett.*(?:erstellt|gedruckt)|etikett erstellt|etichetta.*(?:creata|stampata)|creato un'etichetta|elektronisch|electroni(?:c|que|sch)|elettronic|pre.?advi[cs]|preannunciat|vorangemeldet|preannonce|data.*entered|donnees.*saisies|daten.*erfasst|dati.*inseriti/.test(t) || /shipment information (?:received|sent to)|\bdata received\b|(?:package|parcel|shipment) data (?:was |has been )?sent to|\bpre ?notification\b|informations d'expedition recues|sendungsinformationen erhalten|informazioni.*spedizione ricevute|(?:consignment|shipment|parcel|package) recorded by|envoi enregistre par|sendung.*absender.*erfasst|spedizione registrata dal/.test(t) || /preparation chez.*expediteur|preparation.*expediteur|preparazione.*mittente|being prepared.*sender|(?:shipper|sender)(?: that)? (?:they are|is|are) preparing|beim absender.*vorbereitet|warehouse of the sender|shippers warehouse|entrepot de l'expediteur|lager des absenders|magazzino del mittente|demande d'envoi.*prise en compte|collection request.*(?:received|recorded)|abholauftrag.*erfasst|richiesta.*ritiro.*registrata/.test(t) || /^(?:reported|recorded|announced|item created|enregistre|annonce|erfasst|angekundigt|registrato|annunciato)$/.test(t)) return "registered";
|
|
3528
3528
|
if (/attend d'etre libere|attente de liberation|wartet auf.*freigabe|attesa di svincolo|(?:customs|clearance).*\bnot\b.*(?:clear|complet|releas)|dedouanement.*pas termine|customs (?:not cleared|clearance not completed)|not.*released by customs|dedouanement.*(?:non termine|pas termine)|(?:\bnot|n't|\bnever)(?: yet)?(?: been)? cleared (?:through |by )?customs|\bbe(?:ing)? cleared (?:through |by )?customs|non dedouane|zollabfertigung.*nicht abgeschlossen|nicht.*zoll.*freigegeben|sdoganamento.*non completato|non sdoganat/.test(t)) return "customs";
|
|
3529
|
-
if (/clearance.*(?:has been completed|completed)|clearance (?:complete|success)|(?:leaving|departed from|left) customs|completion of customs|formalites.*(?:terminee|achevee)|released by (?:customs|a government agency)|customs (?:cleared|released)|\bcleared (?:through |by )?customs|dedouanement.*(?:termine|acheve)|fin du dedouanement|libere.*(?:douane|autorite)|douane.*libere|zollabfertigung.*abgeschlossen|(?:zoll|behorde).*freigegeben|sdoganamento.*completato|completamento.*sdoganamento|svincolat.*(?:dogana|autorita)/.test(t)) return "in_transit";
|
|
3529
|
+
if (/clearance.*(?:has been completed|completed)|clearance (?:complete|success)|(?:leaving|departed from|left) customs|completion of customs|formalites.*(?:terminee|achevee)|released by (?:customs|a government agency)|customs (?:cleared|(?:has |have )?released)|\bcleared (?:through |by )?customs|dedouanement.*(?:termine|acheve)|fin du dedouanement|libere.*(?:douane|autorite)|douane.*libere|zollabfertigung.*abgeschlossen|(?:zoll|behorde).*freigegeben|sdoganamento.*completato|completamento.*sdoganamento|svincolat.*(?:dogana|autorita)/.test(t)) return "in_transit";
|
|
3530
3530
|
if (/customs|clearance|douan|formalites (?:d')?(?:import|export)|zoll|dogan|government agency|autorite gouvernementale|staatliche behorde|autorita governativa/.test(t)) return "customs";
|
|
3531
3531
|
if (/out for (?:physical )?delivery|being delivered|in delivery|(?:loading|loaded).*delivery vehicle|on (?:\w+ )?vehicle for delivery|(?:courier|driver|delivery champion) has (?:the|your) (?:shipment|parcel|package|item)|en cours de livraison|en livraison|de la livraison de (?:son|votre) colis ce jour|charg(?:e|ement).*vehicule de livraison|in zustellung|zustellfahrzeug.*(?:geladen|verladen)|(?:beladen|verladung).*zustellfahrzeug|in consegna|caric(?:at|amento).*veicolo.*consegna/.test(t) || /^(?:en )?reparto$|\ben reparto\b|\b(?:siendo|sendo|a ser) (?:entregad[oa]s?|entregues?|repartid[oa]s?|distribuid[oa]s?)\b|\b(?:salida|salio|sale|ha salido) (?:a|para) (?:el )?reparto\b|\bruta de (?:reparto|entrega)\b|\ben proceso de entrega\b|\bvehiculo de reparto\b|\b(?:repartidor|mensajero|cartero|conductor) (?:ya )?(?:tiene|lleva) (?:tu|su|el|la) (?:paquete|envio|pedido|encomienda)\b|\bem distribuicao\b|\bsaiu para (?:a )?(?:entrega|distribuicao)\b|\bem (?:rota|processo|curso) de entrega\b|\bem entrega\b|\bveiculo de (?:entrega|distribuicao)\b|\b(?:carteiro|estafeta|motorista|entregador) (?:ja )?(?:tem|leva) (?:a|o) (?:sua |tua )?(?:encomenda|envio|objeto|pacote|volume)\b/.test(t)) return "out_for_delivery";
|
|
3532
3532
|
if (/\bdelivered\b|^final delivery$|delivery complete(?:d)?\b|delivery (?:was )?successful|(?:a ete|est) distribue|^(?:livre|livree)(?:$|[ ,])|(?:colis|envoi|est|a ete) livre|zugestellt|\bconsegnat[oa]\b|livraison effectuee|consegna completata/.test(t) || !/\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(t) && /(?<!\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|\brecogid[oa] por (?:el |la )?destinatari[oa]\b|\b(?:levantad|retirad)[oa] pelo destinatario\b/.test(t)) return "delivered";
|
|
3533
|
+
if (/^(?:the |your )?(?:item|package|parcel|shipment) (?:has been |was |is )?(?:received for transport|dropped off by (?:the )?sender at (?:our |the )?postal partner)\b/.test(t)) return "accepted";
|
|
3534
|
+
if (/^(?:load vehicle|scan ok gateway)$/.test(t) || /^(?:the )?item in process in office of exchange\b/.test(t)) return "in_transit";
|
|
3533
3535
|
if (/^(?:posting\/collection|posted|origin scan)$|received at (?:our |the )?(?:[a-z]+ )?origin (?:office|facility|hub)|dropped off (?:after|before) (?:the )?latest drop ?off time/.test(t) || /accepted|picked up|pick up was successful|^drop off$|pick ?up scan|prepared the package for return to ups or pickup by ups|handed (?:over )?to (?:dpd|gls)|package received at dhl|item booked|consignment was mailed|posted at a postal point|pris en charge|prise en charge|collecte.*(?:reussi|effectue)|recupere.*boite aux lettres|depose.*point postal|remis a (?:dpd|gls)|envoi.*depose|abholung.*erfolgreich|eingeliefert|an (?:dpd|gls) ubergeben|paket.*(?:angenommen|ubernommen)|ritiro.*(?:riuscito|effettuato)|pres[oa] in carico|affidat[oa] a (?:dpd|gls)|depositat.*punto postale|spedizione.*impostata/.test(t)) return "accepted";
|
|
3534
3536
|
if (/transit|en route|on (?:its|the) way|\bfacility\b|airport|\bleaving\b|\bdispatched\b|^delay(?:ed)?$|received by (?:the )?(?:local )?(?:delivery|logistics) company|received by postnl|notification.*sent to the recipient|delivery option requested|instruction de livraison recue|expedie (?:depuis|du|de)\b|acheminement|unterwegs|transport|trasport|arriv|depart|processed|processing completed|sorted|sorting|\btri\b|trie|traitement|traite|sortier|bearbeitet|bearbeitung|elaborat|elaborazion|smistat|parcel cent(?:er|re)|delivery cent(?:er|re)|centre.*(?:colis|distribution)|paketzentrum|verteilzentrum|centro.*(?:pacchi|distribuzione)|depot|import scan|scan.*import|importscan|scansione.*import|delivery.*delayed|livraison.*retard|zustellung.*verzogert|consegna.*ritard|transferred|transfere|weitergeleitet|inoltrato|left.*(?:point|center)|quitte|verlassen|raggiunto|close bag|fermeture du sac|sack.*geschlossen|chiusura.*sacco|scanned into|scanne dans|eingescannt|scansionat|\bloaded\b|^(?:item|parcel|shipment) distributed$|charg.*vehicule de transport|transportfahrzeug.*geladen|caric.*veicolo.*trasporto/.test(t) || /^enviad[oa]$|\bclasificad[oa]s?\b|\bclasificacion\b|\ben ruta\b|\ben camino\b|\bllegad[oa]s? (?:a|al|en)\b|\bllegada (?:a|al|en|de)\b|\bha llegado\b|\bllego (?:a|al)\b|\bsalida (?:de|del|desde)\b|\b(?:ha )?salido (?:de|del)\b|\bsalio (?:de|del)\b|\ben destino\b|\b(?:agencia|delegacion|plataforma|oficina|franquicia) (?:de )?destino\b|\b(?:unidad|centro|oficina|delegacion|base|agencia|plataforma) de reparto\b|\bcentro (?:de )?(?:clasificacion|distribucion|tratamiento|logistico|operaciones|operativo)\b|\blectura\b|\bleid[oa]s?\b|\brecepcionad[oa]s?\b|\bprocesad[oa]s?\b|\bexpedid[oa]s?\b|\btriagem\b|\btriad[oa]s?\b|\bem tratamento\b|\btratad[oa]s?\b|\bcentro (?:de )?(?:distribuicao|tratamento|triagem|operacional)\b|\bchegad[ao] (?:a|ao|no|na|em)\b|\bchegou (?:a|ao|no|na|em)\b|\bsaida (?:do|da|de|dos|das)\b|\bsaiu (?:do|da|de|dos|das)\b|\bem rota\b|\ba caminho\b|\bencaminhad[oa]s?\b|\b(?:em )?transferencia\b|\btransferid[oa]s?\b|\bprocessad[oa]s?\b|\brecebid[oa] (?:no|na|em) (?:centro|armazem|unidade|plataforma)\b|\brecibid[oa] en (?:el |la |nuestr[oa] )?(?:almacen|centro|delegacion|plataforma|hub|agencia)\b/.test(t)) return "in_transit";
|
|
3535
3537
|
}
|
|
@@ -10,7 +10,7 @@ export declare class DPDChallengeError extends ChallengeError {
|
|
|
10
10
|
/**
|
|
11
11
|
* The guest API answered, but the answer proves nothing about the shipment
|
|
12
12
|
* (unreachable, malformed, unauthenticated, or an HTTP status the guest flow
|
|
13
|
-
* handles itself). The rendered page is allowed to recover from it.
|
|
13
|
+
* handles itself, except HTTP 503). The rendered page is allowed to recover from it.
|
|
14
14
|
*/
|
|
15
15
|
export declare class DPDAPIError extends IndeterminateError {
|
|
16
16
|
constructor(message: string, options?: CarrierErrorOptions);
|
|
@@ -2,7 +2,7 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { load } from 'cheerio';
|
|
4
4
|
import { lookupBudget } from '../../core/adapter/index.js';
|
|
5
|
-
import { BudgetExceededError, carrierErrorKind, ChallengeError, IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
|
|
5
|
+
import { BudgetExceededError, carrierErrorKind, ChallengeError, IndeterminateError, InvalidInputError, NotFoundError, SchemaError, UpstreamHttpError } from '../../core/errors/index.js';
|
|
6
6
|
import { runSteps } from '../../core/runner/index.js';
|
|
7
7
|
import { isoTime, explicitOffsetTime, zonedTime } from '../../core/time/index.js';
|
|
8
8
|
import { TrawlClient, decodeText, fetchBounded, parseJsonBytes } from '../../core/transport/index.js';
|
|
@@ -19,7 +19,8 @@ import { API_LABELS, PROOF_OF_DELIVERY_SCAN, apiStage, apiStatus, scanStage, wor
|
|
|
19
19
|
// `continueWithoutVerification=true`. A rejected postcode (HTTP 400) is
|
|
20
20
|
// retried once without verification and reported as unverified rather than
|
|
21
21
|
// failing the lookup.
|
|
22
|
-
// - The consignee web page is the fallback when the guest API is inconclusive
|
|
22
|
+
// - The consignee web page is the fallback when the guest API is inconclusive,
|
|
23
|
+
// except HTTP 503: the service is down, irrespective of the parcel number.
|
|
23
24
|
// It sits behind Cloudflare, so it is fetched through the browser service's
|
|
24
25
|
// legacy command API when one is configured and directly otherwise.
|
|
25
26
|
const TRACKING_BASE = 'https://www.dpdgroup.com/ch/mydpd/my-parcels/incoming';
|
|
@@ -54,7 +55,7 @@ export class DPDChallengeError extends ChallengeError {
|
|
|
54
55
|
/**
|
|
55
56
|
* The guest API answered, but the answer proves nothing about the shipment
|
|
56
57
|
* (unreachable, malformed, unauthenticated, or an HTTP status the guest flow
|
|
57
|
-
* handles itself). The rendered page is allowed to recover from it.
|
|
58
|
+
* handles itself, except HTTP 503). The rendered page is allowed to recover from it.
|
|
58
59
|
*/
|
|
59
60
|
export class DPDAPIError extends IndeterminateError {
|
|
60
61
|
constructor(message, options) {
|
|
@@ -778,6 +779,10 @@ export class DPDTracker {
|
|
|
778
779
|
catch (error) {
|
|
779
780
|
throw new DPDAPIError('DPD guest API is unreachable', { cause: error });
|
|
780
781
|
}
|
|
782
|
+
// A 503 describes DPD's availability, so the page must not turn it into
|
|
783
|
+
// an answer about the parcel or hold up the caller's other lookups.
|
|
784
|
+
if (result.response.status === 503)
|
|
785
|
+
throw new UpstreamHttpError('DPD guest API', 503);
|
|
781
786
|
if (!result.response.ok)
|
|
782
787
|
throw new DPDAPIHttpError(result.response.status);
|
|
783
788
|
let payload;
|
|
@@ -62,6 +62,7 @@ export function classifyIndiaPostEvent(...values) {
|
|
|
62
62
|
'returnedfromcustoms',
|
|
63
63
|
'releasedbyexportcustoms',
|
|
64
64
|
'releasedbycustoms',
|
|
65
|
+
'outofexportcustoms',
|
|
65
66
|
]))
|
|
66
67
|
return { status: 'in_transit', stage: 'in_transit' };
|
|
67
68
|
if (includesAny(key, ['customs', 'customclearance', 'customreceive'])) {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
{ "code": "ItemDelivered", "wording": "Item Delivered", "language": "en", "stage": "delivered", "confirmedBy": "fixture", "firstSeen": "2026-09-01", "note": "Substring keys `itemdelivered`, `deliveredtorecipient`, `delivereddelivery`." },
|
|
15
15
|
{ "code": "DeliveryAttempted", "wording": "Delivery Attempted", "language": "en", "stage": "failed_attempt", "confirmedBy": "prior-art", "firstSeen": "2026-09-01", "note": "Substring keys `deliveryattempted`, `deliveryfailed`, `notdelivered`, `undelivered`." },
|
|
16
16
|
{ "code": "InsufficientAddress", "wording": "Insufficient Address", "language": "en", "stage": "exception", "confirmedBy": "prior-art", "firstSeen": "2026-09-01", "note": "Substring keys `insufficientaddress`, `addresseecannotbelocated`, `damaged`, `refused`, `lost`. Reclassified to exception 2026-09-12." },
|
|
17
|
-
{ "code": "ReturnToSender", "wording": "Return To Sender", "language": "en", "stage": "returned", "confirmedBy": "prior-art", "firstSeen": "2026-09-01", "note": "Substring keys `returntosender`, `returnedtocustomer`, `returnedtobookingoffice`, `returnitem`." }
|
|
17
|
+
{ "code": "ReturnToSender", "wording": "Return To Sender", "language": "en", "stage": "returned", "confirmedBy": "prior-art", "firstSeen": "2026-09-01", "note": "Substring keys `returntosender`, `returnedtocustomer`, `returnedtobookingoffice`, `returnitem`." },
|
|
18
|
+
{"wording": "Out of Export Customs", "language": "en", "stage": "in_transit", "confirmedBy": "fixture", "firstSeen": "2026-10-04", "code": "ExportCustoms", "note": "ExportCustoms also covers entry; the exit label ends that step."}
|
|
18
19
|
]
|
|
19
20
|
}
|
|
@@ -69,6 +69,10 @@ export function eventStatus(group, code, label, hasEvents) {
|
|
|
69
69
|
const described = labelStatus(label, false);
|
|
70
70
|
if (described === 'exception')
|
|
71
71
|
return described;
|
|
72
|
+
if (code.toUpperCase() === 'MD1'
|
|
73
|
+
&& comparable(label) === 'votre envoi est sur son site de distribution. nous le preparons pour le mettre en livraison.') {
|
|
74
|
+
return 'in_transit';
|
|
75
|
+
}
|
|
72
76
|
return CODE_STATUSES.get(code.toLocaleUpperCase('en-US'))
|
|
73
77
|
?? GROUP_STATUSES.get(group.toLocaleUpperCase('en-US'))
|
|
74
78
|
?? (described !== 'unknown' ? described : labelStatus(label, hasEvents));
|
|
@@ -158,6 +158,15 @@
|
|
|
158
158
|
"confirmedBy": "live",
|
|
159
159
|
"firstSeen": "2026-09-20",
|
|
160
160
|
"note": "Resolved by the shared classifier; \"sont terminees\" is the release and stays in_transit."
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"wording": "Votre envoi est sur son site de distribution. Nous le préparons pour le mettre en livraison.",
|
|
164
|
+
"language": "fr",
|
|
165
|
+
"stage": "in_transit",
|
|
166
|
+
"confirmedBy": "fixture",
|
|
167
|
+
"firstSeen": "2026-10-04",
|
|
168
|
+
"code": "MD1",
|
|
169
|
+
"note": "DISTOU/MD1 also covers preparation before the delivery round."
|
|
161
170
|
}
|
|
162
171
|
]
|
|
163
172
|
}
|
|
@@ -22,7 +22,7 @@ export function postiStatus(main, subStatus) {
|
|
|
22
22
|
/** Map each scan's own label, never the current parcel state or explanatory reason text. */
|
|
23
23
|
export function postiEventStage(description) {
|
|
24
24
|
const text = description.trim().toLowerCase();
|
|
25
|
-
if (/^(?:the )?item has been delivered\.?$/.test(text))
|
|
25
|
+
if (/^(?:the )?item (?:has been delivered|delivered to the recipient)\.?$/.test(text))
|
|
26
26
|
return 'delivered';
|
|
27
27
|
if (/^(?:the )?item is ready for (?:a )?pick ?up\.?$/.test(text))
|
|
28
28
|
return 'ready_for_pickup';
|
|
@@ -32,10 +32,14 @@ export function postiEventStage(description) {
|
|
|
32
32
|
return 'returned';
|
|
33
33
|
if (/^declare the item|^the item has been declared|^handling fee for item/.test(text))
|
|
34
34
|
return 'customs';
|
|
35
|
+
if (/^(?:the )?item (?:has been registered|has arrived to destination country|is on the way to the recipient|is ready for delivery in destination country|in process in office of exchange|is on its way to the destination country)\.?$/.test(text))
|
|
36
|
+
return 'in_transit';
|
|
35
37
|
if (/^(?:the )?item (?:is in (?:sorting|transport)|accepted from transport|has been released for delivery|arrived in the destination country|has departed from country of origin)/.test(text))
|
|
36
38
|
return 'in_transit';
|
|
37
39
|
if (/^(?:the )?item (?:has been received|has been accepted)/.test(text))
|
|
38
40
|
return 'accepted';
|
|
41
|
+
if (/^(?:the )?item received for transport\.?$/.test(text))
|
|
42
|
+
return 'accepted';
|
|
39
43
|
// Notifications, electronic pre-advice and unmapped wording prove no new movement.
|
|
40
44
|
return 'pending';
|
|
41
45
|
}
|
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
{ "wording": "Item is ready for a pick up", "language": "en", "stage": "ready_for_pickup", "confirmedBy": "live", "firstSeen": "2026-09-21" },
|
|
14
14
|
{ "wording": "Item accepted from transport", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-09-21" },
|
|
15
15
|
{ "wording": "Item has been released for delivery.", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-09-21" },
|
|
16
|
-
{ "wording": "We sent the recipient an email about the item.", "language": "en", "stage": "pending", "confirmedBy": "live", "firstSeen": "2026-09-21", "note": "Notification alone proves no new movement." }
|
|
16
|
+
{ "wording": "We sent the recipient an email about the item.", "language": "en", "stage": "pending", "confirmedBy": "live", "firstSeen": "2026-09-21", "note": "Notification alone proves no new movement." },
|
|
17
|
+
{"wording": "Item delivered to the recipient.", "language": "en", "stage": "delivered", "confirmedBy": "fixture", "firstSeen": "2026-10-04"},
|
|
18
|
+
{"wording": "Item has been registered", "language": "en", "stage": "in_transit", "confirmedBy": "fixture", "firstSeen": "2026-10-04", "note": "Posti describes registration as repeated handling during transport, not electronic pre-advice."},
|
|
19
|
+
{"wording": "Item has arrived to destination country", "language": "en", "stage": "in_transit", "confirmedBy": "fixture", "firstSeen": "2026-10-04"},
|
|
20
|
+
{"wording": "Item is on the way to the recipient", "language": "en", "stage": "in_transit", "confirmedBy": "fixture", "firstSeen": "2026-10-04"},
|
|
21
|
+
{"wording": "Item is ready for delivery in destination country", "language": "en", "stage": "in_transit", "confirmedBy": "fixture", "firstSeen": "2026-10-04", "note": "Readiness in the country does not establish the delivery round."},
|
|
22
|
+
{"wording": "Item in process in office of exchange.", "language": "en", "stage": "in_transit", "confirmedBy": "fixture", "firstSeen": "2026-10-04"},
|
|
23
|
+
{"wording": "The item is on its way to the destination country.", "language": "en", "stage": "in_transit", "confirmedBy": "fixture", "firstSeen": "2026-10-04"},
|
|
24
|
+
{"wording": "Item received for transport", "language": "en", "stage": "accepted", "confirmedBy": "fixture", "firstSeen": "2026-10-04"}
|
|
17
25
|
]
|
|
18
26
|
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Every event carries a `category` token next to its localized
|
|
5
5
|
* `status_description`. Categories are the primary signal. The English
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* labels refine the overloaded Processing and Customs categories, which also
|
|
7
|
+
* cover sorting, acceptance and customs release.
|
|
8
8
|
* Categories are matched case-insensitively after trimming, because the
|
|
9
9
|
* international tracker has been seen returning them capitalized
|
|
10
10
|
* ("Pre-advised") and lower case in the same history.
|
|
@@ -20,6 +20,14 @@ export { CATEGORY_STATUS as POSTNL_STATUS };
|
|
|
20
20
|
/** The status and stage for one PostNL event category, or undefined when unmapped. */
|
|
21
21
|
export function postNLStatus(category, description) {
|
|
22
22
|
const code = (typeof category === 'string' ? category : '').trim().toLocaleLowerCase('en-US');
|
|
23
|
+
const label = (typeof description === 'string' ? description : '').trim().toLocaleLowerCase('en-US').replace(/[.!]+$/, '');
|
|
24
|
+
if (code === 'customs' && label === 'the item is released by customs') {
|
|
25
|
+
return { status: 'in_transit', stage: 'in_transit' };
|
|
26
|
+
}
|
|
27
|
+
if (code === 'processing' && ['the item has arrived at the domestic sorting centre',
|
|
28
|
+
'the item is at the local sorting centre'].includes(label)) {
|
|
29
|
+
return { status: 'in_transit', stage: 'in_transit' };
|
|
30
|
+
}
|
|
23
31
|
if (code === 'processing' && typeof description === 'string'
|
|
24
32
|
&& description.trim().toLocaleLowerCase('en-US') === 'the item is out for delivery') {
|
|
25
33
|
return { status: 'out_for_delivery', stage: 'out_for_delivery' };
|
|
@@ -135,6 +135,30 @@
|
|
|
135
135
|
"stage": "out_for_delivery",
|
|
136
136
|
"confirmedBy": "live",
|
|
137
137
|
"note": "Exact English description refines the overloaded Processing category."
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"wording": "The item is released by customs",
|
|
141
|
+
"language": "en",
|
|
142
|
+
"stage": "in_transit",
|
|
143
|
+
"confirmedBy": "fixture",
|
|
144
|
+
"firstSeen": "2026-10-04",
|
|
145
|
+
"note": "The Customs category includes release; this exact label ends the customs step."
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"wording": "The item has arrived at the domestic sorting centre",
|
|
149
|
+
"language": "en",
|
|
150
|
+
"stage": "in_transit",
|
|
151
|
+
"confirmedBy": "fixture",
|
|
152
|
+
"firstSeen": "2026-10-04",
|
|
153
|
+
"note": "The Processing category also covers handling after acceptance."
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"wording": "The item is at the local sorting centre",
|
|
157
|
+
"language": "en",
|
|
158
|
+
"stage": "in_transit",
|
|
159
|
+
"confirmedBy": "fixture",
|
|
160
|
+
"firstSeen": "2026-10-04",
|
|
161
|
+
"note": "The Processing category also covers handling after acceptance."
|
|
138
162
|
}
|
|
139
163
|
]
|
|
140
164
|
}
|
|
@@ -23,6 +23,7 @@ const EXPRESS_CODES = {
|
|
|
23
23
|
HW: { status: 'in_transit', stage: 'in_transit' },
|
|
24
24
|
AS: { status: 'in_transit', stage: 'in_transit' },
|
|
25
25
|
IR: { status: 'in_transit', stage: 'in_transit' },
|
|
26
|
+
RC: { status: 'in_transit', stage: 'in_transit' },
|
|
26
27
|
OD: { status: 'out_for_delivery', stage: 'out_for_delivery' },
|
|
27
28
|
OK: { status: 'delivered', stage: 'delivered' },
|
|
28
29
|
LP: { status: 'exception', stage: 'exception' },
|
|
@@ -121,6 +121,14 @@
|
|
|
121
121
|
"stage": "returned",
|
|
122
122
|
"confirmedBy": "live",
|
|
123
123
|
"firstSeen": "2026-09-28"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"code": "RC",
|
|
127
|
+
"wording": "Customs has released the goods",
|
|
128
|
+
"language": "en",
|
|
129
|
+
"stage": "in_transit",
|
|
130
|
+
"confirmedBy": "fixture",
|
|
131
|
+
"firstSeen": "2026-10-04"
|
|
124
132
|
}
|
|
125
133
|
]
|
|
126
134
|
}
|
|
@@ -108,7 +108,7 @@ export function trackingLanguageStage(description) {
|
|
|
108
108
|
// Completion/release is distinct from pending or explicitly negated clearance.
|
|
109
109
|
if (/attend d'etre libere|attente de liberation|wartet auf.*freigabe|attesa di svincolo|(?:customs|clearance).*\bnot\b.*(?:clear|complet|releas)|dedouanement.*pas termine|customs (?:not cleared|clearance not completed)|not.*released by customs|dedouanement.*(?:non termine|pas termine)|(?:\bnot|n't|\bnever)(?: yet)?(?: been)? cleared (?:through |by )?customs|\bbe(?:ing)? cleared (?:through |by )?customs|non dedouane|zollabfertigung.*nicht abgeschlossen|nicht.*zoll.*freigegeben|sdoganamento.*non completato|non sdoganat/.test(text))
|
|
110
110
|
return 'customs';
|
|
111
|
-
if (/clearance.*(?:has been completed|completed)|clearance (?:complete|success)|(?:leaving|departed from|left) customs|completion of customs|formalites.*(?:terminee|achevee)|released by (?:customs|a government agency)|customs (?:cleared|released)|\bcleared (?:through |by )?customs|dedouanement.*(?:termine|acheve)|fin du dedouanement|libere.*(?:douane|autorite)|douane.*libere|zollabfertigung.*abgeschlossen|(?:zoll|behorde).*freigegeben|sdoganamento.*completato|completamento.*sdoganamento|svincolat.*(?:dogana|autorita)/.test(text))
|
|
111
|
+
if (/clearance.*(?:has been completed|completed)|clearance (?:complete|success)|(?:leaving|departed from|left) customs|completion of customs|formalites.*(?:terminee|achevee)|released by (?:customs|a government agency)|customs (?:cleared|(?:has |have )?released)|\bcleared (?:through |by )?customs|dedouanement.*(?:termine|acheve)|fin du dedouanement|libere.*(?:douane|autorite)|douane.*libere|zollabfertigung.*abgeschlossen|(?:zoll|behorde).*freigegeben|sdoganamento.*completato|completamento.*sdoganamento|svincolat.*(?:dogana|autorita)/.test(text))
|
|
112
112
|
return 'in_transit';
|
|
113
113
|
if (/customs|clearance|douan|formalites (?:d')?(?:import|export)|zoll|dogan|government agency|autorite gouvernementale|staatliche behorde|autorita governativa/.test(text))
|
|
114
114
|
return 'customs';
|
|
@@ -126,6 +126,12 @@ export function trackingLanguageStage(description) {
|
|
|
126
126
|
&& /(?<!\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|\brecogid[oa] por (?:el |la )?destinatari[oa]\b|\b(?:levantad|retirad)[oa] pelo destinatario\b/.test(text))
|
|
127
127
|
return 'delivered';
|
|
128
128
|
// Posting/collection by the carrier, not recipient pickup or data submission.
|
|
129
|
+
if (/^(?:the |your )?(?:item|package|parcel|shipment) (?:has been |was |is )?(?:received for transport|dropped off by (?:the )?sender at (?:our |the )?postal partner)\b/.test(text))
|
|
130
|
+
return 'accepted';
|
|
131
|
+
// A vehicle load and a gateway scan prove handling, but do not name the delivery round.
|
|
132
|
+
if (/^(?:load vehicle|scan ok gateway)$/.test(text)
|
|
133
|
+
|| /^(?:the )?item in process in office of exchange\b/.test(text))
|
|
134
|
+
return 'in_transit';
|
|
129
135
|
if (/^(?:posting\/collection|posted|origin scan)$|received at (?:our |the )?(?:[a-z]+ )?origin (?:office|facility|hub)|dropped off (?:after|before) (?:the )?latest drop ?off time/.test(text))
|
|
130
136
|
return 'accepted';
|
|
131
137
|
if (/accepted|picked up|pick up was successful|^drop off$|pick ?up scan|prepared the package for return to ups or pickup by ups|handed (?:over )?to (?:dpd|gls)|package received at dhl|item booked|consignment was mailed|posted at a postal point|pris en charge|prise en charge|collecte.*(?:reussi|effectue)|recupere.*boite aux lettres|depose.*point postal|remis a (?:dpd|gls)|envoi.*depose|abholung.*erfolgreich|eingeliefert|an (?:dpd|gls) ubergeben|paket.*(?:angenommen|ubernommen)|ritiro.*(?:riuscito|effettuato)|pres[oa] in carico|affidat[oa] a (?:dpd|gls)|depositat.*punto postale|spedizione.*impostata/.test(text))
|
|
@@ -43,6 +43,12 @@ function sourceEventStage(description, includeBroadMovement = true) {
|
|
|
43
43
|
return 'registered';
|
|
44
44
|
if (/prise en charge de votre colis sur notre site logistique/.test(french))
|
|
45
45
|
return 'accepted';
|
|
46
|
+
// Posti's handling labels survive in aggregator histories. Registration is
|
|
47
|
+
// a repeated physical scan here, and the reason can describe future customs
|
|
48
|
+
// or delivery without making either the event's stage.
|
|
49
|
+
if (/^(?:the )?item (?:accepted from transport(?:[.!]?$|[.!]? this is a transportation marking\b)|has arrived to destination country(?:[.!]?$|[.!]? the item has reached (?:the target country|finland)\b)|is ready for delivery in destination country[.!]?$|in process in office of exchange(?:[.!]?$|[.!]? the item is being processed\b))/i.test(description)
|
|
50
|
+
|| /^(?:the )?item has been registered(?:[.!]?$| the item can be registered several times during delivery\b)/i.test(description))
|
|
51
|
+
return 'in_transit';
|
|
46
52
|
if (nonterminalEnglishReturn(description))
|
|
47
53
|
return 'exception';
|
|
48
54
|
if (/returned to (?:the )?sender/i.test(description))
|
|
@@ -76,7 +82,7 @@ function sourceEventStage(description, includeBroadMovement = true) {
|
|
|
76
82
|
if (/out for delivery/i.test(description)
|
|
77
83
|
&& !/(?:once|when|as soon as) (?:the |your )?(?:shipment|parcel|package|item) is out for delivery/i.test(description))
|
|
78
84
|
return 'out_for_delivery';
|
|
79
|
-
if (/clearance (?:processing )?completed|customs (?:cleared|released)/i.test(description))
|
|
85
|
+
if (/clearance (?:processing )?completed|customs (?:cleared|released)|customs (?:has |have )released (?:the )?(?:goods|shipment|parcel|package|item)\b/i.test(description))
|
|
80
86
|
return 'in_transit';
|
|
81
87
|
if (/customs|clearance/i.test(description))
|
|
82
88
|
return 'customs';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-parcel-scraper",
|
|
3
|
-
"version": "0.5.0-main.
|
|
3
|
+
"version": "0.5.0-main.323",
|
|
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",
|