universal-parcel-scraper 0.41.0-main.397 → 0.41.1-main.399

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/data/catalog.json CHANGED
@@ -828,10 +828,9 @@
828
828
  ]
829
829
  },
830
830
  "dpd": {
831
- "displayName": "DPD",
831
+ "displayName": "DPD Switzerland",
832
832
  "color": "#dc0032",
833
833
  "aliases": [
834
- "DPD Switzerland",
835
834
  "DPD (Schweiz) AG",
836
835
  "myDPD"
837
836
  ],
@@ -752,13 +752,9 @@ var ve = {
752
752
  ]
753
753
  },
754
754
  dpd: {
755
- displayName: "DPD",
755
+ displayName: "DPD Switzerland",
756
756
  color: "#dc0032",
757
- aliases: [
758
- "DPD Switzerland",
759
- "DPD (Schweiz) AG",
760
- "myDPD"
761
- ],
757
+ aliases: ["DPD (Schweiz) AG", "myDPD"],
762
758
  countries: ["CH"],
763
759
  selectable: !0,
764
760
  timezone: "Europe/Zurich",
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "id": "dpd",
3
- "displayName": "DPD",
3
+ "displayName": "DPD Switzerland",
4
4
  "aliases": [
5
- "DPD Switzerland",
6
5
  "DPD (Schweiz) AG",
7
6
  "myDPD"
8
7
  ],
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
+ import { DateTime } from 'luxon';
2
3
  import { CarrierError, ChallengeError, IndeterminateError, SchemaError, TransportError } from '../../core/errors/index.js';
3
- import { zonedTime } from '../../core/time/index.js';
4
+ import { calendarDay, isoTime, zonedTime } from '../../core/time/index.js';
4
5
  import { clean, decodeText, fetchBounded, userAgentOf } from '../../core/transport/index.js';
5
6
  import { xmlDocument } from '../../core/transport/xml.js';
6
7
  export const DPD_DE_APP_API = 'https://api.paketnavigator.de/services/v1/Navigator3Service.asmx';
@@ -51,6 +52,63 @@ function scalar(node, name, max = 200) {
51
52
  invalid();
52
53
  return clean(matches[0]?.text ?? '', max);
53
54
  }
55
+ function optional(node, name) {
56
+ const matches = children(node, name);
57
+ if (matches.length > 1)
58
+ invalid();
59
+ return matches[0];
60
+ }
61
+ const affirmative = (value) => value === 'true' || value === '1';
62
+ const placeholderDay = (day) => day === '2000-01-01' || day === '0001-01-01';
63
+ /** Validate source digits without assigning a zone to an offset-less estimate. */
64
+ function estimateClock(raw) {
65
+ if (!/^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)?$/.test(raw)
66
+ || placeholderDay(raw.slice(0, 10)))
67
+ return null;
68
+ return isoTime(raw, 'UTC');
69
+ }
70
+ function estimateDay(raw) {
71
+ if (estimateClock(raw))
72
+ return raw.slice(0, 10);
73
+ const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw);
74
+ const display = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(raw);
75
+ const day = iso ? calendarDay(Number(iso[1]), Number(iso[2]), Number(iso[3]))
76
+ : display ? calendarDay(Number(display[3]), Number(display[2]), Number(display[1])) : null;
77
+ return day && !placeholderDay(day) ? day : null;
78
+ }
79
+ function announcedDay(wording) {
80
+ const date = /^Your parcel is estimated to be delivered on:\s*(?:[A-Za-z]+,\s*)?(\d{2}\.\d{2}\.\d{4})$/i.exec(wording)?.[1];
81
+ return date ? estimateDay(date) : null;
82
+ }
83
+ /** Specified dates are forecasts, never scans; delivered display dates are actuals. */
84
+ function deliveryEstimate(tracking, announced) {
85
+ const planned = optional(tracking, 'NewDeliveryInfo');
86
+ const day = planned && affirmative(scalar(planned, 'PlannedDeliveryDateSpecified', 8))
87
+ ? estimateDay(scalar(planned, 'PlannedDeliveryDate', 64)) : null;
88
+ // The app displays a changed delivery day ahead of its previous forecast.
89
+ if (day && planned && affirmative(scalar(planned, 'DateChanged', 8)))
90
+ return day;
91
+ const live = optional(tracking, 'LiveTracking');
92
+ if (live && affirmative(scalar(live, 'EstimatedDeliveryDateTimeSpecified', 8))) {
93
+ const from = scalar(live, 'EstimatedDeliveryDateTimeFrom', 64);
94
+ const to = scalar(live, 'EstimatedDeliveryDateTimeTo', 64);
95
+ const start = estimateClock(from);
96
+ const end = estimateClock(to);
97
+ const hasOffset = (value) => /(?:Z|[+-]\d{2}:\d{2})$/.test(value);
98
+ if (start && end && hasOffset(from) === hasOffset(to) && end.timestamp >= start.timestamp) {
99
+ // An explicit instant can be shown on the German delivery clock. Unresolved
100
+ // wall clocks retain their digits, without receiving an invented offset.
101
+ const display = (raw, timestamp) => hasOffset(raw)
102
+ ? DateTime.fromMillis(timestamp, { zone: 'Europe/Berlin' }).toFormat('yyyy-MM-dd HH:mm')
103
+ : raw.slice(0, 16).replace('T', ' ');
104
+ const first = display(from, start.timestamp);
105
+ const last = display(to, end.timestamp);
106
+ return `${first}–${first.slice(0, 10) === last.slice(0, 10) ? last.slice(11) : last}`;
107
+ }
108
+ }
109
+ // DeliveryDateTime is display text and can omit the year: never invent it.
110
+ return day ?? announced ?? estimateDay(scalar(tracking, 'DeliveryDateTime', 64));
111
+ }
54
112
  const escaped = (value) => value.replace(/[<>&"']/g, character => `&#${character.charCodeAt(0)};`);
55
113
  const elements = (fields) => Object.entries(fields)
56
114
  .map(([name, value]) => `<${name}>${typeof value === 'string' ? escaped(value) : elements(value)}</${name}>`).join('');
@@ -81,15 +139,20 @@ export function parseDpdDeApp(data, scans, number) {
81
139
  const events = [];
82
140
  const seen = new Set();
83
141
  let placed = '';
142
+ let announced = null;
84
143
  for (const row of rows) {
85
144
  const wording = scalar(row, 'StatusText', 500);
145
+ if (!wording)
146
+ invalid();
147
+ // These rows may carry no scan clock. The date is in the announcement itself.
148
+ if (/^Your parcel is estimated to be delivered\b/i.test(wording)) {
149
+ announced = announcedDay(wording);
150
+ continue;
151
+ }
86
152
  const day = scalar(row, 'ScanDate', 16);
87
153
  const time = scalar(row, 'ScanTime', 16);
88
- if (!wording || !/^\d{2}\.\d{2}\.\d{4}$/.test(day) || !/^\d{2}:\d{2}$/.test(time))
154
+ if (!/^\d{2}\.\d{2}\.\d{4}$/.test(day) || !/^\d{2}:\d{2}$/.test(time))
89
155
  invalid();
90
- // An announced delivery day is not a scan.
91
- if (/^Your parcel is estimated to be delivered\b/i.test(wording))
92
- continue;
93
156
  const place = /^(.{1,120}) \(([A-Z]{2})\)$/.exec(scalar(row, 'Location'));
94
157
  if (place)
95
158
  placed = place[2];
@@ -125,10 +188,13 @@ export function parseDpdDeApp(data, scans, number) {
125
188
  const current = events[0];
126
189
  const rail = DPD_DE_APP_RAIL[scalar(one(tracking, 'LastStatusInfo'), 'StatusID', 64)];
127
190
  const stage = current.stage ?? rail?.stage;
191
+ const status = rail?.status ?? statusOf(current.stage);
128
192
  const kilograms = Number(scalar(one(tracking, 'OrderInfo'), 'Weight', 16).replace(',', '.'));
129
193
  return {
130
- status: rail?.status ?? statusOf(current.stage), ...(stage ? { current_stage: stage, current_stage_source: 'carrier_map' } : {}),
194
+ status, ...(stage ? { current_stage: stage, current_stage_source: 'carrier_map' } : {}),
131
195
  last_status_text: current.description ?? null, last_update: current.time ?? null,
196
+ expected_delivery: affirmative(scalar(tracking, 'Delivered', 8)) || status === 'delivered' || status === 'exception'
197
+ || stage === 'delivered' || stage === 'ready_for_pickup' ? null : deliveryEstimate(tracking, announced),
132
198
  ...(current.time ? {} : { last_update_local: current.local_time }),
133
199
  ...(Number.isFinite(kilograms) && kilograms > 0 ? { weight_kg: kilograms } : {}),
134
200
  events: events.slice(0, 100),
@@ -828,10 +828,9 @@
828
828
  ]
829
829
  },
830
830
  "dpd": {
831
- "displayName": "DPD",
831
+ "displayName": "DPD Switzerland",
832
832
  "color": "#dc0032",
833
833
  "aliases": [
834
- "DPD Switzerland",
835
834
  "DPD (Schweiz) AG",
836
835
  "myDPD"
837
836
  ],
@@ -502,9 +502,9 @@ export declare const CARRIER_CATALOG: {
502
502
  }];
503
503
  };
504
504
  readonly dpd: {
505
- readonly displayName: "DPD";
505
+ readonly displayName: "DPD Switzerland";
506
506
  readonly color: "#dc0032";
507
- readonly aliases: readonly ["DPD Switzerland", "DPD (Schweiz) AG", "myDPD"];
507
+ readonly aliases: readonly ["DPD (Schweiz) AG", "myDPD"];
508
508
  readonly countries: readonly ["CH"];
509
509
  readonly selectable: true;
510
510
  readonly timezone: "Europe/Zurich";
@@ -829,10 +829,9 @@ export const CARRIER_CATALOG = {
829
829
  ]
830
830
  },
831
831
  "dpd": {
832
- "displayName": "DPD",
832
+ "displayName": "DPD Switzerland",
833
833
  "color": "#dc0032",
834
834
  "aliases": [
835
- "DPD Switzerland",
836
835
  "DPD (Schweiz) AG",
837
836
  "myDPD"
838
837
  ],
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.41.0"
5
+ "version": "0.41.1"
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.41.0-main.397",
3
+ "version": "0.41.1-main.399",
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",