@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/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
+ */