universal-parcel-scraper 0.53.0-main.667 → 0.53.0-main.670

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 2,993 such statuses, each filed under one stage.
158
+ The carrier folders record 2,998 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.
@@ -14,8 +14,8 @@ const MAX_BYTES = 2_000_000;
14
14
  /** Opening a session has taken up to 75 seconds. */
15
15
  const SESSION_OPEN_MS = 120_000;
16
16
  /**
17
- * The service accepts a session for at least four hours, an idle one too. From this age the next
18
- * one opens beside it, and replaces it once open.
17
+ * The service accepts a session for at least eight hours, one left idle for four too. From this
18
+ * age the next one opens beside it, and replaces it once open.
19
19
  */
20
20
  const SESSION_RENEW_MS = 3 * 3_600_000;
21
21
  /** A kept session that failed to open is tried again after this long. */
@@ -81,10 +81,11 @@ export function parseEcoscooting(payload, rawNumber) {
81
81
  }
82
82
  // A shop's name and address, once a scan places the parcel there. It stays
83
83
  // after collection so the parcel still says where it was collected, but a
84
- // delivery by the courier, at the door, has none. The pickup PIN, the shop's
85
- // phone and the station id are never read.
84
+ // delivery by the courier, at the door, has none, and neither has a parcel on
85
+ // its way back to the sender, which no longer waits there. The pickup PIN, the
86
+ // shop's phone and the station id are never read.
86
87
  function pickupPoint(station, events, stage) {
87
- if (!isRecord(station) || !events.some(event => PICKUP_POINT_CODES.has(String(event.provider_code))))
88
+ if (!isRecord(station) || stage === 'returned' || !events.some(event => PICKUP_POINT_CODES.has(String(event.provider_code))))
88
89
  return {};
89
90
  if (stage === 'delivered' && !COLLECTION_CODES.has(String(events[0]?.provider_code)))
90
91
  return {};
@@ -2,7 +2,7 @@ import { lookupBudget } from '../../core/adapter/index.js';
2
2
  import { IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
3
3
  import { lastMovement } from '../../core/result/pickup.js';
4
4
  import { zonedTime } from '../../core/time/index.js';
5
- import { clean, fetchBounded, parseJsonBytes, UpstreamHttpError, userAgentOf } from '../../core/transport/index.js';
5
+ import { clean, cleanScalar, fetchBounded, parseJsonBytes, UpstreamHttpError, userAgentOf } from '../../core/transport/index.js';
6
6
  import { isRecord } from '../../core/types.js';
7
7
  import { classifyPacketaStatus, packetaEventStage } from './status.js';
8
8
  // Protocol provenance:
@@ -15,10 +15,11 @@ import { classifyPacketaStatus, packetaEventStage } from './status.js';
15
15
  // Expired 2023-era reported Z codes return the same 404 (unknown and expired
16
16
  // are intentionally indistinguishable to anonymous callers).
17
17
  // - Shape reference: the prior-art payload samples (confirmed live 2026-08-19
18
- // against real delivered parcels; values fictionalized there). Only
19
- // packetStatusId "3" (delivered) is live-confirmed; the other codes are
20
- // reconstructed — an unmapped code is schema drift, never a guess. The maps
21
- // live in status.ts.
18
+ // against real delivered parcels; values fictionalized there). The
19
+ // packetStatusId names come from the public tracking page's own script;
20
+ // only "3" (delivered), "5" and "21" (returns) are live-confirmed, the other
21
+ // codes are reconstructed — an unmapped code is schema drift, never a guess.
22
+ // The maps live in status.ts.
22
23
  const TRACKING_ENDPOINT = 'https://tracking.packeta.com/api/getPacketById';
23
24
  const TRACKING_LOCALE = 'en';
24
25
  const DEFAULT_TIMEOUT_MS = 15_000;
@@ -100,12 +101,15 @@ export function parsePacketaTrackingResponse(payload, trackingNumber) {
100
101
  const events = parsed.slice(0, MAX_EVENTS_TO_RETURN).map(({ event }) => event);
101
102
  // Sender (merchant) and branchAddress (Z-BOX/partner shop) carry no
102
103
  // recipient PII and are the only pickup signal — Packeta exposes no ETA.
103
- // For a parcel a courier brings to the door, branchAddress names a
104
- // home-delivery branch, so a delivered parcel keeps it only when its last
105
- // movement made it ready for pickup.
104
+ // For a parcel a courier brings to the door, courierId is "1" and
105
+ // branchAddress names a home-delivery branch, which Packeta's own page does
106
+ // not link to. A returned parcel's branch is a depot or the point it no
107
+ // longer waits at, and a delivered one keeps its branch only when its last
108
+ // movement made it ready for pickup there.
106
109
  const sender = clean(item.sender, 200) || null;
107
- const pickupPoint = classified?.stage === 'delivered' && lastMovement(events) !== 'ready_for_pickup'
108
- ? null : clean(item.branchAddress, 300) || null;
110
+ const atPoint = cleanScalar(item.courierId, 8) !== '1' && classified?.stage !== 'returned'
111
+ && (classified?.stage !== 'delivered' || lastMovement(events) === 'ready_for_pickup');
112
+ const pickupPoint = atPoint ? clean(item.branchAddress, 300) || null : null;
109
113
  const deliveredAt = classified?.status === 'delivered' ? events[0]?.time ?? null : null;
110
114
  if (!classified) {
111
115
  return {
@@ -11,10 +11,12 @@
11
11
  * where the final stage came from.
12
12
  *
13
13
  * Provenance: derived from the prior-art client
14
- * https://github.com/ha-parcel-integrations/ha-packeta (MIT). Only
15
- * `packetStatusId` "3" (delivered) is live-confirmed there; the other ids are
16
- * reconstructed. The event sentences were confirmed live on 2026-08-19 against
17
- * real delivered parcels (values fictionalized in that project's fixtures).
14
+ * https://github.com/ha-parcel-integrations/ha-packeta (MIT), and from the id
15
+ * names the public tracking page's own script declares (read 2026-10-09).
16
+ * `packetStatusId` "3" (delivered), "5" (returned to sender) and "21" are
17
+ * live-confirmed; the other ids are reconstructed. The event sentences were
18
+ * confirmed live on 2026-08-19 against real delivered parcels (values
19
+ * fictionalized in that project's fixtures); the return's sentence on 2026-10-09.
18
20
  */
19
21
  import type { ClassifiedStatus, Stage } from '../../core/status/index.js';
20
22
  /** The stage and status for a `packetStatusId`, or undefined when unmapped. */
@@ -1,3 +1,4 @@
1
+ const RETURNED = { status: 'exception', stage: 'returned' };
1
2
  const PACKET_STATUS = {
2
3
  // TO_BE_PROCESSED
3
4
  '997': { status: 'pending', stage: 'registered' },
@@ -7,11 +8,20 @@ const PACKET_STATUS = {
7
8
  '31': { status: 'in_transit', stage: 'in_transit' },
8
9
  // READY_FOR_PICKUP
9
10
  '2': { status: 'out_for_delivery', stage: 'ready_for_pickup' },
11
+ // READY_LAST_DAY: the last day it waits at the point
12
+ '996': { status: 'out_for_delivery', stage: 'ready_for_pickup' },
10
13
  // ISSUED_AND_ACCOUNTED (live-confirmed delivered wording:
11
14
  // "The package has been delivered")
12
15
  '3': { status: 'delivered', stage: 'delivered' },
13
- // LOST_OR_UNKNOWN
14
- '21': { status: 'exception', stage: 'failed_attempt' },
16
+ // RETURNED_AND_ACCOUNTED (live wording "Returned to sender")
17
+ '5': RETURNED,
18
+ // RETURN
19
+ '20': RETURNED,
20
+ // TO_BE_RETURNED
21
+ '998': RETURNED,
22
+ // Named LOST_OR_UNKNOWN in the page's script, but worded "Return (on the way
23
+ // back)" live.
24
+ '21': RETURNED,
15
25
  };
16
26
  const EVENT_TEXT_STAGES = [
17
27
  ['aware of your parcel and are waiting for the sender', 'registered'],
@@ -24,6 +34,7 @@ const EVENT_TEXT_STAGES = [
24
34
  ['ready for pickup', 'ready_for_pickup'],
25
35
  ['the parcel is with you', 'delivered'],
26
36
  ['investigating the status of the parcel', 'failed_attempt'],
37
+ ['returned the parcel back to the sender', 'returned'],
27
38
  ];
28
39
  /** The stage and status for a `packetStatusId`, or undefined when unmapped. */
29
40
  export function classifyPacketaStatus(code) {
@@ -5,8 +5,12 @@
5
5
  { "code": "1", "language": "en", "stage": "in_transit", "confirmedBy": "prior-art", "firstSeen": "2026-09-10", "note": "WAITING_FOR_DELIVERY (in warehouse); reconstructed." },
6
6
  { "code": "31", "language": "en", "stage": "in_transit", "confirmedBy": "prior-art", "firstSeen": "2026-09-10", "note": "ON_THE_WAY; reconstructed." },
7
7
  { "code": "2", "language": "en", "stage": "ready_for_pickup", "confirmedBy": "prior-art", "firstSeen": "2026-09-10", "note": "READY_FOR_PICKUP; reconstructed." },
8
+ { "code": "996", "language": "en", "stage": "ready_for_pickup", "confirmedBy": "bundle", "firstSeen": "2026-10-09", "note": "READY_LAST_DAY, named in the tracking page's script; not observed." },
8
9
  { "code": "3", "wording": "The package has been delivered", "language": "en", "stage": "delivered", "confirmedBy": "live", "firstSeen": "2026-08-19", "note": "ISSUED_AND_ACCOUNTED; the only packetStatusId confirmed against real delivered parcels." },
9
- { "code": "21", "language": "en", "stage": "failed_attempt", "confirmedBy": "prior-art", "firstSeen": "2026-09-10", "note": "LOST_OR_UNKNOWN; reconstructed." },
10
+ { "code": "5", "wording": "Returned to sender", "language": "en", "stage": "returned", "confirmedBy": "live", "firstSeen": "2026-10-09", "note": "RETURNED_AND_ACCOUNTED, named in the tracking page's script." },
11
+ { "code": "20", "language": "en", "stage": "returned", "confirmedBy": "bundle", "firstSeen": "2026-10-09", "note": "RETURN, named in the tracking page's script; not observed." },
12
+ { "code": "998", "language": "en", "stage": "returned", "confirmedBy": "bundle", "firstSeen": "2026-10-09", "note": "TO_BE_RETURNED, named in the tracking page's script; not observed." },
13
+ { "code": "21", "wording": "Return (on the way back)", "language": "en", "stage": "returned", "confirmedBy": "live", "firstSeen": "2026-10-09", "note": "Named LOST_OR_UNKNOWN in the tracking page's script, but worded as a return live." },
10
14
  { "wording": "We are aware of your parcel and are waiting for the sender…", "language": "en", "stage": "registered", "confirmedBy": "live", "firstSeen": "2026-08-19", "note": "Event sentence; substring match." },
11
15
  { "wording": "…assigned a tracking number…", "language": "en", "stage": "registered", "confirmedBy": "live", "firstSeen": "2026-08-19", "note": "Event sentence; substring match." },
12
16
  { "wording": "…successfully received the parcel for transport…", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-08-19", "note": "Event sentence; substring match." },
@@ -16,6 +20,7 @@
16
20
  { "wording": "…on its way to you…", "language": "en", "stage": "out_for_delivery", "confirmedBy": "live", "firstSeen": "2026-08-19", "note": "Event sentence; substring match." },
17
21
  { "wording": "The parcel is ready for pickup…", "language": "en", "stage": "ready_for_pickup", "confirmedBy": "live", "firstSeen": "2026-08-19", "note": "Event sentence; substring match." },
18
22
  { "wording": "The parcel is with you…", "language": "en", "stage": "delivered", "confirmedBy": "live", "firstSeen": "2026-08-19", "note": "Event sentence; substring match." },
19
- { "wording": "…investigating the status of the parcel…", "language": "en", "stage": "failed_attempt", "confirmedBy": "live", "firstSeen": "2026-08-19", "note": "Event sentence; substring match." }
23
+ { "wording": "…investigating the status of the parcel…", "language": "en", "stage": "failed_attempt", "confirmedBy": "live", "firstSeen": "2026-08-19", "note": "Event sentence; substring match." },
24
+ { "wording": "We have returned the parcel back to the sender.", "language": "en", "stage": "returned", "confirmedBy": "live", "firstSeen": "2026-10-09", "note": "Event sentence; substring match." }
20
25
  ]
21
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-parcel-scraper",
3
- "version": "0.53.0-main.667",
3
+ "version": "0.53.0-main.670",
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",