@pinelab/vendure-plugin-qls-fulfillment 3.0.0 → 3.2.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,13 @@
1
+ # 3.2.0 (2026-08-12)
2
+
3
+ - Restore the legacy Angular Admin UI extension alongside the React Dashboard.
4
+
5
+ # 3.1.0 (2026-08-12)
6
+
7
+ - Trigger `saveAdditionalVariantData` from incoming stock webhooks using the webhook payload directly.
8
+ - Avoid fetching the QLS product entity during this flow.
9
+ - Add webhook coverage for additional variant data.
10
+
1
11
  # 3.0.0 (2026-07-29)
2
12
 
3
13
  - Migrated to React Dashboard.
@@ -40,7 +40,7 @@ let QlsWebhooksController = class QlsWebhooksController {
40
40
  return core_1.Logger.error(`Incoming webhook with invalid channel token for channel '${channelToken}' to '${request.url}'`, constants_1.loggerCtx, JSON.stringify(body));
41
41
  }
42
42
  if (isStockWebhook(body)) {
43
- await this.qlsProductService.updateStockBySku(ctx, body.sku, body.amount_available);
43
+ await this.qlsProductService.handleStockWebhook(ctx, body);
44
44
  }
45
45
  else if (isOrderWebhook(body)) {
46
46
  await this.qlsOrderService.handleOrderStatusUpdate(ctx, body);
@@ -163,7 +163,7 @@ export interface FulfillmentOrder {
163
163
  product: unknown;
164
164
  }>;
165
165
  }
166
- export type IncomingStockWebhook = Pick<FulfillmentProduct, 'sku' | 'amount_available'>;
166
+ export type IncomingStockWebhook = FulfillmentProductDetail;
167
167
  export type IncomingOrderWebhook = Pick<FulfillmentOrder, 'customer_reference' | 'status' | 'cancelled' | 'amount_delivered' | 'amount_total'>;
168
168
  /**
169
169
  * This is the full data returend by the QLS API when fetching a fulfillment product by ID
@@ -1,6 +1,6 @@
1
1
  import { OnApplicationBootstrap, OnModuleInit } from '@nestjs/common';
2
2
  import { EventBus, ID, Job, JobQueueService, ListQueryBuilder, ProductPriceApplicator, ProductVariant, ProductVariantService, RequestContext, StockLevelService, StockLocationService, TransactionalConnection } from '@vendure/core';
3
- import { FulfillmentProduct } from '../lib/client-types';
3
+ import { FulfillmentProduct, IncomingStockWebhook } from '../lib/client-types';
4
4
  import { QlsClient } from '../lib/qls-client';
5
5
  import { QlsPluginOptions, QlsProductJobData } from '../types';
6
6
  import { ModuleRef } from '@nestjs/core';
@@ -66,10 +66,13 @@ export declare class QlsProductService implements OnModuleInit, OnApplicationBoo
66
66
  */
67
67
  addAdditionalEANsToQls(ctx: RequestContext, variantId: ID, additionalEANs: string[]): Promise<string[]>;
68
68
  /**
69
- * Update the stock level for a variant based on the given available stock.
70
- * Returns true if the stock changed
69
+ * Update the stock level for a variant based on its SKU.
71
70
  */
72
71
  updateStockBySku(ctx: RequestContext, sku: string, availableStock: number): Promise<boolean>;
72
+ /**
73
+ * Handles an incoming stock webhook and saves any additional variant data.
74
+ */
75
+ handleStockWebhook(ctx: RequestContext, qlsProduct: IncomingStockWebhook): Promise<boolean>;
73
76
  /**
74
77
  * Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
75
78
  */
@@ -217,18 +217,6 @@ let QlsProductService = class QlsProductService {
217
217
  else if (result.status === 'updated') {
218
218
  updatedInQls.push(variant);
219
219
  }
220
- if (result.qlsProductId &&
221
- this.options.productSync.saveAdditionalVariantData) {
222
- try {
223
- const qlsProduct = await client.getFulfillmentProductById(result.qlsProductId);
224
- if (qlsProduct) {
225
- await this.options.productSync.saveAdditionalVariantData(ctx, new core_1.Injector(this.moduleRef), qlsProduct, variant);
226
- }
227
- }
228
- catch (e) {
229
- core_1.Logger.error(`Error in saveAdditionalData for variant '${variant.sku}': ${(0, catch_unknown_1.asError)(e).message}`, constants_1.loggerCtx);
230
- }
231
- }
232
220
  }
233
221
  catch (e) {
234
222
  const error = (0, catch_unknown_1.asError)(e);
@@ -312,8 +300,7 @@ let QlsProductService = class QlsProductService {
312
300
  return eansToAdd;
313
301
  }
314
302
  /**
315
- * Update the stock level for a variant based on the given available stock.
316
- * Returns true if the stock changed
303
+ * Update the stock level for a variant based on its SKU.
317
304
  */
318
305
  async updateStockBySku(ctx, sku, availableStock) {
319
306
  const result = await this.variantService.findAll(ctx, {
@@ -329,6 +316,33 @@ let QlsProductService = class QlsProductService {
329
316
  }
330
317
  return await this.updateStock(ctx, variant.id, availableStock);
331
318
  }
319
+ /**
320
+ * Handles an incoming stock webhook and saves any additional variant data.
321
+ */
322
+ async handleStockWebhook(ctx, qlsProduct) {
323
+ const result = await this.variantService.findAll(ctx, {
324
+ filter: { sku: { eq: qlsProduct.sku } },
325
+ });
326
+ if (!result.items.length) {
327
+ core_1.Logger.info(`Variant with sku '${qlsProduct.sku}' not found, not handling stock webhook`, constants_1.loggerCtx);
328
+ return false;
329
+ }
330
+ const variant = result.items[0];
331
+ if (result.items.length > 1) {
332
+ core_1.Logger.error(`Multiple variants found for sku '${qlsProduct.sku}', using '${variant.id}'`, constants_1.loggerCtx);
333
+ }
334
+ await this.updateStockBySku(ctx, qlsProduct.sku, qlsProduct.amount_available);
335
+ if (!this.options.productSync.saveAdditionalVariantData) {
336
+ return true;
337
+ }
338
+ try {
339
+ await this.options.productSync.saveAdditionalVariantData(ctx, new core_1.Injector(this.moduleRef), qlsProduct, variant);
340
+ }
341
+ catch (e) {
342
+ core_1.Logger.error(`Error in saveAdditionalData for variant '${variant.sku}': ${(0, catch_unknown_1.asError)(e).message}`, constants_1.loggerCtx);
343
+ }
344
+ return true;
345
+ }
332
346
  /**
333
347
  * Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
334
348
  */
@@ -0,0 +1,246 @@
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
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pinelab/vendure-plugin-qls-fulfillment",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "Vendure plugin to fulfill orders via QLS.",
5
5
  "keywords": [
6
6
  "fulfillment",