@pinelab/vendure-plugin-qls-fulfillment 3.2.0 → 3.3.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 CHANGED
@@ -1,3 +1,7 @@
1
+ # 3.3.0 (2026-09-24)
2
+
3
+ - Handle the `shipment.barcode` webhook and store the QLS tracking code and tracking URL on the order's custom fields.
4
+
1
5
  # 3.2.0 (2026-08-12)
2
6
 
3
7
  - Restore the legacy Angular Admin UI extension alongside the React Dashboard.
package/README.md CHANGED
@@ -34,6 +34,7 @@ The mutation returns the list of all existing EANs for the product in QLS after
34
34
  - Pushes orders to QLS automatically on order placement
35
35
  - Supports manual order push to QLS via Admin UI
36
36
  - Updates order status in Vendure based on QLS webhooks
37
+ - Stores tracking codes and tracking URLs from incoming `shipment.barcode` webhooks on the order's (private) `qlsTrackingCodes` and `qlsTrackingUrls` custom fields. An order can have multiple shipments/parcels, so codes and URLs are accumulated in these lists.
37
38
 
38
39
  ## Getting started
39
40
 
@@ -93,6 +94,7 @@ You should set up webhooks for the following events:
93
94
  - `fulfillment_product.stock`
94
95
  - `fulfillment_order.status`
95
96
  - `fulfillment_order.completed`
97
+ - `shipment.barcode`
96
98
 
97
99
  The URL for all these events should be `https://<YOUR_VENDURE_HOST>/qls/webhook/<CHANNEL_TOKEN>?secret=<PLUGIN_SECRET>`. E.g. `https://example.com/qls/webhook/your-channel-token?secret=121231`.
98
100
 
@@ -3,7 +3,7 @@ import { Request } from 'express';
3
3
  import { QlsProductService } from '../services/qls-product.service';
4
4
  import { QlsPluginOptions } from '../types';
5
5
  import { QlsOrderService } from '../services/qls-order.service';
