@jazzdev/dpd-local-sdk 1.0.0
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/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +442 -0
- package/dist/index.d.mts +525 -0
- package/dist/index.d.ts +525 -0
- package/dist/index.js +1195 -0
- package/dist/index.mjs +1106 -0
- package/package.json +60 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1106 @@
|
|
|
1
|
+
// src/config/index.ts
|
|
2
|
+
var DPD_API = {
|
|
3
|
+
BASE_URL: "https://api.dpdlocal.co.uk",
|
|
4
|
+
ENDPOINTS: {
|
|
5
|
+
AUTH: "/user/?action=login",
|
|
6
|
+
SHIPMENT: "/shipping/shipment",
|
|
7
|
+
LABEL: "/shipping/shipment",
|
|
8
|
+
// Will append /{shipmentId}/label/
|
|
9
|
+
TRACKING: "/shipping/network/"
|
|
10
|
+
// Note: Address validation uses postcodes.io API instead of DPD
|
|
11
|
+
},
|
|
12
|
+
TIMEOUT: 3e4,
|
|
13
|
+
// 30 seconds
|
|
14
|
+
RETRY_ATTEMPTS: 3,
|
|
15
|
+
RETRY_DELAY: 1e3
|
|
16
|
+
// 1 second
|
|
17
|
+
};
|
|
18
|
+
var SERVICE_NAMES = {
|
|
19
|
+
"12": "Next Day Delivery",
|
|
20
|
+
"07": "By 12 PM Delivery"
|
|
21
|
+
};
|
|
22
|
+
var SERVICE_DESCRIPTIONS = {
|
|
23
|
+
"12": "Standard next day delivery - most affordable option",
|
|
24
|
+
"07": "Premium delivery by 12 PM the next day"
|
|
25
|
+
};
|
|
26
|
+
function createDPDConfig(options) {
|
|
27
|
+
const defaultPricing = {
|
|
28
|
+
freeDeliveryThreshold: 60,
|
|
29
|
+
// £60
|
|
30
|
+
flatDeliveryFee: 6,
|
|
31
|
+
// £6.00
|
|
32
|
+
minimumOrderValue: 25,
|
|
33
|
+
// £25
|
|
34
|
+
services: {
|
|
35
|
+
"12": {
|
|
36
|
+
// Next Day
|
|
37
|
+
basePrice: 6,
|
|
38
|
+
perKgPrice: 0.3,
|
|
39
|
+
customerPrice: 6
|
|
40
|
+
},
|
|
41
|
+
"07": {
|
|
42
|
+
// By 12
|
|
43
|
+
basePrice: 7,
|
|
44
|
+
perKgPrice: 0.42,
|
|
45
|
+
customerPrice: 7
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const defaultServices = {
|
|
50
|
+
enabled: ["12", "07"],
|
|
51
|
+
default: "12"
|
|
52
|
+
};
|
|
53
|
+
const defaultLabels = {
|
|
54
|
+
format: "thermal",
|
|
55
|
+
printer: {
|
|
56
|
+
model: "TSC-DA210",
|
|
57
|
+
dpi: 203,
|
|
58
|
+
speed: 6,
|
|
59
|
+
connection: "USB"
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const defaultNotifications = {
|
|
63
|
+
email: {
|
|
64
|
+
enabled: true,
|
|
65
|
+
provider: "resend",
|
|
66
|
+
fromEmail: options.notifications?.email?.fromEmail || "",
|
|
67
|
+
adminEmail: options.notifications?.email?.adminEmail || ""
|
|
68
|
+
},
|
|
69
|
+
sms: {
|
|
70
|
+
enabled: true,
|
|
71
|
+
provider: "dpd"
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
credentials: options.credentials,
|
|
76
|
+
business: options.business,
|
|
77
|
+
services: {
|
|
78
|
+
...defaultServices,
|
|
79
|
+
...options.services
|
|
80
|
+
},
|
|
81
|
+
pricing: {
|
|
82
|
+
...defaultPricing,
|
|
83
|
+
...options.pricing,
|
|
84
|
+
services: {
|
|
85
|
+
...defaultPricing.services,
|
|
86
|
+
...options.pricing?.services
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
labels: {
|
|
90
|
+
...defaultLabels,
|
|
91
|
+
...options.labels
|
|
92
|
+
},
|
|
93
|
+
notifications: {
|
|
94
|
+
...defaultNotifications,
|
|
95
|
+
...options.notifications
|
|
96
|
+
},
|
|
97
|
+
testMode: options.testMode ?? process.env.NODE_ENV !== "production"
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
var calculateDeliveryFee = (subtotal, service = "12", config2) => {
|
|
101
|
+
if (subtotal >= config2.pricing.freeDeliveryThreshold) {
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
return config2.pricing.services[service]?.customerPrice || config2.pricing.flatDeliveryFee;
|
|
105
|
+
};
|
|
106
|
+
var calculateDPDCost = (weight, service, config2) => {
|
|
107
|
+
const serviceConfig = config2.pricing.services[service];
|
|
108
|
+
if (!serviceConfig) {
|
|
109
|
+
throw new Error(`Invalid service code: ${service}`);
|
|
110
|
+
}
|
|
111
|
+
const basePrice = serviceConfig.basePrice;
|
|
112
|
+
const weightCharge = weight * serviceConfig.perKgPrice;
|
|
113
|
+
return basePrice + weightCharge;
|
|
114
|
+
};
|
|
115
|
+
var qualifiesForFreeDelivery = (subtotal, config2) => {
|
|
116
|
+
return subtotal >= config2.pricing.freeDeliveryThreshold;
|
|
117
|
+
};
|
|
118
|
+
var meetsMinimumOrderValue = (subtotal, config2) => {
|
|
119
|
+
return subtotal >= config2.pricing.minimumOrderValue;
|
|
120
|
+
};
|
|
121
|
+
var getNextCollectionDate = () => {
|
|
122
|
+
const now = /* @__PURE__ */ new Date();
|
|
123
|
+
const collectionDate = new Date(now);
|
|
124
|
+
if (collectionDate.getDay() === 0) {
|
|
125
|
+
collectionDate.setDate(collectionDate.getDate() + 1);
|
|
126
|
+
}
|
|
127
|
+
return collectionDate.toISOString().split("T")[0];
|
|
128
|
+
};
|
|
129
|
+
var getEstimatedDeliveryDate = (_service, collectionDate) => {
|
|
130
|
+
const collection = collectionDate ? new Date(collectionDate) : /* @__PURE__ */ new Date();
|
|
131
|
+
const delivery = new Date(collection);
|
|
132
|
+
delivery.setDate(delivery.getDate() + 1);
|
|
133
|
+
if (delivery.getDay() === 0) {
|
|
134
|
+
delivery.setDate(delivery.getDate() + 1);
|
|
135
|
+
}
|
|
136
|
+
return delivery.toISOString().split("T")[0];
|
|
137
|
+
};
|
|
138
|
+
var getTrackingUrl = (parcelNumber) => {
|
|
139
|
+
return `https://track.dpdlocal.co.uk/?parcelNumber=${parcelNumber}`;
|
|
140
|
+
};
|
|
141
|
+
var isValidServiceCode = (code, config2) => {
|
|
142
|
+
return config2.services.enabled.includes(code);
|
|
143
|
+
};
|
|
144
|
+
var getServiceName = (code) => {
|
|
145
|
+
return SERVICE_NAMES[code] || code;
|
|
146
|
+
};
|
|
147
|
+
var getServiceDescription = (code) => {
|
|
148
|
+
return SERVICE_DESCRIPTIONS[code] || "";
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// src/lib/auth.ts
|
|
152
|
+
var tokenCache = {
|
|
153
|
+
geoSession: null,
|
|
154
|
+
expiry: null
|
|
155
|
+
};
|
|
156
|
+
async function authenticate(credentials) {
|
|
157
|
+
const { username, password } = credentials;
|
|
158
|
+
const authHeader = Buffer.from(`${username}:${password}`).toString("base64");
|
|
159
|
+
const response = await fetch(`${DPD_API.BASE_URL}${DPD_API.ENDPOINTS.AUTH}`, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: {
|
|
162
|
+
"Content-Type": "application/json",
|
|
163
|
+
Accept: "application/json",
|
|
164
|
+
Authorization: `Basic ${authHeader}`
|
|
165
|
+
},
|
|
166
|
+
signal: AbortSignal.timeout(DPD_API.TIMEOUT)
|
|
167
|
+
});
|
|
168
|
+
const raw = await response.text();
|
|
169
|
+
let payload;
|
|
170
|
+
try {
|
|
171
|
+
payload = raw ? JSON.parse(raw) : null;
|
|
172
|
+
} catch {
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
throw new Error(`Authentication failed: ${raw || response.statusText}`);
|
|
175
|
+
}
|
|
176
|
+
throw new Error("Invalid JSON response from DPD API");
|
|
177
|
+
}
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
payload?.error?.errorMessage ?? `Authentication failed: ${response.status} ${response.statusText}`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (payload?.error) {
|
|
184
|
+
throw new Error(payload.error.errorMessage ?? "Authentication failed");
|
|
185
|
+
}
|
|
186
|
+
const geoSession = payload?.data?.geoSession;
|
|
187
|
+
if (!geoSession) {
|
|
188
|
+
throw new Error("No GeoSession token received from DPD");
|
|
189
|
+
}
|
|
190
|
+
tokenCache.geoSession = geoSession;
|
|
191
|
+
tokenCache.expiry = new Date(Date.now() + 90 * 60 * 1e3);
|
|
192
|
+
return geoSession;
|
|
193
|
+
}
|
|
194
|
+
async function getGeoSession(credentials, forceRefresh = false) {
|
|
195
|
+
if (!forceRefresh && tokenCache.geoSession && tokenCache.expiry && tokenCache.expiry > /* @__PURE__ */ new Date()) {
|
|
196
|
+
return tokenCache.geoSession;
|
|
197
|
+
}
|
|
198
|
+
return await authenticate(credentials);
|
|
199
|
+
}
|
|
200
|
+
function clearGeoSession() {
|
|
201
|
+
tokenCache.geoSession = null;
|
|
202
|
+
tokenCache.expiry = null;
|
|
203
|
+
}
|
|
204
|
+
function hasValidToken() {
|
|
205
|
+
return !!(tokenCache.geoSession && tokenCache.expiry && tokenCache.expiry > /* @__PURE__ */ new Date());
|
|
206
|
+
}
|
|
207
|
+
function getTokenExpiry() {
|
|
208
|
+
return tokenCache.expiry;
|
|
209
|
+
}
|
|
210
|
+
async function authenticatedRequest(credentials, options) {
|
|
211
|
+
const {
|
|
212
|
+
method,
|
|
213
|
+
endpoint,
|
|
214
|
+
body,
|
|
215
|
+
headers = {},
|
|
216
|
+
retry = true,
|
|
217
|
+
retryAttempts = DPD_API.RETRY_ATTEMPTS
|
|
218
|
+
} = options;
|
|
219
|
+
let lastError = null;
|
|
220
|
+
for (let attempt = 0; attempt < retryAttempts; attempt++) {
|
|
221
|
+
try {
|
|
222
|
+
const geoSession = await getGeoSession(credentials, attempt > 0);
|
|
223
|
+
const url = endpoint.startsWith("http") ? endpoint : `${DPD_API.BASE_URL}${endpoint}`;
|
|
224
|
+
const response = await fetch(url, {
|
|
225
|
+
method,
|
|
226
|
+
headers: {
|
|
227
|
+
"Content-Type": "application/json",
|
|
228
|
+
GeoSession: geoSession,
|
|
229
|
+
...headers
|
|
230
|
+
},
|
|
231
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
232
|
+
signal: AbortSignal.timeout(DPD_API.TIMEOUT)
|
|
233
|
+
});
|
|
234
|
+
const raw = await response.text();
|
|
235
|
+
const acceptHeader = headers.Accept || headers.accept;
|
|
236
|
+
const isLabelRequest = acceptHeader === "text/vnd.citizen-clp" || acceptHeader === "text/vnd.eltron-epl" || acceptHeader === "text/html";
|
|
237
|
+
let data = null;
|
|
238
|
+
if (isLabelRequest) {
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Label request failed: ${response.status} ${response.statusText}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return raw;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
data = raw ? JSON.parse(raw) : null;
|
|
248
|
+
} catch {
|
|
249
|
+
if (!response.ok) {
|
|
250
|
+
throw new Error(raw || response.statusText);
|
|
251
|
+
}
|
|
252
|
+
throw new Error("Invalid JSON response from DPD API");
|
|
253
|
+
}
|
|
254
|
+
const hasData = data?.data && Object.keys(data.data).length > 0;
|
|
255
|
+
const hasError = data?.error;
|
|
256
|
+
if (!response.ok || hasError && !hasData) {
|
|
257
|
+
if (hasError) {
|
|
258
|
+
console.error(`
|
|
259
|
+
\u274C DPD API Error Response:`);
|
|
260
|
+
console.error(
|
|
261
|
+
` HTTP Status: ${response.status} ${response.statusText}`
|
|
262
|
+
);
|
|
263
|
+
console.error(
|
|
264
|
+
` Error Object:`,
|
|
265
|
+
JSON.stringify(data.error, null, 2)
|
|
266
|
+
);
|
|
267
|
+
console.error(` Full Response:`, JSON.stringify(data, null, 2));
|
|
268
|
+
}
|
|
269
|
+
if (response.status === 401 && retry && attempt < retryAttempts - 1) {
|
|
270
|
+
clearGeoSession();
|
|
271
|
+
await delay(DPD_API.RETRY_DELAY * (attempt + 1));
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
throw new Error(
|
|
275
|
+
data?.error?.errorMessage ?? data?.error?.errorAction ?? data?.error?.obj ?? JSON.stringify(data?.error) ?? `Request failed: ${response.statusText}`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
if (hasError && hasData) {
|
|
279
|
+
console.warn(`
|
|
280
|
+
\u26A0\uFE0F DPD API Warning (data was still returned):`);
|
|
281
|
+
console.warn(` Warning:`, JSON.stringify(data.error, null, 2));
|
|
282
|
+
console.warn(` Data returned successfully despite warning`);
|
|
283
|
+
}
|
|
284
|
+
return data;
|
|
285
|
+
} catch (error) {
|
|
286
|
+
lastError = error instanceof Error ? error : new Error("Unknown error");
|
|
287
|
+
if (!retry || attempt >= retryAttempts - 1) {
|
|
288
|
+
throw lastError;
|
|
289
|
+
}
|
|
290
|
+
await delay(DPD_API.RETRY_DELAY * (attempt + 1));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
throw lastError ?? new Error("Request failed after retries");
|
|
294
|
+
}
|
|
295
|
+
function delay(ms) {
|
|
296
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
297
|
+
}
|
|
298
|
+
async function testConnection(credentials) {
|
|
299
|
+
try {
|
|
300
|
+
clearGeoSession();
|
|
301
|
+
const geoSession = await authenticate(credentials);
|
|
302
|
+
if (!geoSession) {
|
|
303
|
+
return {
|
|
304
|
+
success: false,
|
|
305
|
+
message: "Authentication succeeded but no token received"
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
success: true,
|
|
310
|
+
message: "Successfully connected to DPD API"
|
|
311
|
+
};
|
|
312
|
+
} catch (error) {
|
|
313
|
+
return {
|
|
314
|
+
success: false,
|
|
315
|
+
message: error instanceof Error ? error.message : "Failed to connect to DPD API"
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/lib/shipment.ts
|
|
321
|
+
async function createShipment(credentials, params, businessConfig) {
|
|
322
|
+
try {
|
|
323
|
+
const {
|
|
324
|
+
orderId,
|
|
325
|
+
orderRef,
|
|
326
|
+
service,
|
|
327
|
+
deliveryAddress,
|
|
328
|
+
totalWeight,
|
|
329
|
+
numberOfParcels,
|
|
330
|
+
customerEmail,
|
|
331
|
+
customerPhone,
|
|
332
|
+
deliveryInstructions,
|
|
333
|
+
collectionDate
|
|
334
|
+
} = params;
|
|
335
|
+
const weightPerParcel = totalWeight / numberOfParcels;
|
|
336
|
+
const parcels = Array.from(
|
|
337
|
+
{ length: numberOfParcels },
|
|
338
|
+
() => ({
|
|
339
|
+
weight: parseFloat(weightPerParcel.toFixed(2))
|
|
340
|
+
})
|
|
341
|
+
);
|
|
342
|
+
const consignment = {
|
|
343
|
+
consignmentNumber: null,
|
|
344
|
+
consignmentRef: orderRef,
|
|
345
|
+
parcel: parcels,
|
|
346
|
+
collectionDetails: {
|
|
347
|
+
address: businessConfig.collectionAddress,
|
|
348
|
+
contactDetails: {
|
|
349
|
+
name: businessConfig.contactName,
|
|
350
|
+
telephone: businessConfig.contactPhone,
|
|
351
|
+
email: businessConfig.contactEmail
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
deliveryDetails: {
|
|
355
|
+
address: {
|
|
356
|
+
organisation: deliveryAddress.organisation || "",
|
|
357
|
+
property: deliveryAddress.property,
|
|
358
|
+
street: deliveryAddress.street,
|
|
359
|
+
locality: deliveryAddress.locality || "",
|
|
360
|
+
town: deliveryAddress.town,
|
|
361
|
+
county: deliveryAddress.county || "",
|
|
362
|
+
postcode: deliveryAddress.postcode,
|
|
363
|
+
countryCode: deliveryAddress.countryCode
|
|
364
|
+
},
|
|
365
|
+
contactDetails: {
|
|
366
|
+
name: deliveryAddress.contactName,
|
|
367
|
+
telephone: deliveryAddress.contactPhone,
|
|
368
|
+
email: customerEmail
|
|
369
|
+
},
|
|
370
|
+
notificationDetails: {
|
|
371
|
+
email: customerEmail,
|
|
372
|
+
mobile: customerPhone
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
networkCode: service,
|
|
376
|
+
numberOfParcels,
|
|
377
|
+
totalWeight: parseFloat(totalWeight.toFixed(2)),
|
|
378
|
+
shippingRef1: orderId,
|
|
379
|
+
shippingRef2: orderRef,
|
|
380
|
+
shippingRef3: `FJ-${Date.now()}`,
|
|
381
|
+
// Unique reference
|
|
382
|
+
deliveryInstructions: deliveryInstructions || void 0,
|
|
383
|
+
liability: false
|
|
384
|
+
};
|
|
385
|
+
const shipmentRequest = {
|
|
386
|
+
jobId: null,
|
|
387
|
+
collectionOnDelivery: false,
|
|
388
|
+
invoice: null,
|
|
389
|
+
collectionDate: collectionDate || getNextCollectionDate(),
|
|
390
|
+
consolidate: false,
|
|
391
|
+
consignment: [consignment]
|
|
392
|
+
};
|
|
393
|
+
const response = await authenticatedRequest(
|
|
394
|
+
credentials,
|
|
395
|
+
{
|
|
396
|
+
method: "POST",
|
|
397
|
+
endpoint: DPD_API.ENDPOINTS.SHIPMENT,
|
|
398
|
+
body: shipmentRequest
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
const responseData = response.data;
|
|
402
|
+
const shipmentId = responseData?.shipmentId;
|
|
403
|
+
const consignmentNumber = responseData?.consignment?.[0]?.consignmentNumber || responseData?.consignmentDetail?.[0]?.consignmentNumber;
|
|
404
|
+
const parcelNumbers = responseData?.consignment?.[0]?.parcelNumbers || responseData?.consignmentDetail?.[0]?.parcelNumbers;
|
|
405
|
+
const parcelNumber = parcelNumbers?.[0];
|
|
406
|
+
if (!consignmentNumber || !shipmentId || !parcelNumber) {
|
|
407
|
+
return {
|
|
408
|
+
success: false,
|
|
409
|
+
error: `Missing required data: ${!shipmentId ? "shipmentId" : !consignmentNumber ? "consignmentNumber" : "parcelNumber"}`,
|
|
410
|
+
errorCode: "INCOMPLETE_RESPONSE"
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
const trackingUrl = getTrackingUrl(parcelNumber);
|
|
414
|
+
return {
|
|
415
|
+
success: true,
|
|
416
|
+
shipmentId,
|
|
417
|
+
consignmentNumber,
|
|
418
|
+
parcelNumber,
|
|
419
|
+
trackingUrl
|
|
420
|
+
};
|
|
421
|
+
} catch (error) {
|
|
422
|
+
return {
|
|
423
|
+
success: false,
|
|
424
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
425
|
+
errorCode: "SHIPMENT_CREATION_FAILED"
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async function generateLabel(credentials, params) {
|
|
430
|
+
try {
|
|
431
|
+
const { shipmentId, format } = params;
|
|
432
|
+
const contentType = format === "thermal" ? "text/vnd.citizen-clp" : "text/html";
|
|
433
|
+
const endpoint = `${DPD_API.ENDPOINTS.LABEL}/${shipmentId}/label/`;
|
|
434
|
+
const response = await authenticatedRequest(credentials, {
|
|
435
|
+
method: "GET",
|
|
436
|
+
endpoint,
|
|
437
|
+
headers: {
|
|
438
|
+
Accept: contentType
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
if (!response || typeof response === "object" && !response.data) {
|
|
442
|
+
return {
|
|
443
|
+
success: false,
|
|
444
|
+
error: "No label data received from DPD"
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
success: true,
|
|
449
|
+
labelData: typeof response === "string" ? response : response.data
|
|
450
|
+
};
|
|
451
|
+
} catch (error) {
|
|
452
|
+
return {
|
|
453
|
+
success: false,
|
|
454
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async function validateAddress(_credentials, params) {
|
|
459
|
+
try {
|
|
460
|
+
const { postcode, town } = params;
|
|
461
|
+
const cleanPostcode = postcode.replace(/\s/g, "").toUpperCase();
|
|
462
|
+
const postcodeRegex = /^[A-Z]{1,2}\d{1,2}[A-Z]?\d[A-Z]{2}$/;
|
|
463
|
+
if (!postcodeRegex.test(cleanPostcode)) {
|
|
464
|
+
return {
|
|
465
|
+
valid: false,
|
|
466
|
+
serviceable: false,
|
|
467
|
+
message: "Invalid UK postcode format"
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
const response = await fetch(
|
|
471
|
+
`https://api.postcodes.io/postcodes/${encodeURIComponent(cleanPostcode)}`
|
|
472
|
+
);
|
|
473
|
+
if (!response.ok) {
|
|
474
|
+
if (response.status === 404) {
|
|
475
|
+
return {
|
|
476
|
+
valid: false,
|
|
477
|
+
serviceable: false,
|
|
478
|
+
message: "Postcode not found in UK database"
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
throw new Error(`Postcode lookup failed: ${response.statusText}`);
|
|
482
|
+
}
|
|
483
|
+
const data = await response.json();
|
|
484
|
+
if (!data.result) {
|
|
485
|
+
return {
|
|
486
|
+
valid: false,
|
|
487
|
+
serviceable: false,
|
|
488
|
+
message: "Invalid postcode"
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const country = data.result.country;
|
|
492
|
+
const serviceable = ["England", "Wales", "Scotland"].includes(country);
|
|
493
|
+
const returnedTown = data.result.admin_district || data.result.parish || "";
|
|
494
|
+
const townMatch = town ? returnedTown.toLowerCase().includes(town.toLowerCase()) || town.toLowerCase().includes(returnedTown.toLowerCase()) : true;
|
|
495
|
+
if (!townMatch) {
|
|
496
|
+
return {
|
|
497
|
+
valid: true,
|
|
498
|
+
serviceable,
|
|
499
|
+
message: `Postcode valid but town mismatch. Expected: ${returnedTown}`
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
valid: true,
|
|
504
|
+
serviceable,
|
|
505
|
+
message: serviceable ? "Address is valid and serviceable by DPD" : `Address is valid but may not be serviceable by DPD (${country})`
|
|
506
|
+
};
|
|
507
|
+
} catch (error) {
|
|
508
|
+
return {
|
|
509
|
+
valid: false,
|
|
510
|
+
serviceable: false,
|
|
511
|
+
message: error instanceof Error ? error.message : "Address validation failed"
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async function trackShipment(credentials, params) {
|
|
516
|
+
try {
|
|
517
|
+
const { consignmentNumber } = params;
|
|
518
|
+
const response = await authenticatedRequest(
|
|
519
|
+
credentials,
|
|
520
|
+
{
|
|
521
|
+
method: "GET",
|
|
522
|
+
endpoint: `${DPD_API.ENDPOINTS.TRACKING}${consignmentNumber}`
|
|
523
|
+
}
|
|
524
|
+
);
|
|
525
|
+
if (!response.data) {
|
|
526
|
+
return {
|
|
527
|
+
success: false,
|
|
528
|
+
error: "No tracking data available"
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
const status = mapDPDStatus(response.data.status ?? "UNKNOWN");
|
|
532
|
+
const statusHistory = response.data.history ? response.data.history.map(
|
|
533
|
+
(event) => ({
|
|
534
|
+
status: mapDPDStatus(event.status),
|
|
535
|
+
timestamp: new Date(event.timestamp).toISOString(),
|
|
536
|
+
message: event.message,
|
|
537
|
+
location: event.location
|
|
538
|
+
})
|
|
539
|
+
) : void 0;
|
|
540
|
+
return {
|
|
541
|
+
success: true,
|
|
542
|
+
status,
|
|
543
|
+
statusHistory,
|
|
544
|
+
estimatedDelivery: response.data.estimatedDelivery,
|
|
545
|
+
actualDelivery: response.data.actualDelivery
|
|
546
|
+
};
|
|
547
|
+
} catch (error) {
|
|
548
|
+
return {
|
|
549
|
+
success: false,
|
|
550
|
+
error: error instanceof Error ? error.message : "Tracking failed"
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function mapDPDStatus(dpdStatus) {
|
|
555
|
+
const statusMap = {
|
|
556
|
+
CREATED: "created",
|
|
557
|
+
"LABEL GENERATED": "label_generated",
|
|
558
|
+
COLLECTED: "collected",
|
|
559
|
+
"IN TRANSIT": "in_transit",
|
|
560
|
+
"OUT FOR DELIVERY": "out_for_delivery",
|
|
561
|
+
DELIVERED: "delivered",
|
|
562
|
+
FAILED: "failed",
|
|
563
|
+
CANCELLED: "cancelled"
|
|
564
|
+
};
|
|
565
|
+
return statusMap[dpdStatus.toUpperCase()] || "created";
|
|
566
|
+
}
|
|
567
|
+
function calculateParcels(totalWeight) {
|
|
568
|
+
const MAX_PARCEL_WEIGHT = 30;
|
|
569
|
+
if (totalWeight <= MAX_PARCEL_WEIGHT) {
|
|
570
|
+
return 1;
|
|
571
|
+
}
|
|
572
|
+
return Math.ceil(totalWeight / MAX_PARCEL_WEIGHT);
|
|
573
|
+
}
|
|
574
|
+
function validateServiceCode(code) {
|
|
575
|
+
return code === "12" || code === "07";
|
|
576
|
+
}
|
|
577
|
+
function generateConsignmentRef(orderId) {
|
|
578
|
+
const timestamp = Date.now().toString(36).toUpperCase();
|
|
579
|
+
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
|
|
580
|
+
return `FJ-${orderId}-${timestamp}${random}`;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/lib/dpd-service.ts
|
|
584
|
+
async function createCompleteShipment(orderId, params, config2, dbAdapter, storageAdapter) {
|
|
585
|
+
try {
|
|
586
|
+
const shipmentResult = await createShipment(
|
|
587
|
+
config2.credentials,
|
|
588
|
+
{ orderId, ...params },
|
|
589
|
+
config2.business
|
|
590
|
+
);
|
|
591
|
+
if (!shipmentResult.success || !shipmentResult.consignmentNumber || !shipmentResult.shipmentId || !shipmentResult.parcelNumber) {
|
|
592
|
+
return shipmentResult;
|
|
593
|
+
}
|
|
594
|
+
const labelResult = await generateAndUploadLabel(
|
|
595
|
+
shipmentResult.shipmentId,
|
|
596
|
+
shipmentResult.consignmentNumber,
|
|
597
|
+
"thermal",
|
|
598
|
+
config2.credentials,
|
|
599
|
+
storageAdapter
|
|
600
|
+
);
|
|
601
|
+
if (!labelResult.success || !labelResult.labelUrl) {
|
|
602
|
+
return {
|
|
603
|
+
...shipmentResult,
|
|
604
|
+
error: `Shipment created but label generation failed: ${labelResult.error}`
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
const dpdCost = calculateDPDCost(params.totalWeight, params.service, config2);
|
|
608
|
+
const customerCharge = calculateDeliveryFee(0, params.service, config2);
|
|
609
|
+
const now = /* @__PURE__ */ new Date();
|
|
610
|
+
const shippingData = {
|
|
611
|
+
provider: "dpd",
|
|
612
|
+
service: params.service,
|
|
613
|
+
shipmentId: shipmentResult.shipmentId,
|
|
614
|
+
consignmentNumber: shipmentResult.consignmentNumber,
|
|
615
|
+
parcelNumber: shipmentResult.parcelNumber,
|
|
616
|
+
trackingUrl: `https://www.dpdlocal.co.uk/service/tracking?parcel=${shipmentResult.parcelNumber}`,
|
|
617
|
+
labelUrl: labelResult.labelUrl,
|
|
618
|
+
status: "label_generated",
|
|
619
|
+
statusHistory: [
|
|
620
|
+
{
|
|
621
|
+
status: "created",
|
|
622
|
+
timestamp: now,
|
|
623
|
+
message: "Shipment created with DPD"
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
status: "label_generated",
|
|
627
|
+
timestamp: now,
|
|
628
|
+
message: "Shipping label generated"
|
|
629
|
+
}
|
|
630
|
+
],
|
|
631
|
+
cost: {
|
|
632
|
+
basePrice: dpdCost,
|
|
633
|
+
weightCharge: params.totalWeight * 0.3,
|
|
634
|
+
totalCost: dpdCost,
|
|
635
|
+
customerCharge
|
|
636
|
+
},
|
|
637
|
+
weight: {
|
|
638
|
+
total: params.totalWeight,
|
|
639
|
+
unit: "kg"
|
|
640
|
+
},
|
|
641
|
+
parcels: params.numberOfParcels,
|
|
642
|
+
collectionDate: params.collectionDate || getNextCollectionDate(),
|
|
643
|
+
estimatedDelivery: getEstimatedDeliveryDate(
|
|
644
|
+
params.service,
|
|
645
|
+
params.collectionDate
|
|
646
|
+
),
|
|
647
|
+
createdAt: now,
|
|
648
|
+
updatedAt: now
|
|
649
|
+
};
|
|
650
|
+
try {
|
|
651
|
+
await dbAdapter.updateOrder(orderId, {
|
|
652
|
+
shipping: shippingData
|
|
653
|
+
});
|
|
654
|
+
} catch (updateError) {
|
|
655
|
+
console.error(
|
|
656
|
+
`\u274C CRITICAL: Failed to update order ${orderId} with shipping data!`
|
|
657
|
+
);
|
|
658
|
+
console.error(` Error:`, updateError);
|
|
659
|
+
throw new Error(
|
|
660
|
+
`Shipment created but failed to update order: ${updateError instanceof Error ? updateError.message : "Unknown error"}`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
return {
|
|
664
|
+
success: true,
|
|
665
|
+
consignmentNumber: shipmentResult.consignmentNumber,
|
|
666
|
+
trackingUrl: shipmentResult.trackingUrl,
|
|
667
|
+
labelUrl: labelResult.labelUrl
|
|
668
|
+
};
|
|
669
|
+
} catch (error) {
|
|
670
|
+
console.error(`
|
|
671
|
+
\u{1F4A5} Complete shipment creation failed:`, error);
|
|
672
|
+
return {
|
|
673
|
+
success: false,
|
|
674
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
675
|
+
errorCode: "COMPLETE_SHIPMENT_FAILED"
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async function generateAndUploadLabel(shipmentId, consignmentNumber, format = "thermal", credentials, storageAdapter) {
|
|
680
|
+
try {
|
|
681
|
+
const labelResult = await generateLabel(credentials, {
|
|
682
|
+
shipmentId,
|
|
683
|
+
format
|
|
684
|
+
});
|
|
685
|
+
if (!labelResult.success || !labelResult.labelData) {
|
|
686
|
+
return labelResult;
|
|
687
|
+
}
|
|
688
|
+
const timestamp = Date.now();
|
|
689
|
+
const extension = format === "thermal" ? "txt" : "html";
|
|
690
|
+
const fileName = `${consignmentNumber}-${timestamp}.${extension}`;
|
|
691
|
+
const labelUrl = await storageAdapter.uploadLabel(
|
|
692
|
+
labelResult.labelData,
|
|
693
|
+
fileName
|
|
694
|
+
);
|
|
695
|
+
return {
|
|
696
|
+
success: true,
|
|
697
|
+
labelUrl,
|
|
698
|
+
labelData: labelResult.labelData
|
|
699
|
+
};
|
|
700
|
+
} catch (error) {
|
|
701
|
+
return {
|
|
702
|
+
success: false,
|
|
703
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
async function validateDeliveryAddress(params, credentials) {
|
|
708
|
+
return await validateAddress(credentials, params);
|
|
709
|
+
}
|
|
710
|
+
async function saveAddress(userId, address, credentials, dbAdapter) {
|
|
711
|
+
const validation = await validateAddress(credentials, {
|
|
712
|
+
postcode: address.postcode,
|
|
713
|
+
town: address.town
|
|
714
|
+
});
|
|
715
|
+
const now = /* @__PURE__ */ new Date();
|
|
716
|
+
const addressData = {
|
|
717
|
+
...address,
|
|
718
|
+
userId,
|
|
719
|
+
validated: validation.valid,
|
|
720
|
+
validatedAt: validation.valid ? now : void 0,
|
|
721
|
+
createdAt: now,
|
|
722
|
+
updatedAt: now
|
|
723
|
+
};
|
|
724
|
+
return await dbAdapter.createSavedAddress(addressData);
|
|
725
|
+
}
|
|
726
|
+
async function getSavedAddresses(userId, dbAdapter) {
|
|
727
|
+
return await dbAdapter.getSavedAddresses(userId);
|
|
728
|
+
}
|
|
729
|
+
async function getSavedAddress(addressId, dbAdapter) {
|
|
730
|
+
return await dbAdapter.getSavedAddress(addressId);
|
|
731
|
+
}
|
|
732
|
+
async function updateSavedAddress(addressId, data, dbAdapter) {
|
|
733
|
+
return await dbAdapter.updateSavedAddress(addressId, data);
|
|
734
|
+
}
|
|
735
|
+
async function deleteSavedAddress(addressId, dbAdapter) {
|
|
736
|
+
return await dbAdapter.deleteSavedAddress(addressId);
|
|
737
|
+
}
|
|
738
|
+
async function getLabelUrl(consignmentNumber, storageAdapter) {
|
|
739
|
+
try {
|
|
740
|
+
const fileName = `${consignmentNumber}`;
|
|
741
|
+
return await storageAdapter.getLabel(fileName);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
console.error("Error getting label URL:", error);
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async function regenerateLabel(shipmentId, consignmentNumber, format = "thermal", credentials, storageAdapter) {
|
|
748
|
+
return await generateAndUploadLabel(
|
|
749
|
+
shipmentId,
|
|
750
|
+
consignmentNumber,
|
|
751
|
+
format,
|
|
752
|
+
credentials,
|
|
753
|
+
storageAdapter
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
async function testDPDConnection(credentials) {
|
|
757
|
+
return await testConnection(credentials);
|
|
758
|
+
}
|
|
759
|
+
async function getAuthStatus(credentials) {
|
|
760
|
+
try {
|
|
761
|
+
const geoSession = await getGeoSession(credentials);
|
|
762
|
+
return {
|
|
763
|
+
authenticated: !!geoSession,
|
|
764
|
+
expiresAt: null
|
|
765
|
+
};
|
|
766
|
+
} catch (_error) {
|
|
767
|
+
return {
|
|
768
|
+
authenticated: false
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
var dpd_service_default = {
|
|
773
|
+
createCompleteShipment,
|
|
774
|
+
generateAndUploadLabel,
|
|
775
|
+
validateDeliveryAddress,
|
|
776
|
+
saveAddress,
|
|
777
|
+
getSavedAddresses,
|
|
778
|
+
getSavedAddress,
|
|
779
|
+
updateSavedAddress,
|
|
780
|
+
deleteSavedAddress,
|
|
781
|
+
getLabelUrl,
|
|
782
|
+
regenerateLabel,
|
|
783
|
+
testDPDConnection,
|
|
784
|
+
getAuthStatus
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
// src/utils/encryption.ts
|
|
788
|
+
import crypto from "crypto";
|
|
789
|
+
var ALGORITHM = "aes-256-gcm";
|
|
790
|
+
var KEY_LENGTH = 32;
|
|
791
|
+
var IV_LENGTH = 16;
|
|
792
|
+
var SALT_LENGTH = 64;
|
|
793
|
+
var TAG_LENGTH = 16;
|
|
794
|
+
var TAG_POSITION = SALT_LENGTH + IV_LENGTH;
|
|
795
|
+
var ENCRYPTED_POSITION = TAG_POSITION + TAG_LENGTH;
|
|
796
|
+
function getEncryptionKey() {
|
|
797
|
+
const envKey = process.env.DPD_ENCRYPTION_KEY;
|
|
798
|
+
if (envKey) {
|
|
799
|
+
return Buffer.from(envKey, "hex");
|
|
800
|
+
}
|
|
801
|
+
if (process.env.NODE_ENV !== "production") {
|
|
802
|
+
console.warn(
|
|
803
|
+
"\u26A0\uFE0F Using default encryption key. Set DPD_ENCRYPTION_KEY in production!"
|
|
804
|
+
);
|
|
805
|
+
return crypto.scryptSync("dpd-dev-key", "salt", KEY_LENGTH);
|
|
806
|
+
}
|
|
807
|
+
throw new Error(
|
|
808
|
+
"DPD_ENCRYPTION_KEY environment variable is required in production"
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
function generateEncryptionKey() {
|
|
812
|
+
const key = crypto.randomBytes(KEY_LENGTH);
|
|
813
|
+
return key.toString("hex");
|
|
814
|
+
}
|
|
815
|
+
function encrypt(text) {
|
|
816
|
+
try {
|
|
817
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
818
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
819
|
+
const key = crypto.scryptSync(getEncryptionKey(), salt, KEY_LENGTH);
|
|
820
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
821
|
+
const encrypted = Buffer.concat([
|
|
822
|
+
cipher.update(text, "utf8"),
|
|
823
|
+
cipher.final()
|
|
824
|
+
]);
|
|
825
|
+
const tag = cipher.getAuthTag();
|
|
826
|
+
const result = Buffer.concat([salt, iv, tag, encrypted]);
|
|
827
|
+
return result.toString("hex");
|
|
828
|
+
} catch (error) {
|
|
829
|
+
console.error("Encryption error:", error);
|
|
830
|
+
throw new Error("Failed to encrypt data");
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
function decrypt(encryptedHex) {
|
|
834
|
+
try {
|
|
835
|
+
const data = Buffer.from(encryptedHex, "hex");
|
|
836
|
+
const salt = data.subarray(0, SALT_LENGTH);
|
|
837
|
+
const iv = data.subarray(SALT_LENGTH, TAG_POSITION);
|
|
838
|
+
const tag = data.subarray(TAG_POSITION, ENCRYPTED_POSITION);
|
|
839
|
+
const encrypted = data.subarray(ENCRYPTED_POSITION);
|
|
840
|
+
const key = crypto.scryptSync(getEncryptionKey(), salt, KEY_LENGTH);
|
|
841
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
842
|
+
decipher.setAuthTag(tag);
|
|
843
|
+
const decrypted = Buffer.concat([
|
|
844
|
+
decipher.update(encrypted),
|
|
845
|
+
decipher.final()
|
|
846
|
+
]);
|
|
847
|
+
return decrypted.toString("utf8");
|
|
848
|
+
} catch (error) {
|
|
849
|
+
console.error("Decryption error:", error);
|
|
850
|
+
throw new Error("Failed to decrypt data");
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
function encryptCredentials(credentials) {
|
|
854
|
+
return {
|
|
855
|
+
accountNumber: credentials.accountNumber,
|
|
856
|
+
username: credentials.username,
|
|
857
|
+
passwordHash: encrypt(credentials.password)
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
function decryptCredentials(encryptedCredentials) {
|
|
861
|
+
return {
|
|
862
|
+
accountNumber: encryptedCredentials.accountNumber,
|
|
863
|
+
username: encryptedCredentials.username,
|
|
864
|
+
password: decrypt(encryptedCredentials.passwordHash)
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
function hash(data) {
|
|
868
|
+
return crypto.createHash("sha256").update(data).digest("hex");
|
|
869
|
+
}
|
|
870
|
+
function verifyHash(data, hashToVerify) {
|
|
871
|
+
const computed = hash(data);
|
|
872
|
+
return crypto.timingSafeEqual(
|
|
873
|
+
Buffer.from(computed),
|
|
874
|
+
Buffer.from(hashToVerify)
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/utils/logger.ts
|
|
879
|
+
var defaultConfig = {
|
|
880
|
+
enabled: true,
|
|
881
|
+
logToConsole: process.env.NODE_ENV !== "production",
|
|
882
|
+
logToDatabase: true
|
|
883
|
+
};
|
|
884
|
+
var config = { ...defaultConfig };
|
|
885
|
+
function configureLogger(newConfig) {
|
|
886
|
+
config = { ...config, ...newConfig };
|
|
887
|
+
}
|
|
888
|
+
function setLoggerAdapter(adapter) {
|
|
889
|
+
config.adapter = adapter;
|
|
890
|
+
}
|
|
891
|
+
async function logOperation(params) {
|
|
892
|
+
if (!config.enabled) {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const logData = {
|
|
896
|
+
orderId: params.orderId,
|
|
897
|
+
consignmentNumber: params.consignmentNumber,
|
|
898
|
+
operation: params.operation,
|
|
899
|
+
request: {
|
|
900
|
+
endpoint: sanitizeEndpoint(params.request.endpoint),
|
|
901
|
+
method: params.request.method,
|
|
902
|
+
headers: sanitizeHeaders(params.request.headers),
|
|
903
|
+
body: sanitizeBody(params.request.body)
|
|
904
|
+
},
|
|
905
|
+
response: {
|
|
906
|
+
status: params.response.status,
|
|
907
|
+
headers: sanitizeHeaders(params.response.headers),
|
|
908
|
+
body: sanitizeBody(params.response.body)
|
|
909
|
+
},
|
|
910
|
+
duration: params.duration,
|
|
911
|
+
success: params.success,
|
|
912
|
+
error: params.error,
|
|
913
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
914
|
+
};
|
|
915
|
+
if (config.logToConsole) {
|
|
916
|
+
logToConsole(logData);
|
|
917
|
+
}
|
|
918
|
+
if (config.logToDatabase && config.adapter) {
|
|
919
|
+
try {
|
|
920
|
+
await config.adapter.createDPDLog(logData);
|
|
921
|
+
} catch (error) {
|
|
922
|
+
console.error("Failed to log to database:", error);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function startTimer() {
|
|
927
|
+
const start = Date.now();
|
|
928
|
+
return () => Date.now() - start;
|
|
929
|
+
}
|
|
930
|
+
async function loggedOperation(params, operation) {
|
|
931
|
+
const timer = startTimer();
|
|
932
|
+
try {
|
|
933
|
+
const result = await operation();
|
|
934
|
+
await logOperation({
|
|
935
|
+
orderId: params.orderId,
|
|
936
|
+
consignmentNumber: params.consignmentNumber,
|
|
937
|
+
operation: params.operation,
|
|
938
|
+
request: {
|
|
939
|
+
endpoint: params.endpoint,
|
|
940
|
+
method: params.method,
|
|
941
|
+
body: params.requestBody
|
|
942
|
+
},
|
|
943
|
+
response: {
|
|
944
|
+
status: result.status,
|
|
945
|
+
headers: result.headers,
|
|
946
|
+
body: result.data
|
|
947
|
+
},
|
|
948
|
+
duration: timer(),
|
|
949
|
+
success: true
|
|
950
|
+
});
|
|
951
|
+
return result.data;
|
|
952
|
+
} catch (error) {
|
|
953
|
+
const errorInfo = {
|
|
954
|
+
code: error instanceof Error ? error.name : "UNKNOWN_ERROR",
|
|
955
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
956
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
957
|
+
};
|
|
958
|
+
await logOperation({
|
|
959
|
+
orderId: params.orderId,
|
|
960
|
+
consignmentNumber: params.consignmentNumber,
|
|
961
|
+
operation: params.operation,
|
|
962
|
+
request: {
|
|
963
|
+
endpoint: params.endpoint,
|
|
964
|
+
method: params.method,
|
|
965
|
+
body: params.requestBody
|
|
966
|
+
},
|
|
967
|
+
response: {
|
|
968
|
+
status: 500,
|
|
969
|
+
body: errorInfo
|
|
970
|
+
},
|
|
971
|
+
duration: timer(),
|
|
972
|
+
success: false,
|
|
973
|
+
error: errorInfo
|
|
974
|
+
});
|
|
975
|
+
throw error;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function sanitizeEndpoint(endpoint) {
|
|
979
|
+
try {
|
|
980
|
+
const url = new URL(endpoint);
|
|
981
|
+
url.searchParams.delete("password");
|
|
982
|
+
url.searchParams.delete("token");
|
|
983
|
+
url.searchParams.delete("key");
|
|
984
|
+
return url.toString();
|
|
985
|
+
} catch {
|
|
986
|
+
return endpoint;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
function sanitizeHeaders(headers) {
|
|
990
|
+
if (!headers) {
|
|
991
|
+
return void 0;
|
|
992
|
+
}
|
|
993
|
+
const sanitized = { ...headers };
|
|
994
|
+
delete sanitized.Authorization;
|
|
995
|
+
delete sanitized.authorization;
|
|
996
|
+
delete sanitized.GeoSession;
|
|
997
|
+
delete sanitized["Set-Cookie"];
|
|
998
|
+
delete sanitized["set-cookie"];
|
|
999
|
+
return sanitized;
|
|
1000
|
+
}
|
|
1001
|
+
function sanitizeBody(body) {
|
|
1002
|
+
if (!body) {
|
|
1003
|
+
return void 0;
|
|
1004
|
+
}
|
|
1005
|
+
let sanitized = JSON.parse(JSON.stringify(body));
|
|
1006
|
+
if (typeof sanitized === "object") {
|
|
1007
|
+
delete sanitized.password;
|
|
1008
|
+
delete sanitized.passwordHash;
|
|
1009
|
+
delete sanitized.token;
|
|
1010
|
+
delete sanitized.geoSession;
|
|
1011
|
+
}
|
|
1012
|
+
let stringified = JSON.stringify(sanitized);
|
|
1013
|
+
const MAX_SIZE = 1e4;
|
|
1014
|
+
if (stringified.length > MAX_SIZE) {
|
|
1015
|
+
stringified = stringified.substring(0, MAX_SIZE) + "... [truncated]";
|
|
1016
|
+
sanitized = stringified;
|
|
1017
|
+
}
|
|
1018
|
+
return sanitized;
|
|
1019
|
+
}
|
|
1020
|
+
function logToConsole(logData) {
|
|
1021
|
+
const color = logData.success ? "\x1B[32m" : "\x1B[31m";
|
|
1022
|
+
const reset = "\x1B[0m";
|
|
1023
|
+
const bold = "\x1B[1m";
|
|
1024
|
+
console.log("\n" + "=".repeat(80));
|
|
1025
|
+
console.log(
|
|
1026
|
+
`${bold}DPD ${logData.operation.toUpperCase()}${reset} - ${logData.success ? `${color}SUCCESS${reset}` : `${color}FAILED${reset}`}`
|
|
1027
|
+
);
|
|
1028
|
+
console.log("=".repeat(80));
|
|
1029
|
+
console.log(`Order ID: ${logData.orderId}`);
|
|
1030
|
+
if (logData.consignmentNumber) {
|
|
1031
|
+
console.log(`Consignment: ${logData.consignmentNumber}`);
|
|
1032
|
+
}
|
|
1033
|
+
console.log(`Request: ${logData.request.method} ${logData.request.endpoint}`);
|
|
1034
|
+
console.log(`Duration: ${logData.duration}ms`);
|
|
1035
|
+
console.log(`Status: ${logData.response.status}`);
|
|
1036
|
+
if (!logData.success && logData.error) {
|
|
1037
|
+
console.log(`${color}Error: ${logData.error.message}${reset}`);
|
|
1038
|
+
}
|
|
1039
|
+
console.log("=".repeat(80) + "\n");
|
|
1040
|
+
}
|
|
1041
|
+
export {
|
|
1042
|
+
dpd_service_default as DPDService,
|
|
1043
|
+
DPD_API,
|
|
1044
|
+
SERVICE_DESCRIPTIONS,
|
|
1045
|
+
SERVICE_NAMES,
|
|
1046
|
+
authenticate,
|
|
1047
|
+
authenticatedRequest,
|
|
1048
|
+
calculateDPDCost,
|
|
1049
|
+
calculateDeliveryFee,
|
|
1050
|
+
calculateParcels,
|
|
1051
|
+
clearGeoSession,
|
|
1052
|
+
configureLogger,
|
|
1053
|
+
createCompleteShipment,
|
|
1054
|
+
createDPDConfig,
|
|
1055
|
+
createShipment,
|
|
1056
|
+
decrypt,
|
|
1057
|
+
decryptCredentials,
|
|
1058
|
+
deleteSavedAddress,
|
|
1059
|
+
encrypt,
|
|
1060
|
+
encryptCredentials,
|
|
1061
|
+
generateAndUploadLabel,
|
|
1062
|
+
generateConsignmentRef,
|
|
1063
|
+
generateEncryptionKey,
|
|
1064
|
+
generateLabel,
|
|
1065
|
+
getAuthStatus,
|
|
1066
|
+
getEstimatedDeliveryDate,
|
|
1067
|
+
getGeoSession,
|
|
1068
|
+
getLabelUrl,
|
|
1069
|
+
getNextCollectionDate,
|
|
1070
|
+
getSavedAddress,
|
|
1071
|
+
getSavedAddresses,
|
|
1072
|
+
getServiceDescription,
|
|
1073
|
+
getServiceName,
|
|
1074
|
+
getTokenExpiry,
|
|
1075
|
+
getTrackingUrl,
|
|
1076
|
+
hasValidToken,
|
|
1077
|
+
hash,
|
|
1078
|
+
isValidServiceCode,
|
|
1079
|
+
logOperation,
|
|
1080
|
+
loggedOperation,
|
|
1081
|
+
meetsMinimumOrderValue,
|
|
1082
|
+
qualifiesForFreeDelivery,
|
|
1083
|
+
regenerateLabel,
|
|
1084
|
+
saveAddress,
|
|
1085
|
+
setLoggerAdapter,
|
|
1086
|
+
startTimer,
|
|
1087
|
+
testConnection,
|
|
1088
|
+
testDPDConnection,
|
|
1089
|
+
trackShipment,
|
|
1090
|
+
updateSavedAddress,
|
|
1091
|
+
validateAddress,
|
|
1092
|
+
validateDeliveryAddress,
|
|
1093
|
+
validateServiceCode,
|
|
1094
|
+
verifyHash
|
|
1095
|
+
};
|
|
1096
|
+
/**
|
|
1097
|
+
* DPD Local SDK
|
|
1098
|
+
*
|
|
1099
|
+
* TypeScript SDK for integrating DPD Local shipping services
|
|
1100
|
+
* Database-agnostic and framework-independent
|
|
1101
|
+
*
|
|
1102
|
+
* @package @your-org/dpd-local-sdk
|
|
1103
|
+
* @version 1.0.0
|
|
1104
|
+
* @author Your Name
|
|
1105
|
+
* @license MIT
|
|
1106
|
+
*/
|