@pinelab/vendure-plugin-qls-fulfillment 3.2.0 → 3.4.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,11 @@
1
+ # 3.4.0 (2026-10-30)
2
+
3
+ - Removed the legacy Angular Admin UI extension. Backend functionality and the React Dashboard extension are unchanged.
4
+
5
+ # 3.3.0 (2026-09-24)
6
+
7
+ - Handle the `shipment.barcode` webhook and store the QLS tracking code and tracking URL on the order's custom fields.
8
+
1
9
  # 3.2.0 (2026-08-12)
2
10
 
3
11
  - Restore the legacy Angular Admin UI extension alongside the React Dashboard.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Vendure plugin to fulfill orders via QLS. This uses QLS as fulfillment center and does not support shipments only via QLS.
4
4
 
5
- [Official documentation here](https://plugins.pinelab.studio/plugin/vendure-plugin-qls-fulfillment)
5
+ [Official documentation here](https://engineering.pinelab.studio/plugin/vendure-plugin-qls-fulfillment)
6
6
 
7
7
  This plugin contains:
8
8
 
@@ -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
 
@@ -1,9 +1,9 @@
1
1
  import { ChannelService } from '@vendure/core';
2
2
  import { Request } from 'express';
3
+ import { IncomingOrderWebhook, IncomingShipmentWebhook, IncomingStockWebhook } from '../lib/client-types';
4
+ import { QlsOrderService } from '../services/qls-order.service';
3
5
  import { QlsProductService } from '../services/qls-product.service';
4
6
  import { QlsPluginOptions } from '../types';
5
- import { QlsOrderService } from '../services/qls-order.service';
6
- import { IncomingOrderWebhook, 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
  }
@@ -16,9 +16,9 @@ exports.QlsWebhooksController = void 0;
16
16
  const common_1 = require("@nestjs/common");
17
17
  const core_1 = require("@vendure/core");
18
18
  const catch_unknown_1 = require("catch-unknown");
19
- const qls_product_service_1 = require("../services/qls-product.service");
20
19
  const constants_1 = require("../constants");
21
20
  const qls_order_service_1 = require("../services/qls-order.service");
21
+ const qls_product_service_1 = require("../services/qls-product.service");
22
22
  let QlsWebhooksController = class QlsWebhooksController {
23
23
  constructor(channelService, qlsProductService, qlsOrderService, options) {
24
24
  this.channelService = channelService;
@@ -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
+ }
@@ -2,8 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.qlsSyncAllProductsTask = void 0;
4
4
  const core_1 = require("@vendure/core");
5
- const qls_product_service_1 = require("../services/qls-product.service");
6
5
  const constants_1 = require("../constants");
6
+ const qls_product_service_1 = require("../services/qls-product.service");
7
7
  /**
8
8
  * Synchronize all products to QLS for all channels, every day at 3:00 AM.
9
9
  */
@@ -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
  ];
@@ -1,3 +1,4 @@
1
+ import { useMutation } from '@tanstack/react-query';
1
2
  import {
2
3
  api,
3
4
  Button,
@@ -11,7 +12,6 @@ import {
11
12
  Input,
12
13
  Label,
13
14
  } from '@vendure/dashboard';
14
- import { useMutation } from '@tanstack/react-query';
15
15
  import { QrCode } from 'lucide-react';
16
16
  import { useState } from 'react';
17
17
  import { toast } from 'sonner';
@@ -1,3 +1,4 @@
1
+ import { useMutation, useQuery } from '@tanstack/react-query';
1
2
  import {
2
3
  api,
3
4
  Button,
@@ -9,7 +10,6 @@ import {
9
10
  DialogTitle,
10
11
  DropdownMenuItem,
11
12
  } from '@vendure/dashboard';
12
- import { useQuery, useMutation } from '@tanstack/react-query';
13
13
  import { RefreshCw } from 'lucide-react';
14
14
  import { useState } from 'react';
15
15
  import { toast } from 'sonner';
@@ -1,5 +1,5 @@
1
- import { api, DropdownMenuItem } from '@vendure/dashboard';
2
1
  import { useMutation } from '@tanstack/react-query';
2
+ import { api, DropdownMenuItem } from '@vendure/dashboard';
3
3
  import { RefreshCw } from 'lucide-react';
4
4
  import { toast } from 'sonner';
5
5
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './api/generated/graphql';
2
+ export * from './config/permissions';
2
3
  export * from './config/sync-products-scheduled-task';
3
4
  export * from './custom-fields';
4
5
  export * from './lib/client-types';
@@ -6,5 +7,4 @@ export * from './lib/qls-client';
6
7
  export * from './qls-plugin';
7
8
  export * from './services/qls-order.service';
8
9
  export * from './services/qls-product.service';
9
- export * from './config/permissions';
10
10
  export * from './types';
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./api/generated/graphql"), exports);
18
+ __exportStar(require("./config/permissions"), exports);
18
19
  __exportStar(require("./config/sync-products-scheduled-task"), exports);
19
20
  __exportStar(require("./custom-fields"), exports);
20
21
  __exportStar(require("./lib/client-types"), exports);
@@ -22,5 +23,4 @@ __exportStar(require("./lib/qls-client"), exports);
22
23
  __exportStar(require("./qls-plugin"), exports);
23
24
  __exportStar(require("./services/qls-order.service"), exports);
24
25
  __exportStar(require("./services/qls-product.service"), exports);
25
- __exportStar(require("./config/permissions"), exports);
26
26
  __exportStar(require("./types"), exports);
@@ -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
@@ -1,7 +1,7 @@
1
1
  import { RequestContext } from '@vendure/core';
2
+ import { QlsServicePoint } from '../api/generated/graphql';
2
3
  import { QlsClientConfig, QlsPluginOptions } from '../types';
3
4
  import type { FulfillmentOrder, FulfillmentOrderInput, FulfillmentProduct, FulfillmentProductDetail, FulfillmentProductInput, QlsApiResponse } from './client-types';
4
- import { QlsServicePoint } from '../api/generated/graphql';
5
5
  export declare function getQlsClient(ctx: RequestContext, pluginOptions: QlsPluginOptions): Promise<QlsClient | undefined>;
6
6
  /**
7
7
  * Wrapper around the QLS Rest API.
@@ -1,8 +1,6 @@
1
- import { Type } from '@vendure/core';
2
- import { AdminUiExtension } from '@vendure/ui-devkit/compiler';
3
- import { QlsPluginOptions } from './types';
1
+ import { type Type } from '@vendure/core';
2
+ import type { QlsPluginOptions } from './types';
4
3
  export declare class QlsPlugin {
5
4
  static options: QlsPluginOptions;
6
5
  static init(options: QlsPluginOptions): Type<QlsPlugin>;
7
- static ui: AdminUiExtension;
8
6
  }
@@ -5,14 +5,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
6
  return c > 3 && r && Object.defineProperty(target, key, r), r;
7
7
  };
8
- var __importDefault = (this && this.__importDefault) || function (mod) {
9
- return (mod && mod.__esModule) ? mod : { "default": mod };
10
- };
11
8
  var QlsPlugin_1;
12
9
  Object.defineProperty(exports, "__esModule", { value: true });
13
10
  exports.QlsPlugin = void 0;
14
11
  const core_1 = require("@vendure/core");
15
- const path_1 = __importDefault(require("path"));
16
12
  const api_extensions_1 = require("./api/api-extensions");
17
13
  const qls_admin_resolver_1 = require("./api/qls-admin.resolver");
18
14
  const qls_shop_resolver_1 = require("./api/qls-shop.resolver");
@@ -20,12 +16,12 @@ const qls_webhooks_controller_1 = require("./api/qls-webhooks-controller");
20
16
  const permissions_1 = require("./config/permissions");
21
17
  const constants_1 = require("./constants");
22
18
  const custom_fields_1 = require("./custom-fields");
19
+ const qls_order_entity_entity_1 = require("./entities/qls-order-entity.entity");
23
20
  const qls_order_service_1 = require("./services/qls-order.service");
24
21
  const qls_product_service_1 = require("./services/qls-product.service");
25
- const qls_order_entity_entity_1 = require("./entities/qls-order-entity.entity");
26
22
  let QlsPlugin = QlsPlugin_1 = class QlsPlugin {
27
23
  static init(options) {
28
- this.options = {
24
+ QlsPlugin_1.options = {
29
25
  ...options,
30
26
  orderSync: {
31
27
  autoPushOrders: true,
@@ -41,11 +37,6 @@ let QlsPlugin = QlsPlugin_1 = class QlsPlugin {
41
37
  }
42
38
  };
43
39
  exports.QlsPlugin = QlsPlugin;
44
- QlsPlugin.ui = {
45
- extensionPath: path_1.default.join(__dirname, 'ui'),
46
- id: 'qls-fulfillment-ui',
47
- providers: ['providers.ts'],
48
- };
49
40
  exports.QlsPlugin = QlsPlugin = QlsPlugin_1 = __decorate([
50
41
  (0, core_1.VendurePlugin)({
51
42
  imports: [core_1.PluginCommonModule],
@@ -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;
@@ -20,6 +20,12 @@ export declare class QlsOrderService implements OnModuleInit, OnApplicationBoots
20
20
  * Returns the result of the job, which will be stored in the job record.
21
21
  */
22
22
  handleOrderJob(job: Job<QlsOrderJobData>): Promise<unknown>;
23
+ /** Checks the address fields required by QLS fulfillment orders. */
24
+ private hasRequiredShippingFields;
25
+ /** Rejects repeated pushes unless explicitly forced. */
26
+ private assertOrderNotSynced;
27
+ /** Loads optional extra order items and reports callback failures consistently. */
28
+ private getAdditionalOrderItems;
23
29
  /**
24
30
  * Push an order to QLS by id.
25
31
  * Returns a human-readable message describing the result of the operation (Used as job result).
@@ -31,6 +37,17 @@ export declare class QlsOrderService implements OnModuleInit, OnApplicationBoots
31
37
  * Update the status of an order in QLS based on the given order code and status
32
38
  */
33
39
  handleOrderStatusUpdate(ctx: RequestContext, body: IncomingOrderWebhook): Promise<void>;
40
+ /**
41
+ * Store the tracking code and tracking URL from an incoming `shipment.barcode` webhook
42
+ * on the order's custom fields. Codes/URLs are accumulated, because an order can have
43
+ * multiple shipments/parcels.
44
+ */
45
+ handleShipmentBarcodeWebhook(ctx: RequestContext, body: IncomingShipmentWebhook): Promise<void>;
46
+ /**
47
+ * The root `barcode`/`tracking_url` fields on the webhook payload are sometimes not yet
48
+ * populated, so fall back to the nested shipment with the same id under `deliveries[].shipments[]`.
49
+ */
50
+ private getShipmentTrackingInfo;
34
51
  triggerPushOrder(ctx: RequestContext, orderId: ID, orderCode?: string): Promise<Job<QlsOrderJobData> | undefined>;
35
52
  /**
36
53
  * Get QLS order id(s) for a Vendure order (for Order.qlsOrderIds field).
@@ -16,15 +16,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
16
16
  };
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.QlsOrderService = void 0;
19
+ const node_util_1 = __importDefault(require("node:util"));
19
20
  const common_1 = require("@nestjs/common");
20
21
  const core_1 = require("@nestjs/core");
21
22
  const core_2 = require("@vendure/core");
22
23
  const catch_unknown_1 = require("catch-unknown");
23
- const util_1 = __importDefault(require("util"));
24
24
  const constants_1 = require("../constants");
25
+ const qls_order_entity_entity_1 = require("../entities/qls-order-entity.entity");
25
26
  const qls_client_1 = require("../lib/qls-client");
26
27
  const qls_order_failed_event_1 = require("./qls-order-failed-event");
27
- const qls_order_entity_entity_1 = require("../entities/qls-order-entity.entity");
28
28
  let QlsOrderService = class QlsOrderService {
29
29
  constructor(options, jobQueueService, eventBus, orderService, moduleRef, connection) {
30
30
  this.options = options;
@@ -74,10 +74,39 @@ let QlsOrderService = class QlsOrderService {
74
74
  ...job.data,
75
75
  ctx: undefined,
76
76
  };
77
- core_2.Logger.error(`Error handling job ${job.data.action}: ${error}`, constants_1.loggerCtx, util_1.default.inspect(dataWithoutCtx, false, 5));
77
+ core_2.Logger.error(`Error handling job ${job.data.action}: ${error}`, constants_1.loggerCtx, node_util_1.default.inspect(dataWithoutCtx, false, 5));
78
78
  throw error;
79
79
  }
80
80
  }
81
+ /** Checks the address fields required by QLS fulfillment orders. */
82
+ hasRequiredShippingFields(address) {
83
+ return !!(address.streetLine1 &&
84
+ address.postalCode &&
85
+ address.city &&
86
+ address.streetLine2 &&
87
+ address.countryCode);
88
+ }
89
+ /** Rejects repeated pushes unless explicitly forced. */
90
+ async assertOrderNotSynced(ctx, order, force) {
91
+ if (force)
92
+ return;
93
+ const existing = await this.connection
94
+ .getRepository(ctx, qls_order_entity_entity_1.QlsOrderEntity)
95
+ .findOne({ where: { vendureOrderId: order.id } });
96
+ if (existing)
97
+ throw new core_2.UserInputError(`Order '${order.code}' has already been synced to QLS`);
98
+ }
99
+ /** Loads optional extra order items and reports callback failures consistently. */
100
+ async getAdditionalOrderItems(ctx, order) {
101
+ try {
102
+ return ((await this.options.orderSync.addAdditionalOrderItems?.(ctx, new core_2.Injector(this.moduleRef), order)) ?? []);
103
+ }
104
+ catch (e) {
105
+ const error = (0, catch_unknown_1.asError)(e);
106
+ core_2.Logger.error(`Error getting additional order items for order '${order.code}': ${error.message}`, constants_1.loggerCtx, error.stack);
107
+ throw new Error(`Error adding additional order items for order '${order.code}': ${error.message}`);
108
+ }
109
+ }
81
110
  /**
82
111
  * Push an order to QLS by id.
83
112
  * Returns a human-readable message describing the result of the operation (Used as job result).
@@ -94,18 +123,7 @@ let QlsOrderService = class QlsOrderService {
94
123
  if (!order) {
95
124
  throw new Error(`No order with id ${orderId} not found`);
96
125
  }
97
- if (!force) {
98
- const existingQlsOrder = await this.connection
99
- .getRepository(ctx, qls_order_entity_entity_1.QlsOrderEntity)
100
- .findOne({
101
- where: {
102
- vendureOrderId: orderId,
103
- },
104
- });
105
- if (existingQlsOrder) {
106
- throw new core_2.UserInputError(`Order '${order.code}' has already been synced to QLS`);
107
- }
108
- }
126
+ await this.assertOrderNotSynced(ctx, order, force);
109
127
  try {
110
128
  // Map order variants to QLS products
111
129
  const qlsProducts = [];
@@ -126,17 +144,7 @@ let QlsOrderService = class QlsOrderService {
126
144
  });
127
145
  }));
128
146
  // Add additional order items, if any
129
- try {
130
- const additionalOrderItems = await this.options.orderSync.addAdditionalOrderItems?.(ctx, new core_2.Injector(this.moduleRef), order);
131
- if (additionalOrderItems) {
132
- qlsProducts.push(...additionalOrderItems);
133
- }
134
- }
135
- catch (e) {
136
- const error = (0, catch_unknown_1.asError)(e);
137
- core_2.Logger.error(`Error getting additional order items for order '${order.code}': ${error.message}`, constants_1.loggerCtx, error.stack);
138
- throw new Error(`Error adding additional order items for order '${order.code}': ${error.message}`);
139
- }
147
+ qlsProducts.push(...(await this.getAdditionalOrderItems(ctx, order)));
140
148
  if (qlsProducts.length === 0) {
141
149
  const message = `No products to push to QLS for order '${order.code}'. Ignoring order.`;
142
150
  core_2.Logger.info(message, constants_1.loggerCtx);
@@ -153,11 +161,7 @@ let QlsOrderService = class QlsOrderService {
153
161
  if (!order.shippingAddress) {
154
162
  throw new Error(`Order '${order.code}' has no shipping address! Can not push order to QLS.`);
155
163
  }
156
- if (!order.shippingAddress.streetLine1 ||
157
- !order.shippingAddress.postalCode ||
158
- !order.shippingAddress.city ||
159
- !order.shippingAddress.streetLine2 ||
160
- !order.shippingAddress.countryCode) {
164
+ if (!this.hasRequiredShippingFields(order.shippingAddress)) {
161
165
  throw new Error(`Shipping address for order '${order.code}' is missing one of required fields: streetLine1, postalCode, city, streetLine2, countryCode. Can not push order to QLS.`);
162
166
  }
163
167
  const processable = (await this.options.orderSync.processOrderFrom?.(ctx, order)) ??
@@ -260,6 +264,54 @@ let QlsOrderService = class QlsOrderService {
260
264
  await this.orderService.transitionToState(ctx, order.id, vendureOrderState);
261
265
  core_2.Logger.info(`Successfully updated order '${orderCode}' to '${vendureOrderState}'`, constants_1.loggerCtx);
262
266
  }
267
+ /**
268
+ * Store the tracking code and tracking URL from an incoming `shipment.barcode` webhook
269
+ * on the order's custom fields. Codes/URLs are accumulated, because an order can have
270
+ * multiple shipments/parcels.
271
+ */
272
+ async handleShipmentBarcodeWebhook(ctx, body) {
273
+ const orderCode = body.reference;
274
+ const { barcode, trackingUrl } = this.getShipmentTrackingInfo(body);
275
+ if (!barcode) {
276
+ core_2.Logger.info(`Ignoring shipment.barcode webhook for order '${orderCode}', because no barcode was found in the payload`, constants_1.loggerCtx);
277
+ return;
278
+ }
279
+ const order = await this.orderService.findOneByCode(ctx, orderCode, []);
280
+ if (!order) {
281
+ return core_2.Logger.warn(`Order with code '${orderCode}' not found, ignoring shipment.barcode webhook`, constants_1.loggerCtx);
282
+ }
283
+ const existingCodes = order.customFields.qlsTrackingCodes ?? [];
284
+ if (existingCodes.includes(barcode)) {
285
+ core_2.Logger.info(`Tracking code '${barcode}' already stored for order '${orderCode}', ignoring duplicate shipment.barcode webhook`, constants_1.loggerCtx);
286
+ return;
287
+ }
288
+ const existingUrls = order.customFields.qlsTrackingUrls ?? [];
289
+ await this.connection.getRepository(ctx, core_2.Order).update({ id: order.id }, {
290
+ customFields: {
291
+ qlsTrackingCodes: [...existingCodes, barcode],
292
+ qlsTrackingUrls: trackingUrl
293
+ ? [...existingUrls, trackingUrl]
294
+ : existingUrls,
295
+ },
296
+ });
297
+ core_2.Logger.info(`Stored tracking code '${barcode}' for order '${orderCode}'`, constants_1.loggerCtx);
298
+ }
299
+ /**
300
+ * The root `barcode`/`tracking_url` fields on the webhook payload are sometimes not yet
301
+ * populated, so fall back to the nested shipment with the same id under `deliveries[].shipments[]`.
302
+ */
303
+ getShipmentTrackingInfo(body) {
304
+ if (body.barcode) {
305
+ return { barcode: body.barcode, trackingUrl: body.tracking_url };
306
+ }
307
+ const nestedShipment = body.deliveries
308
+ ?.flatMap((delivery) => delivery.shipments ?? [])
309
+ .find((shipment) => shipment.id === body.id);
310
+ return {
311
+ barcode: nestedShipment?.barcode ?? null,
312
+ trackingUrl: nestedShipment?.tracking_url ?? null,
313
+ };
314
+ }
263
315
  async triggerPushOrder(ctx, orderId, orderCode) {
264
316
  const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
265
317
  if (!client) {
@@ -1,9 +1,9 @@
1
1
  import { OnApplicationBootstrap, OnModuleInit } from '@nestjs/common';
2
+ import { ModuleRef } from '@nestjs/core';
2
3
  import { EventBus, ID, Job, JobQueueService, ListQueryBuilder, ProductPriceApplicator, ProductVariant, ProductVariantService, RequestContext, StockLevelService, StockLocationService, TransactionalConnection } from '@vendure/core';
3
4
  import { FulfillmentProduct, IncomingStockWebhook } from '../lib/client-types';
4
5
  import { QlsClient } from '../lib/qls-client';
5
6
  import { QlsPluginOptions, QlsProductJobData } from '../types';
6
- import { ModuleRef } from '@nestjs/core';
7
7
  type SyncProductsResult = {
8
8
  updatedInQls: Partial<ProductVariant>[];
9
9
  createdInQls: Partial<ProductVariant>[];
@@ -16,15 +16,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
16
16
  };
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.QlsProductService = void 0;
19
+ const node_util_1 = __importDefault(require("node:util"));
19
20
  const common_1 = require("@nestjs/common");
20
- const core_1 = require("@vendure/core");
21
+ const core_1 = require("@nestjs/core");
22
+ const core_2 = require("@vendure/core");
21
23
  const catch_unknown_1 = require("catch-unknown");
22
24
  const typeorm_1 = require("typeorm");
23
- const util_1 = __importDefault(require("util"));
24
25
  const constants_1 = require("../constants");
25
26
  const qls_client_1 = require("../lib/qls-client");
26
27
  const qls_variant_sync_failed_event_1 = require("./qls-variant-sync-failed-event");
27
- const core_2 = require("@nestjs/core");
28
28
  // Wait for 700ms to avoid rate limit of 500/5 minutes
29
29
  const waitToPreventRateLimit = () => new Promise((resolve) => setTimeout(resolve, 700));
30
30
  let QlsProductService = class QlsProductService {
@@ -42,13 +42,13 @@ let QlsProductService = class QlsProductService {
42
42
  }
43
43
  onApplicationBootstrap() {
44
44
  // Listen for ProductVariantEvent and add a job to the queue
45
- this.eventBus.ofType(core_1.ProductVariantEvent).subscribe((event) => {
45
+ this.eventBus.ofType(core_2.ProductVariantEvent).subscribe((event) => {
46
46
  if (event.type !== 'created' && event.type !== 'updated') {
47
47
  return;
48
48
  }
49
49
  this.triggerSyncVariants(event.ctx, event.entity.map((v) => v.id)).catch((e) => {
50
50
  const error = (0, catch_unknown_1.asError)(e);
51
- core_1.Logger.error(`Error adding job to queue: ${error.message}`, constants_1.loggerCtx, error.stack);
51
+ core_2.Logger.error(`Error adding job to queue: ${error.message}`, constants_1.loggerCtx, error.stack);
52
52
  });
53
53
  });
54
54
  }
@@ -66,7 +66,7 @@ let QlsProductService = class QlsProductService {
66
66
  */
67
67
  async handleProductJob(job) {
68
68
  try {
69
- const ctx = core_1.RequestContext.deserialize(job.data.ctx);
69
+ const ctx = core_2.RequestContext.deserialize(job.data.ctx);
70
70
  if (job.data.action === 'full-sync-products') {
71
71
  const result = await this.runFullSync(ctx);
72
72
  return {
@@ -87,7 +87,7 @@ let QlsProductService = class QlsProductService {
87
87
  ...job.data,
88
88
  ctx: undefined,
89
89
  };
90
- core_1.Logger.error(`Error handling job ${job.data.action}: ${error}`, constants_1.loggerCtx, util_1.default.inspect(dataWithoutCtx, false, 5));
90
+ core_2.Logger.error(`Error handling job ${job.data.action}: ${error}`, constants_1.loggerCtx, node_util_1.default.inspect(dataWithoutCtx, false, 5));
91
91
  throw error;
92
92
  }
93
93
  }
@@ -104,33 +104,35 @@ let QlsProductService = class QlsProductService {
104
104
  if (!client) {
105
105
  throw new Error(`QLS not enabled for channel ${ctx.channel.token}`);
106
106
  }
107
- core_1.Logger.info(`Running full sync for channel ${ctx.channel.token}`, constants_1.loggerCtx);
107
+ core_2.Logger.info(`Running full sync for channel ${ctx.channel.token}`, constants_1.loggerCtx);
108
108
  const allQlsProducts = await client.getAllFulfillmentProducts();
109
109
  const allVariants = await this.getAllVariants(ctx);
110
- core_1.Logger.info(`Running full sync for ${allQlsProducts.length} QLS products and ${allVariants.length} Vendure variants`, constants_1.loggerCtx);
110
+ core_2.Logger.info(`Running full sync for ${allQlsProducts.length} QLS products and ${allVariants.length} Vendure variants`, constants_1.loggerCtx);
111
111
  // Update stock in Vendure based on QLS products
112
112
  const updatedStock = [];
113
113
  for (const variant of allVariants) {
114
- const qlsProduct = allQlsProducts.find((p) => p.sku == variant.sku);
114
+ const qlsProduct = allQlsProducts.find((p) => p.sku === variant.sku);
115
115
  if (qlsProduct) {
116
116
  await this.updateStock(ctx, variant.id, qlsProduct.amount_available);
117
117
  updatedStock.push(variant);
118
118
  }
119
119
  }
120
- core_1.Logger.info(`Updated stock for ${updatedStock.length} variants based on QLS stock levels`, constants_1.loggerCtx);
120
+ core_2.Logger.info(`Updated stock for ${updatedStock.length} variants based on QLS stock levels`, constants_1.loggerCtx);
121
121
  // Create or update products in QLS
122
122
  const createdQlsProducts = [];
123
123
  const updatedQlsProducts = [];
124
124
  const failed = [];
125
125
  for (const variant of allVariants) {
126
126
  try {
127
- const existingQlsProduct = allQlsProducts.find((p) => p.sku == variant.sku);
127
+ const existingQlsProduct = allQlsProducts.find((p) => p.sku === variant.sku);
128
128
  const result = await this.createOrUpdateProductInQls(ctx, client, variant, existingQlsProduct ?? null);
129
- if (result.status === 'created') {
130
- createdQlsProducts.push(variant);
131
- }
132
- else if (result.status === 'updated') {
133
- updatedQlsProducts.push(variant);
129
+ switch (result.status) {
130
+ case 'created':
131
+ createdQlsProducts.push(variant);
132
+ break;
133
+ case 'updated':
134
+ updatedQlsProducts.push(variant);
135
+ break;
134
136
  }
135
137
  if (result.status === 'created' || result.status === 'updated') {
136
138
  // Wait only if we created or updated a product, otherwise no calls have been made yet.
@@ -139,15 +141,15 @@ let QlsProductService = class QlsProductService {
139
141
  }
140
142
  catch (e) {
141
143
  const error = (0, catch_unknown_1.asError)(e);
142
- core_1.Logger.error(`Error creating or updating variant '${variant.sku}' in QLS: ${error.message}`, constants_1.loggerCtx, error.stack);
144
+ core_2.Logger.error(`Error creating or updating variant '${variant.sku}' in QLS: ${error.message}`, constants_1.loggerCtx, error.stack);
143
145
  failed.push(variant);
144
146
  await this.eventBus.publish(new qls_variant_sync_failed_event_1.QlsVariantSyncFailedEvent(ctx, variant, new Date(), error.message));
145
147
  await waitToPreventRateLimit();
146
148
  }
147
149
  }
148
- core_1.Logger.info(`Created ${createdQlsProducts.length} products in QLS`, constants_1.loggerCtx);
149
- core_1.Logger.info(`Updated ${updatedQlsProducts.length} products in QLS`, constants_1.loggerCtx);
150
- core_1.Logger.info(`Finished full sync with ${failed.length} failures`, constants_1.loggerCtx);
150
+ core_2.Logger.info(`Created ${createdQlsProducts.length} products in QLS`, constants_1.loggerCtx);
151
+ core_2.Logger.info(`Updated ${updatedQlsProducts.length} products in QLS`, constants_1.loggerCtx);
152
+ core_2.Logger.info(`Finished full sync with ${failed.length} failures`, constants_1.loggerCtx);
151
153
  return {
152
154
  updatedInQls: updatedQlsProducts,
153
155
  createdInQls: createdQlsProducts,
@@ -157,7 +159,7 @@ let QlsProductService = class QlsProductService {
157
159
  }
158
160
  catch (e) {
159
161
  const error = (0, catch_unknown_1.asError)(e);
160
- core_1.Logger.error(`Error running full sync: ${error.message}`, constants_1.loggerCtx, error.stack);
162
+ core_2.Logger.error(`Error running full sync: ${error.message}`, constants_1.loggerCtx, error.stack);
161
163
  throw error;
162
164
  }
163
165
  }
@@ -170,14 +172,14 @@ let QlsProductService = class QlsProductService {
170
172
  if (!client) {
171
173
  throw new Error(`QLS not enabled for channel ${ctx.channel.token}`);
172
174
  }
173
- core_1.Logger.warn(`Removing all products from QLS for channel ${ctx.channel.token}...`, constants_1.loggerCtx);
175
+ core_2.Logger.warn(`Removing all products from QLS for channel ${ctx.channel.token}...`, constants_1.loggerCtx);
174
176
  const allProducts = await client.getAllFulfillmentProducts();
175
177
  for (const product of allProducts) {
176
178
  await client.deleteFulfillmentProduct(product.id);
177
- core_1.Logger.info(`Removed product '${product.sku}' (${product.id}) from QLS`, constants_1.loggerCtx);
179
+ core_2.Logger.info(`Removed product '${product.sku}' (${product.id}) from QLS`, constants_1.loggerCtx);
178
180
  await waitToPreventRateLimit();
179
181
  }
180
- core_1.Logger.warn(`Removed ${allProducts.length} products from QLS for channel ${ctx.channel.token}`, constants_1.loggerCtx);
182
+ core_2.Logger.warn(`Removed ${allProducts.length} products from QLS for channel ${ctx.channel.token}`, constants_1.loggerCtx);
181
183
  }
182
184
  /**
183
185
  * Creates or updates the fulfillment products in QLS for the given product variants.
@@ -185,7 +187,7 @@ let QlsProductService = class QlsProductService {
185
187
  async syncVariants(ctx, productVariantIds) {
186
188
  const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
187
189
  if (!client) {
188
- core_1.Logger.debug(`QLS not enabled for channel ${ctx.channel.token}. Not handling product update/create.`, constants_1.loggerCtx);
190
+ core_2.Logger.debug(`QLS not enabled for channel ${ctx.channel.token}. Not handling product update/create.`, constants_1.loggerCtx);
189
191
  return {
190
192
  updatedInQls: [],
191
193
  createdInQls: [],
@@ -205,7 +207,7 @@ let QlsProductService = class QlsProductService {
205
207
  ]);
206
208
  if (!variant) {
207
209
  // Can happen when a variant is deleted from Vendure after the job was triggered
208
- core_1.Logger.info(`Variant with id ${variantId} not found. Not creating or updating product in QLS.`, constants_1.loggerCtx);
210
+ core_2.Logger.info(`Variant with id ${variantId} not found. Not creating or updating product in QLS.`, constants_1.loggerCtx);
209
211
  continue;
210
212
  }
211
213
  try {
@@ -221,7 +223,7 @@ let QlsProductService = class QlsProductService {
221
223
  catch (e) {
222
224
  const error = (0, catch_unknown_1.asError)(e);
223
225
  // Log as warning, because this is probably a functional mistake, i.e. duplicate barcodes or EANs
224
- core_1.Logger.warn(`Error syncing variant ${variantId} (${variant.sku}) to QLS: ${error.message}`, constants_1.loggerCtx);
226
+ core_2.Logger.warn(`Error syncing variant ${variantId} (${variant.sku}) to QLS: ${error.message}`, constants_1.loggerCtx);
225
227
  failed.push({ id: variantId });
226
228
  await this.eventBus.publish(new qls_variant_sync_failed_event_1.QlsVariantSyncFailedEvent(ctx, variant, new Date(), error.message));
227
229
  }
@@ -248,7 +250,7 @@ let QlsProductService = class QlsProductService {
248
250
  async triggerSyncVariants(ctx, productVariantIds) {
249
251
  const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
250
252
  if (!client) {
251
- core_1.Logger.debug(`QLS not enabled for channel ${ctx.channel.token}. Not triggering product sync.`, constants_1.loggerCtx);
253
+ core_2.Logger.debug(`QLS not enabled for channel ${ctx.channel.token}. Not triggering product sync.`, constants_1.loggerCtx);
252
254
  return;
253
255
  }
254
256
  return this.productJobQueue.add({
@@ -294,7 +296,7 @@ let QlsProductService = class QlsProductService {
294
296
  .filter((ean) => ean.length > 0);
295
297
  await Promise.all(eansToAdd.map((ean) => client.addBarcode(qlsProductId, ean)));
296
298
  if (eansToAdd.length > 0) {
297
- core_1.Logger.info(`Added additional EANs: ${eansToAdd.join(',')} to variant '${variant.sku}' in QLS`, constants_1.loggerCtx);
299
+ core_2.Logger.info(`Added additional EANs: ${eansToAdd.join(',')} to variant '${variant.sku}' in QLS`, constants_1.loggerCtx);
298
300
  }
299
301
  await this.triggerSyncVariants(ctx, [variantId]);
300
302
  return eansToAdd;
@@ -307,12 +309,12 @@ let QlsProductService = class QlsProductService {
307
309
  filter: { sku: { eq: sku } },
308
310
  });
309
311
  if (!result.items.length) {
310
- core_1.Logger.info(`Variant with sku '${sku}' not found, not updating stock`, constants_1.loggerCtx);
312
+ core_2.Logger.info(`Variant with sku '${sku}' not found, not updating stock`, constants_1.loggerCtx);
311
313
  return false;
312
314
  }
313
315
  const variant = result.items[0];
314
316
  if (result.items.length > 1) {
315
- core_1.Logger.error(`Multiple variants found for sku '${sku}', using '${variant.id}'`, constants_1.loggerCtx);
317
+ core_2.Logger.error(`Multiple variants found for sku '${sku}', using '${variant.id}'`, constants_1.loggerCtx);
316
318
  }
317
319
  return await this.updateStock(ctx, variant.id, availableStock);
318
320
  }
@@ -324,22 +326,22 @@ let QlsProductService = class QlsProductService {
324
326
  filter: { sku: { eq: qlsProduct.sku } },
325
327
  });
326
328
  if (!result.items.length) {
327
- core_1.Logger.info(`Variant with sku '${qlsProduct.sku}' not found, not handling stock webhook`, constants_1.loggerCtx);
329
+ core_2.Logger.info(`Variant with sku '${qlsProduct.sku}' not found, not handling stock webhook`, constants_1.loggerCtx);
328
330
  return false;
329
331
  }
330
332
  const variant = result.items[0];
331
333
  if (result.items.length > 1) {
332
- core_1.Logger.error(`Multiple variants found for sku '${qlsProduct.sku}', using '${variant.id}'`, constants_1.loggerCtx);
334
+ core_2.Logger.error(`Multiple variants found for sku '${qlsProduct.sku}', using '${variant.id}'`, constants_1.loggerCtx);
333
335
  }
334
336
  await this.updateStockBySku(ctx, qlsProduct.sku, qlsProduct.amount_available);
335
337
  if (!this.options.productSync.saveAdditionalVariantData) {
336
338
  return true;
337
339
  }
338
340
  try {
339
- await this.options.productSync.saveAdditionalVariantData(ctx, new core_1.Injector(this.moduleRef), qlsProduct, variant);
341
+ await this.options.productSync.saveAdditionalVariantData(ctx, new core_2.Injector(this.moduleRef), qlsProduct, variant);
340
342
  }
341
343
  catch (e) {
342
- core_1.Logger.error(`Error in saveAdditionalData for variant '${variant.sku}': ${(0, catch_unknown_1.asError)(e).message}`, constants_1.loggerCtx);
344
+ core_2.Logger.error(`Error in saveAdditionalData for variant '${variant.sku}': ${(0, catch_unknown_1.asError)(e).message}`, constants_1.loggerCtx);
343
345
  }
344
346
  return true;
345
347
  }
@@ -347,17 +349,17 @@ let QlsProductService = class QlsProductService {
347
349
  * Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
348
350
  */
349
351
  async createOrUpdateProductInQls(ctx, client, variant, existingProduct) {
350
- if (await this.options.productSync.excludeVariantFromSync?.(ctx, new core_1.Injector(this.moduleRef), variant)) {
351
- core_1.Logger.info(`Variant '${variant.sku}' excluded from sync to QLS.`, constants_1.loggerCtx);
352
+ if (await this.options.productSync.excludeVariantFromSync?.(ctx, new core_2.Injector(this.moduleRef), variant)) {
353
+ core_2.Logger.info(`Variant '${variant.sku}' excluded from sync to QLS.`, constants_1.loggerCtx);
352
354
  return { status: 'not-changed' };
353
355
  }
354
356
  if (!variant.enabled || !variant.product?.enabled) {
355
357
  const disabledEntity = !variant.enabled ? 'Variant' : 'Product';
356
358
  if (existingProduct) {
357
- core_1.Logger.warn(`${disabledEntity} '${variant.sku}' is disabled but exists in QLS (product ID ${existingProduct.id}). Skipping sync.`, constants_1.loggerCtx);
359
+ core_2.Logger.warn(`${disabledEntity} '${variant.sku}' is disabled but exists in QLS (product ID ${existingProduct.id}). Skipping sync.`, constants_1.loggerCtx);
358
360
  }
359
361
  else {
360
- core_1.Logger.info(`${disabledEntity} '${variant.sku}' is disabled, skipping sync to QLS.`, constants_1.loggerCtx);
362
+ core_2.Logger.info(`${disabledEntity} '${variant.sku}' is disabled, skipping sync to QLS.`, constants_1.loggerCtx);
361
363
  }
362
364
  return { status: 'not-changed', qlsProductId: existingProduct?.id };
363
365
  }
@@ -371,7 +373,7 @@ let QlsProductService = class QlsProductService {
371
373
  ...additionalVariantFields,
372
374
  });
373
375
  qlsProduct = result;
374
- core_1.Logger.info(`Created product '${variant.sku}' in QLS`, constants_1.loggerCtx);
376
+ core_2.Logger.info(`Created product '${variant.sku}' in QLS`, constants_1.loggerCtx);
375
377
  createdOrUpdated = 'created';
376
378
  }
377
379
  else if (this.shouldUpdateProductInQls(variant, existingProduct, additionalVariantFields)) {
@@ -380,21 +382,21 @@ let QlsProductService = class QlsProductService {
380
382
  name: variant.name,
381
383
  ...additionalVariantFields,
382
384
  });
383
- core_1.Logger.info(`Updated product '${variant.sku}' in QLS`, constants_1.loggerCtx);
385
+ core_2.Logger.info(`Updated product '${variant.sku}' in QLS`, constants_1.loggerCtx);
384
386
  createdOrUpdated = 'updated';
385
387
  }
386
388
  if (qlsProduct && qlsProduct?.id !== variant.customFields.qlsProductId) {
387
389
  // Update variant with QLS product ID if it changed
388
390
  // Do not use variantService.update because it will trigger a change event and cause an infinite loop
389
391
  await this.connection
390
- .getRepository(ctx, core_1.ProductVariant)
392
+ .getRepository(ctx, core_2.ProductVariant)
391
393
  .update({ id: variant.id }, { customFields: { qlsProductId: qlsProduct.id } });
392
- core_1.Logger.info(`Set QLS product ID for variant '${variant.sku}' to ${qlsProduct.id}`, constants_1.loggerCtx);
394
+ core_2.Logger.info(`Set QLS product ID for variant '${variant.sku}' to ${qlsProduct.id}`, constants_1.loggerCtx);
393
395
  }
394
396
  // Note: additional EANs are no longer synced automatically during product sync.
395
397
  // Use the addAdditionalEANsToQls mutation to add EANs manually.
396
398
  if (createdOrUpdated === 'not-changed') {
397
- core_1.Logger.info(`Variant '${variant.sku}' not updated in QLS, because no changes were found.`, constants_1.loggerCtx);
399
+ core_2.Logger.info(`Variant '${variant.sku}' not updated in QLS, because no changes were found.`, constants_1.loggerCtx);
398
400
  }
399
401
  return { status: createdOrUpdated, qlsProductId: qlsProduct?.id };
400
402
  }
@@ -426,7 +428,7 @@ let QlsProductService = class QlsProductService {
426
428
  'product.featuredAsset',
427
429
  ];
428
430
  const [items, totalItems] = await this.listQueryBuilder
429
- .build(core_1.ProductVariant, {
431
+ .build(core_2.ProductVariant, {
430
432
  skip,
431
433
  take,
432
434
  }, {
@@ -437,7 +439,7 @@ let QlsProductService = class QlsProductService {
437
439
  })
438
440
  .getManyAndCount();
439
441
  let variants = await Promise.all(items.map(async (item) => this.productPriceApplicator.applyChannelPriceAndTax(item, ctx, undefined, false)));
440
- variants = variants.map((v) => (0, core_1.translateDeep)(v, ctx.languageCode));
442
+ variants = variants.map((v) => (0, core_2.translateDeep)(v, ctx.languageCode));
441
443
  allVariants.push(...variants);
442
444
  if (allVariants.length >= totalItems) {
443
445
  hasMore = false;
@@ -452,7 +454,7 @@ let QlsProductService = class QlsProductService {
452
454
  */
453
455
  async updateStock(ctx, variantId, availableStock) {
454
456
  if (!this.options.productSync.synchronizeStockLevels) {
455
- core_1.Logger.warn(`Stock sync disabled. Not updating stock for variant '${variantId}'`, constants_1.loggerCtx);
457
+ core_2.Logger.warn(`Stock sync disabled. Not updating stock for variant '${variantId}'`, constants_1.loggerCtx);
456
458
  return false;
457
459
  }
458
460
  // Find default Stock Location
@@ -461,28 +463,28 @@ let QlsProductService = class QlsProductService {
461
463
  const stockLevel = await this.stockLevelService.getStockLevel(ctx, variantId, defaultStockLocation.id);
462
464
  const delta = availableStock - stockLevel.stockOnHand;
463
465
  if (delta !== 0) {
464
- const adjustment = new core_1.StockAdjustment({
466
+ const adjustment = new core_2.StockAdjustment({
465
467
  quantity: delta,
466
468
  stockLocation: { id: defaultStockLocation.id },
467
469
  productVariant: { id: variantId },
468
470
  });
469
471
  // Update stock level
470
- await this.connection.getRepository(ctx, core_1.StockLevel).save({
472
+ await this.connection.getRepository(ctx, core_2.StockLevel).save({
471
473
  id: stockLevel.id,
472
474
  stockOnHand: availableStock,
473
475
  stockAllocated: 0, // Reset allocations, because allocation is handled by QLS
474
476
  });
475
- await this.eventBus.publish(new core_1.StockMovementEvent(ctx, [adjustment]));
477
+ await this.eventBus.publish(new core_2.StockMovementEvent(ctx, [adjustment]));
476
478
  }
477
479
  else {
478
480
  // No stock change, but still reset allocations to ensure consistency
479
- await this.connection.getRepository(ctx, core_1.StockLevel).save({
481
+ await this.connection.getRepository(ctx, core_2.StockLevel).save({
480
482
  id: stockLevel.id,
481
483
  stockOnHand: availableStock,
482
484
  stockAllocated: 0, // Reset allocations, because allocation is handled by QLS
483
485
  });
484
486
  }
485
- core_1.Logger.info(`Updated stock for variant ${variantId} to ${availableStock}`, constants_1.loggerCtx);
487
+ core_2.Logger.info(`Updated stock for variant ${variantId} to ${availableStock}`, constants_1.loggerCtx);
486
488
  return delta !== 0;
487
489
  }
488
490
  };
@@ -490,12 +492,12 @@ exports.QlsProductService = QlsProductService;
490
492
  exports.QlsProductService = QlsProductService = __decorate([
491
493
  (0, common_1.Injectable)(),
492
494
  __param(1, (0, common_1.Inject)(constants_1.PLUGIN_INIT_OPTIONS)),
493
- __metadata("design:paramtypes", [core_1.TransactionalConnection, Object, core_1.JobQueueService,
494
- core_1.StockLevelService,
495
- core_1.ProductVariantService,
496
- core_1.StockLocationService,
497
- core_1.EventBus,
498
- core_1.ListQueryBuilder,
499
- core_1.ProductPriceApplicator,
500
- core_2.ModuleRef])
495
+ __metadata("design:paramtypes", [core_2.TransactionalConnection, Object, core_2.JobQueueService,
496
+ core_2.StockLevelService,
497
+ core_2.ProductVariantService,
498
+ core_2.StockLocationService,
499
+ core_2.EventBus,
500
+ core_2.ListQueryBuilder,
501
+ core_2.ProductPriceApplicator,
502
+ core_1.ModuleRef])
501
503
  ], QlsProductService);
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.4.0",
4
4
  "description": "Vendure plugin to fulfill orders via QLS.",
5
5
  "keywords": [
6
6
  "fulfillment",
@@ -8,7 +8,7 @@
8
8
  "qls"
9
9
  ],
10
10
  "author": "Martijn van de Brug <martijn@pinelab.studio>",
11
- "homepage": "https://plugins.pinelab.studio/",
11
+ "homepage": "https://engineering.pinelab.studio/",
12
12
  "repository": "https://github.com/Pinelab-studio/pinelab-vendure-plugins",
13
13
  "license": "MIT",
14
14
  "private": false,
@@ -23,13 +23,13 @@
23
23
  "CHANGELOG.md"
24
24
  ],
25
25
  "scripts": {
26
- "build": "rimraf dist && yarn generate && tsc && copyfiles -u 1 'src/ui/**/*' 'src/dashboard/**/*' dist/",
26
+ "build": "rimraf dist && yarn generate && tsc && copyfiles -u 1 'src/dashboard/**/*' dist/",
27
27
  "build:dashboard": "vite build --config ./vite.config.mts",
28
28
  "dev:dashboard": "vite --clearScreen false",
29
29
  "start": "nodemon --watch src --exec ts-node test/dev-server.ts",
30
30
  "generate": "graphql-codegen",
31
31
  "test": "vitest run --bail 1",
32
- "lint": "eslint ."
32
+ "lint": "biome check ."
33
33
  },
34
34
  "dependencies": {
35
35
  "catch-unknown": "^2.0.0"
@@ -1,246 +0,0 @@
1
- import {
2
- addActionBarDropdownMenuItem,
3
- addActionBarItem,
4
- ModalService,
5
- } from '@vendure/admin-ui/core';
6
- import { firstValueFrom, of } from 'rxjs';
7
- import { map, catchError, take } from 'rxjs/operators';
8
- import gql from 'graphql-tag';
9
-
10
- export default [
11
- // Product sync button in product list
12
- addActionBarDropdownMenuItem({
13
- id: 'qls-fulfillment-sync',
14
- label: 'Synchronize with QLS',
15
- locationId: 'product-list',
16
- icon: 'resistor',
17
- requiresPermission: ['QLSFullSync'],
18
- onClick: (_, { dataService, notificationService }) => {
19
- dataService
20
- .mutate(
21
- gql`
22
- mutation TriggerQlsProductSync {
23
- triggerQlsProductSync
24
- }
25
- `
26
- )
27
- .subscribe({
28
- next: () => {
29
- notificationService.notify({
30
- message: `Triggered QLS full product sync...`,
31
- type: 'success',
32
- });
33
- },
34
- error: (err) => {
35
- notificationService.notify({
36
- message: err.message,
37
- type: 'error',
38
- });
39
- },
40
- });
41
- },
42
- }),
43
- // QLS order link button on order detail page
44
- addActionBarItem({
45
- id: 'qls-visit-order',
46
- label: 'QLS',
47
- locationId: 'order-detail',
48
- buttonColor: 'primary',
49
- buttonStyle: 'outline',
50
- icon: 'link',
51
- buttonState: (context) => {
52
- return context.entity$.pipe(
53
- map((entity) => ({
54
- visible: !!entity?.orderPlacedAt,
55
- disabled: false,
56
- }))
57
- );
58
- },
59
- onClick: async (event, { route, dataService, notificationService }) => {
60
- const orderId = route.snapshot.params.id;
61
- const res = await firstValueFrom(
62
- dataService.query(
63
- gql`
64
- query Order($orderId: ID!) {
65
- order(id: $orderId) {
66
- id
67
- qlsOrderUrl
68
- }
69
- }
70
- `,
71
- { orderId }
72
- ).single$
73
- );
74
- const url = (res as any).order.qlsOrderUrl;
75
- if (url) {
76
- window.open(url, '_blank');
77
- } else {
78
- notificationService.notify({
79
- message: 'No QLS order found for this order.',
80
- type: 'error',
81
- });
82
- }
83
- },
84
- }),
85
- // QLS product link button on product variant detail page
86
- addActionBarItem({
87
- id: 'qls-visit-product',
88
- label: 'QLS',
89
- locationId: 'product-variant-detail',
90
- buttonColor: 'primary',
91
- buttonStyle: 'outline',
92
- icon: 'link',
93
- onClick: async (event, { route, dataService, notificationService }) => {
94
- const variantId = route.snapshot.params.id;
95
- const res = await firstValueFrom(
96
- dataService.query(
97
- gql`
98
- query ProductVariant($id: ID!) {
99
- productVariant(id: $id) {
100
- id
101
- qlsProductUrl
102
- }
103
- }
104
- `,
105
- { id: variantId }
106
- ).single$
107
- );
108
- const url = (res as any).productVariant.qlsProductUrl;
109
- if (url) {
110
- window.open(url, '_blank');
111
- } else {
112
- notificationService.notify({
113
- message: 'No QLS product found for this variant.',
114
- type: 'error',
115
- });
116
- }
117
- },
118
- }),
119
- // Push order to QLS button on order detail page
120
- addActionBarDropdownMenuItem({
121
- id: 'qls-fulfillment-push-order',
122
- label: 'Push order to QLS',
123
- locationId: 'order-detail',
124
- icon: 'resistor',
125
- requiresPermission: ['QLSFullSync'],
126
- hasDivider: true,
127
- onClick: async (
128
- _,
129
- { route, dataService, notificationService, injector }
130
- ) => {
131
- const orderId = route.snapshot.params.id;
132
- const res = await firstValueFrom(
133
- dataService.query(
134
- gql`
135
- query Order($orderId: ID!) {
136
- order(id: $orderId) {
137
- id
138
- qlsOrderIds
139
- }
140
- }
141
- `,
142
- { orderId }
143
- ).single$
144
- );
145
- if ((res as any).order.qlsOrderIds.length > 0) {
146
- const modalService = injector.get(ModalService);
147
- const confirmed = await firstValueFrom(
148
- modalService.dialog({
149
- title: 'Push order to QLS',
150
- body: 'This order already exists in QLS. Are you sure you want to push it again?',
151
- buttons: [
152
- { type: 'secondary', label: 'Cancel', returnValue: false },
153
- { type: 'primary', label: 'Push to QLS', returnValue: true },
154
- ],
155
- })
156
- );
157
- if (!confirmed) return;
158
- }
159
-
160
- dataService
161
- .mutate(
162
- gql`
163
- mutation PushOrderToQls($orderId: ID!) {
164
- pushOrderToQls(orderId: $orderId)
165
- }
166
- `,
167
- { orderId }
168
- )
169
- .subscribe({
170
- next: (result) => {
171
- notificationService.notify({
172
- message: (result as any).pushOrderToQls,
173
- type: 'success',
174
- });
175
- },
176
- error: (err) => {
177
- notificationService.notify({
178
- message: err.message,
179
- type: 'error',
180
- });
181
- },
182
- });
183
- },
184
- }),
185
- addActionBarDropdownMenuItem({
186
- id: 'qls-fulfillment-add-ean',
187
- label: 'Add additional EAN to QLS',
188
- locationId: 'product-variant-detail',
189
- icon: 'resistor',
190
- requiresPermission: ['QLSFullSync'],
191
- onClick: async (
192
- _,
193
- { route, dataService, notificationService, entity$ }
194
- ) => {
195
- const ean = window.prompt('Enter additional EAN to add to QLS:');
196
- if (!ean) {
197
- return;
198
- }
199
- const variantId = route.snapshot.params.id;
200
- try {
201
- const variant = await firstValueFrom(entity$.pipe(take(1)));
202
- const confirmed = window.confirm(
203
- `Wil je EAN "${ean}" toevoegen aan "${variant?.name}"?`
204
- );
205
- if (!confirmed) {
206
- return;
207
- }
208
- } catch (err) {
209
- notificationService.notify({
210
- message: (err as any).message,
211
- type: 'error',
212
- });
213
- return;
214
- }
215
- dataService
216
- .mutate(
217
- gql`
218
- mutation AddAdditionalEANSToQLS(
219
- $variantId: ID!
220
- $additionalEANS: [String!]!
221
- ) {
222
- addAdditionalEANSToQLS(
223
- variantId: $variantId
224
- additionalEANS: $additionalEANS
225
- )
226
- }
227
- `,
228
- { variantId, additionalEANS: [ean] }
229
- )
230
- .subscribe({
231
- next: () => {
232
- notificationService.notify({
233
- message: `Added EAN ${ean} to QLS`,
234
- type: 'success',
235
- });
236
- },
237
- error: (err) => {
238
- notificationService.notify({
239
- message: err.message,
240
- type: 'error',
241
- });
242
- },
243
- });
244
- },
245
- }),
246
- ];