6
- import { IncomingOrderWebhook, IncomingStockWebhook } from '../lib/client-types';
6
+ import { IncomingOrderWebhook, IncomingShipmentWebhook, IncomingStockWebhook } from '../lib/client-types';
7
7
  export declare class QlsWebhooksController {
8
8
  private channelService;
9
9
  private qlsProductService;
@@ -13,6 +13,6 @@ export declare class QlsWebhooksController {
13
13
  /**
14
14
  * Endpoint for all incoming webhooks
15
15
  */
16
- events(channelToken: string, webhookSecret: string, request: Request, body: IncomingStockWebhook | IncomingOrderWebhook): Promise<void>;
16
+ events(channelToken: string, webhookSecret: string, request: Request, body: IncomingStockWebhook | IncomingOrderWebhook | IncomingShipmentWebhook): Promise<void>;
17
17
  private getCtxForChannel;
18
18
  }
@@ -42,6 +42,9 @@ let QlsWebhooksController = class QlsWebhooksController {
42
42
  if (isStockWebhook(body)) {
43
43
  await this.qlsProductService.handleStockWebhook(ctx, body);
44
44
  }
45
+ else if (isShipmentWebhook(body)) {
46
+ await this.qlsOrderService.handleShipmentBarcodeWebhook(ctx, body);
47
+ }
45
48
  else if (isOrderWebhook(body)) {
46
49
  await this.qlsOrderService.handleOrderStatusUpdate(ctx, body);
47
50
  }
@@ -92,3 +95,6 @@ function isStockWebhook(body) {
92
95
  function isOrderWebhook(body) {
93
96
  return 'customer_reference' in body && 'status' in body;
94
97
  }
98
+ function isShipmentWebhook(body) {
99
+ return 'carrier_id' in body && 'reference' in body;
100
+ }
@@ -7,6 +7,8 @@ declare module '@vendure/core' {
7
7
  interface CustomOrderFields {
8
8
  qlsServicePointId?: string;
9
9
  qlsServicePointDetails?: string;
10
+ qlsTrackingCodes?: string[];
11
+ qlsTrackingUrls?: string[];
10
12
  }
11
13
  }
12
14
  /** Returns variant custom fields with the given Admin UI tab name. */
@@ -44,4 +44,36 @@ exports.orderCustomFields = [
44
44
  readonly: false,
45
45
  ui: { tab: 'QLS' },
46
46
  },
47
+ {
48
+ name: 'qlsTrackingCodes',
49
+ type: 'string',
50
+ list: true,
51
+ label: [{ value: 'QLS Tracking Codes', languageCode: core_1.LanguageCode.en }],
52
+ description: [
53
+ {
54
+ value: 'Barcodes/tracking codes of all shipments/parcels created for this order in QLS.',
55
+ languageCode: core_1.LanguageCode.en,
56
+ },
57
+ ],
58
+ nullable: true,
59
+ public: false,
60
+ readonly: true,
61
+ ui: { tab: 'QLS' },
62
+ },
63
+ {
64
+ name: 'qlsTrackingUrls',
65
+ type: 'string',
66
+ list: true,
67
+ label: [{ value: 'QLS Tracking URLs', languageCode: core_1.LanguageCode.en }],
68
+ description: [
69
+ {
70
+ value: 'Tracking URLs of all shipments/parcels created for this order in QLS.',
71
+ languageCode: core_1.LanguageCode.en,
72
+ },
73
+ ],
74
+ nullable: true,
75
+ public: false,
76
+ readonly: true,
77
+ ui: { tab: 'QLS' },
78
+ },
47
79
  ];
@@ -165,6 +165,26 @@ export interface FulfillmentOrder {
165
165
  }
166
166
  export type IncomingStockWebhook = FulfillmentProductDetail;
167
167
  export type IncomingOrderWebhook = Pick<FulfillmentOrder, 'customer_reference' | 'status' | 'cancelled' | 'amount_delivered' | 'amount_total'>;
168
+ /**
169
+ * Incoming `shipment.barcode` webhook payload from QLS.
170
+ * `reference` matches the Vendure order code.
171
+ * Note: `barcode`/`tracking_url` on the root object are sometimes not yet populated,
172
+ * so we also fall back to the nested shipment with the same `id` under `deliveries[].shipments[]`.
173
+ */
174
+ export interface IncomingShipmentWebhook {
175
+ id: string;
176
+ reference: string;
177
+ carrier_id: string;
178
+ barcode: string | null;
179
+ tracking_url: string | null;
180
+ deliveries?: Array<{
181
+ shipments?: Array<{
182
+ id: string;
183
+ barcode: string | null;
184
+ tracking_url: string | null;
185
+ }>;
186
+ }>;
187
+ }
168
188
  /**
169
189
  * This is the full data returend by the QLS API when fetching a fulfillment product by ID
170
190
  * Getting via list, sku or filtering does NOT return this full object
@@ -2,7 +2,7 @@ import { OnApplicationBootstrap, OnModuleInit } from '@nestjs/common';
2
2
  import { ModuleRef } from '@nestjs/core';
3
3
  import { EventBus, ID, Job, JobQueueService, OrderService, RequestContext, TransactionalConnection } from '@vendure/core';
4
4
  import { QlsServicePoint, QlsServicePointSearchInput } from '../api/generated/graphql';
5
- import { IncomingOrderWebhook } from '../lib/client-types';
5
+ import { IncomingOrderWebhook, IncomingShipmentWebhook } from '../lib/client-types';
6
6
  import { QlsOrderJobData, QlsPluginOptions } from '../types';
7
7
  export declare class QlsOrderService implements OnModuleInit, OnApplicationBootstrap {
8
8
  private options;
@@ -31,6 +31,17 @@ export declare class QlsOrderService implements OnModuleInit, OnApplicationBoots
31
31
  * Update the status of an order in QLS based on the given order code and status
32
32
  */
33
33
  handleOrderStatusUpdate(ctx: RequestContext, body: IncomingOrderWebhook): Promise<void>;
34
+ /**
35
+ * Store the tracking code and tracking URL from an incoming `shipment.barcode` webhook
36
+ * on the order's custom fields. Codes/URLs are accumulated, because an order can have
37
+ * multiple shipments/parcels.
38
+ */
39
+ handleShipmentBarcodeWebhook(ctx: RequestContext, body: IncomingShipmentWebhook): Promise<void>;
40
+ /**
41
+ * The root `barcode`/`tracking_url` fields on the webhook payload are sometimes not yet
42
+ * populated, so fall back to the nested shipment with the same id under `deliveries[].shipments[]`.
43
+ */
44
+ private getShipmentTrackingInfo;
34
45
  triggerPushOrder(ctx: RequestContext, orderId: ID, orderCode?: string): Promise<Job<QlsOrderJobData> | undefined>;
35
46
  /**
36
47
  * Get QLS order id(s) for a Vendure order (for Order.qlsOrderIds field).
@@ -260,6 +260,54 @@ let QlsOrderService = class QlsOrderService {
260
260
  await this.orderService.transitionToState(ctx, order.id, vendureOrderState);
261
261
  core_2.Logger.info(`Successfully updated order '${orderCode}' to '${vendureOrderState}'`, constants_1.loggerCtx);
262
262
  }
263
+ /**
264
+ * Store the tracking code and tracking URL from an incoming `shipment.barcode` webhook
265
+ * on the order's custom fields. Codes/URLs are accumulated, because an order can have
266
+ * multiple shipments/parcels.
267
+ */
268
+ async handleShipmentBarcodeWebhook(ctx, body) {
269
+ const orderCode = body.reference;
270
+ const { barcode, trackingUrl } = this.getShipmentTrackingInfo(body);
271
+ if (!barcode) {
272
+ core_2.Logger.info(`Ignoring shipment.barcode webhook for order '${orderCode}', because no barcode was found in the payload`, constants_1.loggerCtx);
273
+ return;
274
+ }
275
+ const order = await this.orderService.findOneByCode(ctx, orderCode, []);
276
+ if (!order) {
277
+ return core_2.Logger.warn(`Order with code '${orderCode}' not found, ignoring shipment.barcode webhook`, constants_1.loggerCtx);
278
+ }
279
+ const existingCodes = order.customFields.qlsTrackingCodes ?? [];
280
+ if (existingCodes.includes(barcode)) {
281
+ core_2.Logger.info(`Tracking code '${barcode}' already stored for order '${orderCode}', ignoring duplicate shipment.barcode webhook`, constants_1.loggerCtx);
282
+ return;
283
+ }
284
+ const existingUrls = order.customFields.qlsTrackingUrls ?? [];
285
+ await this.connection.getRepository(ctx, core_2.Order).update({ id: order.id }, {
286
+ customFields: {
287
+ qlsTrackingCodes: [...existingCodes, barcode],
288
+ qlsTrackingUrls: trackingUrl
289
+ ? [...existingUrls, trackingUrl]
290
+ : existingUrls,
291
+ },
292
+ });
293
+ core_2.Logger.info(`Stored tracking code '${barcode}' for order '${orderCode}'`, constants_1.loggerCtx);
294
+ }
295
+ /**
296
+ * The root `barcode`/`tracking_url` fields on the webhook payload are sometimes not yet
297
+ * populated, so fall back to the nested shipment with the same id under `deliveries[].shipments[]`.
298
+ */
299
+ getShipmentTrackingInfo(body) {
300
+ if (body.barcode) {
301
+ return { barcode: body.barcode, trackingUrl: body.tracking_url };
302
+ }
303
+ const nestedShipment = body.deliveries
304
+ ?.flatMap((delivery) => delivery.shipments ?? [])
305
+ .find((shipment) => shipment.id === body.id);
306
+ return {
307
+ barcode: nestedShipment?.barcode ?? null,
308
+ trackingUrl: nestedShipment?.tracking_url ?? null,
309
+ };
310
+ }
263
311
  async triggerPushOrder(ctx, orderId, orderCode) {
264
312
  const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
265
313
  if (!client) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pinelab/vendure-plugin-qls-fulfillment",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Vendure plugin to fulfill orders via QLS.",
5
5
  "keywords": [
6
6
  "fulfillment",