universal-parcel-scraper 0.49.2-main.565 → 0.50.0-main.568
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/dist/carriers/bring-posten/parser.js +16 -3
- package/dist/carriers/dpd/adapter.js +3 -2
- package/dist/carriers/dpd-de/adapter.js +3 -1
- package/dist/carriers/dpd-de/app.d.ts +3 -2
- package/dist/carriers/dpd-de/app.js +2 -2
- package/dist/carriers/dpd-de/service.d.ts +23 -2
- package/dist/carriers/dpd-de/service.js +109 -37
- package/dist/carriers/postnord/parser.js +32 -2
- package/dist/node.d.ts +1 -0
- package/dist/node.js +1 -0
- package/dist/server/openapi.json +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { DateTime } from 'luxon';
|
|
|
2
2
|
import { isValidS10TrackingNumber, normalizeTrackingNumber } from '../../core/detection/index.js';
|
|
3
3
|
import { IndeterminateError, InvalidInputError, SchemaError } from '../../core/errors/index.js';
|
|
4
4
|
import { calendarDay, explicitOffsetTime } from '../../core/time/index.js';
|
|
5
|
-
import { clean } from '../../core/transport/index.js';
|
|
5
|
+
import { clean, cleanScalar } from '../../core/transport/index.js';
|
|
6
6
|
import { isRecord } from '../../core/types.js';
|
|
7
7
|
import { bringStatus, bringWording } from './status.js';
|
|
8
8
|
/**
|
|
@@ -64,6 +64,18 @@ function estimatedDay(domain) {
|
|
|
64
64
|
const day = /^(\d{4})-(\d{2})-(\d{2})(?:$|T)/.exec(eta.dateOfEstimatedDeliveryIso);
|
|
65
65
|
return day ? calendarDay(Number(day[1]), Number(day[2]), Number(day[3])) : null;
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether the parcel was collected at the pickup point the portal names: the
|
|
69
|
+
* delivery, and the movement before it that made the parcel ready for pickup,
|
|
70
|
+
* were scanned at that point's unit. Notices prove no movement.
|
|
71
|
+
*/
|
|
72
|
+
function collectedAt(rows, pickupUnit) {
|
|
73
|
+
const unit = cleanScalar(pickupUnit, 32);
|
|
74
|
+
const moves = rows.filter(row => bringStatus(String(row.status), text(row.lmCauseCode, 8)));
|
|
75
|
+
const index = moves.findIndex(row => row.status !== 'DELIVERED');
|
|
76
|
+
return Boolean(unit) && index > 0 && moves[index].status === 'READY_FOR_PICKUP'
|
|
77
|
+
&& moves.slice(0, index + 1).every(row => cleanScalar(row.unitId, 32) === unit);
|
|
78
|
+
}
|
|
67
79
|
export function parseBring(payload, rawNumber) {
|
|
68
80
|
const number = normalizeBringNumber(rawNumber);
|
|
69
81
|
if (!isRecord(payload))
|
|
@@ -126,8 +138,9 @@ export function parseBring(payload, rawNumber) {
|
|
|
126
138
|
const sender = text(consignment.senderName, 200) || text(parcel.senderName, 200);
|
|
127
139
|
const pickupInfo = isRecord(parcel.domain.deliveryType) && isRecord(parcel.domain.deliveryType.pickupPointInfo)
|
|
128
140
|
? parcel.domain.deliveryType.pickupPointInfo : {};
|
|
129
|
-
const
|
|
130
|
-
|
|
141
|
+
const atPickupPoint = current?.stage === 'ready_for_pickup'
|
|
142
|
+
|| (current?.stage === 'delivered' && collectedAt(parcel.eventSet.filter(isRecord), parcel.expectedPickupUnitId));
|
|
143
|
+
const pickup = atPickupPoint ? text(parcel.expectedPickupUnitName, 200) || text(pickupInfo.expectedPickupUnitName, 200) : '';
|
|
131
144
|
const country = isRecord(consignment.recipientAddress) ? consignment.recipientAddress.countryCode : undefined;
|
|
132
145
|
const active = current && ['pending', 'in_transit', 'out_for_delivery'].includes(current.status) && current.stage !== 'ready_for_pickup';
|
|
133
146
|
return { status: current?.status ?? 'unknown', ...(current ? { current_stage: current.stage } : {}),
|
|
@@ -9,7 +9,7 @@ import { runSteps } from '../../core/runner/index.js';
|
|
|
9
9
|
import { calendarDay, isoTime, explicitOffsetTime, zonedTime } from '../../core/time/index.js';
|
|
10
10
|
import { TrawlClient, decodeText, fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
|
|
11
11
|
import { isRecord } from '../../core/types.js';
|
|
12
|
-
import {
|
|
12
|
+
import { sharedDpdAppService } from '../dpd-de/service.js';
|
|
13
13
|
import { API_LABELS, PROOF_OF_DELIVERY_SCAN, apiStage, apiStatus, scanStage, wordingStatus, } from './status.js';
|
|
14
14
|
// Protocol provenance:
|
|
15
15
|
// - The myDPD Android application talks to a guest JSON API: a Firebase
|
|
@@ -1025,7 +1025,8 @@ export const adapter = (environment) => {
|
|
|
1025
1025
|
// A host can follow a rotated key without waiting for a release.
|
|
1026
1026
|
firebaseApiKey: environment.env.DPD_FIREBASE_API_KEY?.trim() || undefined,
|
|
1027
1027
|
// The German DPD app's service holds the group's Pickup shops, Swiss ones included.
|
|
1028
|
-
|
|
1028
|
+
// Its session is the process's, which DPD Germany shares.
|
|
1029
|
+
shops: sharedDpdAppService(environment),
|
|
1029
1030
|
});
|
|
1030
1031
|
return {
|
|
1031
1032
|
id: 'dpd',
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { CarrierError, carrierErrorKind } from '../../core/errors/index.js';
|
|
2
2
|
import { DPDTracker } from '../dpd/adapter.js';
|
|
3
3
|
import { DPD_DE_OTHER_COUNTRY, DpdDeAppClient } from './app.js';
|
|
4
|
+
import { sharedDpdAppService } from './service.js';
|
|
4
5
|
/** Left to the guest API when the app's session is still opening ahead of it. */
|
|
5
6
|
const GUEST_RESERVE_MS = 20_000;
|
|
6
7
|
/** Germany reads the German app's service, then the guest protocol with its own business-unit selector. */
|
|
7
8
|
export const adapter = (environment) => {
|
|
8
|
-
|
|
9
|
+
// The process's session, which DPD Switzerland shares.
|
|
10
|
+
const app = new DpdDeAppClient({ service: sharedDpdAppService(environment) });
|
|
9
11
|
const tracker = new DPDTracker({
|
|
10
12
|
country: 'DE',
|
|
11
13
|
fetcher: environment.fetcher,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CarrierResult } from '../../core/result/index.js';
|
|
2
2
|
import type { ClassifiedStatus } from '../../core/status/index.js';
|
|
3
3
|
import type { XmlNode } from '../../core/transport/xml.js';
|
|
4
|
-
import { type DpdDePartner } from './service.js';
|
|
4
|
+
import { DpdAppService, type DpdDePartner } from './service.js';
|
|
5
5
|
/** Activity the service places outside Germany; the app tier would read the same parcel. */
|
|
6
6
|
export declare const DPD_DE_OTHER_COUNTRY = "other_country";
|
|
7
7
|
/** The app's progress rail: one coarse state for the whole parcel. */
|
|
@@ -14,10 +14,11 @@ export declare function parseDpdDeApp(data: XmlNode, scans: XmlNode, number: str
|
|
|
14
14
|
result: CarrierResult;
|
|
15
15
|
shop: string;
|
|
16
16
|
};
|
|
17
|
-
/** DPD Germany's tracking through the German app's service,
|
|
17
|
+
/** DPD Germany's tracking through the German app's service, over the given one or a new one. */
|
|
18
18
|
export declare class DpdDeAppClient {
|
|
19
19
|
#private;
|
|
20
20
|
constructor(options?: {
|
|
21
|
+
service?: DpdAppService;
|
|
21
22
|
partner?: DpdDePartner;
|
|
22
23
|
fetcher?: typeof fetch;
|
|
23
24
|
userAgent?: string;
|
|
@@ -188,11 +188,11 @@ export function parseDpdDeApp(data, scans, number) {
|
|
|
188
188
|
};
|
|
189
189
|
return { result, shop: pickup?.id ?? '' };
|
|
190
190
|
}
|
|
191
|
-
/** DPD Germany's tracking through the German app's service,
|
|
191
|
+
/** DPD Germany's tracking through the German app's service, over the given one or a new one. */
|
|
192
192
|
export class DpdDeAppClient {
|
|
193
193
|
#service;
|
|
194
194
|
constructor(options = {}) {
|
|
195
|
-
this.#service = new DpdAppService(options);
|
|
195
|
+
this.#service = options.service ?? new DpdAppService(options);
|
|
196
196
|
}
|
|
197
197
|
/**
|
|
198
198
|
* `sessionWaitMs` bounds the wait for a session still opening; the whole `timeoutMs` by default.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AdapterEnvironment } from '../../core/adapter/index.js';
|
|
1
2
|
import { type XmlNode } from '../../core/transport/xml.js';
|
|
2
3
|
export declare const DPD_DE_APP_API = "https://api.paketnavigator.de/services/v1/Navigator3Service.asmx";
|
|
3
4
|
/** The lookup stopped waiting for the session, which keeps opening for the next one. */
|
|
@@ -28,8 +29,9 @@ export interface DpdParcelShop {
|
|
|
28
29
|
}
|
|
29
30
|
/**
|
|
30
31
|
* The service needs an anonymous device session, which it takes tens of seconds to open and then
|
|
31
|
-
* accepts for hours
|
|
32
|
-
*
|
|
32
|
+
* accepts for hours. One is kept per instance, and `sharedDpdAppService` gives the process one
|
|
33
|
+
* instance per transport. The opening runs on its own clock, so a lookup that stops waiting
|
|
34
|
+
* leaves it to the next. A session past its renewal age serves while the next one opens.
|
|
33
35
|
*/
|
|
34
36
|
export declare class DpdAppService {
|
|
35
37
|
#private;
|
|
@@ -49,11 +51,30 @@ export declare class DpdAppService {
|
|
|
49
51
|
}): Promise<DpdParcelShop | undefined>;
|
|
50
52
|
/** Forgets a session the service refused, unless another lookup already replaced it. */
|
|
51
53
|
expire(session: string): void;
|
|
54
|
+
/**
|
|
55
|
+
* Keeps a session open from now on, for a long-lived host: one opens at once if there is none,
|
|
56
|
+
* and the next before the current one lapses. Openings then run whether or not a lookup waits
|
|
57
|
+
* for them. The timer does not keep the process alive.
|
|
58
|
+
*/
|
|
59
|
+
keep(): void;
|
|
52
60
|
/**
|
|
53
61
|
* One session for every lookup. The opening runs while a lookup that asked for it is still
|
|
54
62
|
* running, even one that stopped waiting; each lookup waits until its signal or `waitMs` ends.
|
|
63
|
+
* A session past its renewal age answers at once while the next one opens.
|
|
55
64
|
*/
|
|
56
65
|
session(signal: AbortSignal, waitMs: number): Promise<string>;
|
|
57
66
|
call(operation: string, fields: Fields, signal: AbortSignal, timeoutMs: number): Promise<XmlNode>;
|
|
58
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* The process's service for a transport and user agent. DPD Germany and DPD Switzerland share it,
|
|
70
|
+
* and so do the adapters of every registry, so a session opens once for all of them.
|
|
71
|
+
*/
|
|
72
|
+
export declare function sharedDpdAppService(options?: Pick<AdapterEnvironment, 'fetcher' | 'userAgent'>): DpdAppService;
|
|
73
|
+
/**
|
|
74
|
+
* Opens the session DPD Germany and DPD Switzerland read the German DPD app's service with, in the
|
|
75
|
+
* background, and keeps one open for the life of the process, so that no lookup waits the tens of
|
|
76
|
+
* seconds an opening takes. For a long-lived host, with the `fetcher` and `userAgent` its adapter
|
|
77
|
+
* environment has. The renewal timer does not keep the process alive.
|
|
78
|
+
*/
|
|
79
|
+
export declare function warmDpdSession(environment?: Pick<AdapterEnvironment, 'fetcher' | 'userAgent'>): void;
|
|
59
80
|
export {};
|
|
@@ -11,8 +11,15 @@ export const DPD_DE_SESSION_OPENING = 'session_opening';
|
|
|
11
11
|
const SOAP = 'http://schemas.xmlsoap.org/soap/envelope/';
|
|
12
12
|
const SERVICE = 'https://cloud.dpd.com/';
|
|
13
13
|
const MAX_BYTES = 2_000_000;
|
|
14
|
-
/** Opening a session has taken up to
|
|
15
|
-
const SESSION_OPEN_MS =
|
|
14
|
+
/** Opening a session has taken up to 75 seconds. */
|
|
15
|
+
const SESSION_OPEN_MS = 120_000;
|
|
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.
|
|
19
|
+
*/
|
|
20
|
+
const SESSION_RENEW_MS = 3 * 3_600_000;
|
|
21
|
+
/** A kept session that failed to open is tried again after this long. */
|
|
22
|
+
const SESSION_RETRY_MS = 15 * 60_000;
|
|
16
23
|
// Shared partner credentials of the public app, distributed with the maintainer's approval.
|
|
17
24
|
const PARTNER = { name: 'Android Paketnavigator3', token: 'A33363237662F5945576', password: '272 WetFd2mpXrgD' };
|
|
18
25
|
export function invalid() { throw new SchemaError('DPD Germany', 'DPD Germany returned invalid tracking XML'); }
|
|
@@ -61,8 +68,9 @@ function shopOf(result, id) {
|
|
|
61
68
|
}
|
|
62
69
|
/**
|
|
63
70
|
* The service needs an anonymous device session, which it takes tens of seconds to open and then
|
|
64
|
-
* accepts for hours
|
|
65
|
-
*
|
|
71
|
+
* accepts for hours. One is kept per instance, and `sharedDpdAppService` gives the process one
|
|
72
|
+
* instance per transport. The opening runs on its own clock, so a lookup that stops waiting
|
|
73
|
+
* leaves it to the next. A session past its renewal age serves while the next one opens.
|
|
66
74
|
*/
|
|
67
75
|
export class DpdAppService {
|
|
68
76
|
#partner;
|
|
@@ -70,8 +78,10 @@ export class DpdAppService {
|
|
|
70
78
|
#userAgent;
|
|
71
79
|
#now;
|
|
72
80
|
#device = randomBytes(8).toString('hex');
|
|
73
|
-
#session =
|
|
81
|
+
#session = null;
|
|
74
82
|
#opening = null;
|
|
83
|
+
#kept = false;
|
|
84
|
+
#timer;
|
|
75
85
|
constructor(options = {}) {
|
|
76
86
|
this.#partner = options.partner ?? PARTNER;
|
|
77
87
|
this.#fetcher = options.fetcher;
|
|
@@ -106,49 +116,85 @@ export class DpdAppService {
|
|
|
106
116
|
}
|
|
107
117
|
/** Forgets a session the service refused, unless another lookup already replaced it. */
|
|
108
118
|
expire(session) {
|
|
109
|
-
if (this.#session === session)
|
|
110
|
-
this.#session =
|
|
119
|
+
if (this.#session?.token === session)
|
|
120
|
+
this.#session = null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Keeps a session open from now on, for a long-lived host: one opens at once if there is none,
|
|
124
|
+
* and the next before the current one lapses. Openings then run whether or not a lookup waits
|
|
125
|
+
* for them. The timer does not keep the process alive.
|
|
126
|
+
*/
|
|
127
|
+
keep() {
|
|
128
|
+
if (this.#kept)
|
|
129
|
+
return;
|
|
130
|
+
this.#kept = true;
|
|
131
|
+
this.#renew();
|
|
132
|
+
}
|
|
133
|
+
/** Opens the next session if the current one is due, or waits until it is. */
|
|
134
|
+
#renew() {
|
|
135
|
+
if (this.#opening)
|
|
136
|
+
return;
|
|
137
|
+
const age = this.#session ? this.#now() - this.#session.openedAt : Infinity;
|
|
138
|
+
if (age < SESSION_RENEW_MS)
|
|
139
|
+
this.#schedule(SESSION_RENEW_MS - age);
|
|
140
|
+
else
|
|
141
|
+
this.#opening = this.#open();
|
|
142
|
+
}
|
|
143
|
+
#schedule(ms) {
|
|
144
|
+
if (!this.#kept)
|
|
145
|
+
return;
|
|
146
|
+
clearTimeout(this.#timer);
|
|
147
|
+
this.#timer = setTimeout(() => this.#renew(), ms);
|
|
148
|
+
this.#timer.unref?.();
|
|
149
|
+
}
|
|
150
|
+
/** An opening, which a kept service finishes and otherwise ends when no lookup holds it. */
|
|
151
|
+
#open() {
|
|
152
|
+
const controller = new AbortController();
|
|
153
|
+
const holders = new Set();
|
|
154
|
+
const token = this.call('getSessionFullState', { SessionToken: '', DeviceData: {
|
|
155
|
+
HardwareID: this.#device, BootSystemID: 'Android_Phone', Version: '15', AppVersion: '4.2.0',
|
|
156
|
+
} }, controller.signal, SESSION_OPEN_MS).then(result => {
|
|
157
|
+
const value = scalar(one(result, 'SessionFullState'), 'SessionToken', 600);
|
|
158
|
+
if (!/^[A-Za-z0-9+/=]{16,512}$/.test(value))
|
|
159
|
+
invalid();
|
|
160
|
+
this.#session = { token: value, openedAt: this.#now() };
|
|
161
|
+
return value;
|
|
162
|
+
});
|
|
163
|
+
const release = () => {
|
|
164
|
+
for (const holder of holders)
|
|
165
|
+
if (holder.aborted)
|
|
166
|
+
holders.delete(holder);
|
|
167
|
+
if (!holders.size && !this.#kept)
|
|
168
|
+
controller.abort(new Error('No lookup is waiting for the DPD Germany session'));
|
|
169
|
+
};
|
|
170
|
+
const opening = { token, holders, release };
|
|
171
|
+
void token.then(() => this.#schedule(SESSION_RENEW_MS), () => this.#schedule(SESSION_RETRY_MS)).finally(() => {
|
|
172
|
+
if (this.#opening === opening)
|
|
173
|
+
this.#opening = null;
|
|
174
|
+
for (const holder of holders)
|
|
175
|
+
holder.removeEventListener('abort', release);
|
|
176
|
+
});
|
|
177
|
+
return opening;
|
|
111
178
|
}
|
|
112
179
|
/**
|
|
113
180
|
* One session for every lookup. The opening runs while a lookup that asked for it is still
|
|
114
181
|
* running, even one that stopped waiting; each lookup waits until its signal or `waitMs` ends.
|
|
182
|
+
* A session past its renewal age answers at once while the next one opens.
|
|
115
183
|
*/
|
|
116
184
|
async session(signal, waitMs) {
|
|
117
|
-
|
|
118
|
-
|
|
185
|
+
const current = this.#session;
|
|
186
|
+
const due = !current || this.#now() - current.openedAt >= SESSION_RENEW_MS;
|
|
187
|
+
if (current && !due)
|
|
188
|
+
return current.token;
|
|
119
189
|
signal.throwIfAborted();
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const holders = new Set();
|
|
123
|
-
const token = this.call('getSessionFullState', { SessionToken: '', DeviceData: {
|
|
124
|
-
HardwareID: this.#device, BootSystemID: 'Android_Phone', Version: '15', AppVersion: '4.2.0',
|
|
125
|
-
} }, controller.signal, SESSION_OPEN_MS).then(result => {
|
|
126
|
-
const value = scalar(one(result, 'SessionFullState'), 'SessionToken', 600);
|
|
127
|
-
if (!/^[A-Za-z0-9+/=]{16,512}$/.test(value))
|
|
128
|
-
invalid();
|
|
129
|
-
return this.#session = value;
|
|
130
|
-
});
|
|
131
|
-
const release = () => {
|
|
132
|
-
for (const holder of holders)
|
|
133
|
-
if (holder.aborted)
|
|
134
|
-
holders.delete(holder);
|
|
135
|
-
if (!holders.size)
|
|
136
|
-
controller.abort(new Error('No lookup is waiting for the DPD Germany session'));
|
|
137
|
-
};
|
|
138
|
-
const opening = { token, holders, release };
|
|
139
|
-
this.#opening = opening;
|
|
140
|
-
void token.catch(() => undefined).finally(() => {
|
|
141
|
-
if (this.#opening === opening)
|
|
142
|
-
this.#opening = null;
|
|
143
|
-
for (const holder of holders)
|
|
144
|
-
holder.removeEventListener('abort', release);
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
const { token, holders, release } = this.#opening;
|
|
190
|
+
const opening = this.#opening ??= this.#open();
|
|
191
|
+
const { token, holders, release } = opening;
|
|
148
192
|
if (!holders.has(signal)) {
|
|
149
193
|
holders.add(signal);
|
|
150
194
|
signal.addEventListener('abort', release, { once: true });
|
|
151
195
|
}
|
|
196
|
+
if (current)
|
|
197
|
+
return current.token;
|
|
152
198
|
let leave;
|
|
153
199
|
let timer;
|
|
154
200
|
const left = new Promise((_resolve, reject) => {
|
|
@@ -198,3 +244,29 @@ export class DpdAppService {
|
|
|
198
244
|
return result;
|
|
199
245
|
}
|
|
200
246
|
}
|
|
247
|
+
const services = new WeakMap();
|
|
248
|
+
const GLOBAL_FETCH = {};
|
|
249
|
+
/**
|
|
250
|
+
* The process's service for a transport and user agent. DPD Germany and DPD Switzerland share it,
|
|
251
|
+
* and so do the adapters of every registry, so a session opens once for all of them.
|
|
252
|
+
*/
|
|
253
|
+
export function sharedDpdAppService(options = {}) {
|
|
254
|
+
const userAgent = userAgentOf(options.userAgent);
|
|
255
|
+
const key = options.fetcher ?? GLOBAL_FETCH;
|
|
256
|
+
let byAgent = services.get(key);
|
|
257
|
+
if (!byAgent)
|
|
258
|
+
services.set(key, byAgent = new Map());
|
|
259
|
+
let service = byAgent.get(userAgent);
|
|
260
|
+
if (!service)
|
|
261
|
+
byAgent.set(userAgent, service = new DpdAppService({ fetcher: options.fetcher, userAgent }));
|
|
262
|
+
return service;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Opens the session DPD Germany and DPD Switzerland read the German DPD app's service with, in the
|
|
266
|
+
* background, and keeps one open for the life of the process, so that no lookup waits the tens of
|
|
267
|
+
* seconds an opening takes. For a long-lived host, with the `fetcher` and `userAgent` its adapter
|
|
268
|
+
* environment has. The renewal timer does not keep the process alive.
|
|
269
|
+
*/
|
|
270
|
+
export function warmDpdSession(environment = {}) {
|
|
271
|
+
sharedDpdAppService(environment).keep();
|
|
272
|
+
}
|
|
@@ -11,6 +11,36 @@ export function normalizePostnordNumber(raw) {
|
|
|
11
11
|
}
|
|
12
12
|
return number;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* The scan that made the parcel available at its service point, while it waits
|
|
16
|
+
* there or once it is collected there: the delivery, scanned at that same point,
|
|
17
|
+
* came right after it. A parcel taken back out for delivery has a movement between.
|
|
18
|
+
*/
|
|
19
|
+
function atPickupPoint(stage, events) {
|
|
20
|
+
if (stage === 'ready_for_pickup')
|
|
21
|
+
return events.find((event) => event.stage === 'ready_for_pickup');
|
|
22
|
+
if (stage !== 'delivered')
|
|
23
|
+
return undefined;
|
|
24
|
+
const index = events.findIndex((event) => event.stage !== 'delivered');
|
|
25
|
+
const ready = events[index];
|
|
26
|
+
return index > 0 && ready?.stage === 'ready_for_pickup' && ready.location
|
|
27
|
+
&& events.slice(0, index).every((event) => event.location === ready.location) ? ready : undefined;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The service point's name, then its street and "postcode town", from the point
|
|
31
|
+
* record the tracking widget shows under a parcel ready for pickup. Without that
|
|
32
|
+
* record the scan's name stands alone. The receiver's address is a separate record.
|
|
33
|
+
*/
|
|
34
|
+
function pickupPoint(raw, scanned) {
|
|
35
|
+
const point = isRecord(raw) ? raw : {};
|
|
36
|
+
const address = isRecord(point.address) ? point.address : {};
|
|
37
|
+
const name = clean(point.name, 160);
|
|
38
|
+
if (!name)
|
|
39
|
+
return scanned;
|
|
40
|
+
const street = clean(address.street, 120);
|
|
41
|
+
const town = clean(address.city, 80);
|
|
42
|
+
return street && town ? [name, street, [clean(address.postalCode, 16), town].filter(Boolean).join(' ')].join('\n') : name;
|
|
43
|
+
}
|
|
14
44
|
export function parsePostnord(payload, number) {
|
|
15
45
|
const requested = normalizePostnordNumber(number);
|
|
16
46
|
if (!isRecord(payload) || payload.shipmentId !== requested || payload.actualReturnedId !== requested
|
|
@@ -69,8 +99,8 @@ export function parsePostnord(payload, number) {
|
|
|
69
99
|
// The portal names the sender, usually the shop; of the receiver's address only the country is read.
|
|
70
100
|
const sender = isRecord(payload.sender) ? clean(payload.sender.name, 200) : '';
|
|
71
101
|
const country = isRecord(payload.receiver) && isRecord(payload.receiver.address) ? payload.receiver.address.countryCode : undefined;
|
|
72
|
-
|
|
73
|
-
const pickup =
|
|
102
|
+
const pickupScan = atPickupPoint(mapped?.stage, events);
|
|
103
|
+
const pickup = pickupScan ? pickupPoint(payload.servicePoint, pickupScan.location ?? '') : '';
|
|
74
104
|
return { status: mapped?.status ?? 'unknown', ...(mapped ? { current_stage: mapped.stage } : {}),
|
|
75
105
|
last_status_text: header || events[0].description, last_update: events[0].time,
|
|
76
106
|
expected_delivery: null, ...(delivery?.time ? { delivered_at: delivery.time } : {}),
|
package/dist/node.d.ts
CHANGED
|
@@ -8,4 +8,5 @@ export { REGISTRY } from './generated/registry.js';
|
|
|
8
8
|
export { UniversalTracker, UniversalTrackingError } from './providers/universal.js';
|
|
9
9
|
export { TrackingCaptureError, SeventeenTrackLookupError, SeventeenTrackNoHistoryError, SeventeenTrackVerificationError } from './providers/universal.js';
|
|
10
10
|
export { amazonShippingEligibility } from './carriers/amazon-shipping/eligibility.js';
|
|
11
|
+
export { warmDpdSession } from './carriers/dpd-de/service.js';
|
|
11
12
|
export { createTrackingServer, type TrackingServerOptions } from './server/index.js';
|
package/dist/node.js
CHANGED
|
@@ -8,4 +8,5 @@ export { REGISTRY } from './generated/registry.js';
|
|
|
8
8
|
export { UniversalTracker, UniversalTrackingError } from './providers/universal.js';
|
|
9
9
|
export { TrackingCaptureError, SeventeenTrackLookupError, SeventeenTrackNoHistoryError, SeventeenTrackVerificationError } from './providers/universal.js';
|
|
10
10
|
export { amazonShippingEligibility } from './carriers/amazon-shipping/eligibility.js';
|
|
11
|
+
export { warmDpdSession } from './carriers/dpd-de/service.js';
|
|
11
12
|
export { createTrackingServer } from './server/index.js';
|
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.
|
|
3
|
+
"version": "0.50.0-main.568",
|
|
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",
|