@pinelab/vendure-plugin-qls-fulfillment 1.0.0-beta.1 → 1.0.0-beta.10

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/README.md CHANGED
@@ -58,7 +58,12 @@ Make sure to monitor failed jobs: A job that failed after its retries were exhau
58
58
  1. An order was not pushed to QLS
59
59
  2. A product was not synced to QLS
60
60
 
61
- Monitor your logs for the string `QLS webhook error`. This means an incoming stock update webhook was not processed correctly.
61
+ Monitor your logs for the following text:
62
+
63
+ - `QLS webhook error` - This means an incoming stock update webhook was not processed correctly.
64
+ - `Error creating or updating variant` - This means a product was not synced to QLS.
65
+
66
+ Make sure to filter by logger context `QlsPlugin`, to prevent false positive alerts.
62
67
 
63
68
  ## Cancelling orders and manually pushing orders to QLS
64
69
 
@@ -30,6 +30,9 @@ export type FulfillmentProduct = {
30
30
  collection_id: string | null;
31
31
  dimensions: unknown;
32
32
  article_number: string | null;
33
+ /**
34
+ * Main EAN of the product
35
+ */
33
36
  ean: string;
34
37
  name: string;
35
38
  sku: string;
@@ -53,8 +56,18 @@ export type FulfillmentProduct = {
53
56
  amount_blocked: number;
54
57
  status: string | null;
55
58
  suppliers: unknown[];
56
- barcodes: string[];
59
+ barcodes: Array<{
60
+ id: number;
61
+ fulfillment_product_id: string;
62
+ company_id: string;
63
+ barcode: string;
64
+ created: string;
65
+ modified: string;
66
+ }>;
57
67
  image_url_handheld: string | null;
68
+ /**
69
+ * All EANs of the product, including the main EAN
70
+ */
58
71
  barcodes_and_ean: string[];
59
72
  };
60
73
  export interface FulfillmentOrderInput {
@@ -22,5 +22,13 @@ export declare class QlsClient {
22
22
  createFulfillmentProduct(data: FulfillmentProductInput): Promise<FulfillmentProduct>;
23
23
  updateFulfillmentProduct(fulfillmentProductId: string, data: FulfillmentProductInput): Promise<FulfillmentProduct>;
24
24
  createFulfillmentOrder(data: Omit<FulfillmentOrderInput, 'brand_id'>): Promise<FulfillmentOrder>;
25
+ /**
26
+ * Add an extra barcode to a fulfillment product in QLS
27
+ */
28
+ addBarcode(productId: string, barcode: string): Promise<void>;
29
+ /**
30
+ * Add an extra barcode to a fulfillment product in QLS
31
+ */
32
+ removeBarcode(productId: string, barcodeId: number): Promise<void>;
25
33
  rawRequest<T>(method: 'POST' | 'GET' | 'PUT' | 'DELETE', action: string, data?: unknown): Promise<QlsApiResponse<T>>;
26
34
  }
@@ -50,7 +50,8 @@ class QlsClient {
50
50
  allProducts.push(...result.data);
51
51
  hasNextPage = result.pagination?.nextPage ?? false;
52
52
  page++;
53
- await new Promise((resolve) => setTimeout(resolve, 1100)); // Limit to 55/minute, because the rate limit is 60/minute
53
+ core_1.Logger.info(`Fetched products ${allProducts.length}/${result.pagination?.count}`, constants_1.loggerCtx);
54
+ await new Promise((resolve) => setTimeout(resolve, 700)); // Limit to ~86/minute, because the rate limit is 500/5 minutes
54
55
  }
55
56
  return allProducts;
56
57
  }
@@ -69,6 +70,20 @@ class QlsClient {
69
70
  });
70
71
  return response.data;
71
72
  }
73
+ /**
74
+ * Add an extra barcode to a fulfillment product in QLS
75
+ */
76
+ async addBarcode(productId, barcode) {
77
+ await this.rawRequest('POST', `fulfillment/products/${productId}/barcodes`, {
78
+ barcode,
79
+ });
80
+ }
81
+ /**
82
+ * Add an extra barcode to a fulfillment product in QLS
83
+ */
84
+ async removeBarcode(productId, barcodeId) {
85
+ await this.rawRequest('DELETE', `fulfillment/products/${productId}/barcodes/${barcodeId}`);
86
+ }
72
87
  async rawRequest(method, action, data) {
73
88
  // Set headers
74
89
  const headers = {
@@ -86,7 +101,7 @@ class QlsClient {
86
101
  if (!response.ok) {
87
102
  const errorText = await response.text();
88
103
  // Log error including the request body
89
- core_1.Logger.error(`QLS request failed: ${response.status} ${response.statusText} - ${errorText}`, constants_1.loggerCtx, data ? JSON.stringify(data, null, 2) : undefined);
104
+ core_1.Logger.error(`QLS request to '${url}' failed: ${response.status} ${response.statusText} - ${errorText}`, constants_1.loggerCtx, data ? JSON.stringify(data, null, 2) : undefined);
90
105
  throw new Error(errorText);
91
106
  }
92
107
  const contentType = response.headers.get('content-type') ?? '';
@@ -1,12 +1,13 @@
1
1
  import { OnApplicationBootstrap, OnModuleInit } from '@nestjs/common';
2
- import { EventBus, ID, Job, JobQueueService, ProductVariant, ProductVariantService, RequestContext, StockLevelService, StockLocationService, TransactionalConnection } from '@vendure/core';
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
4
  import { QlsClient } from '../lib/qls-client';
4
5
  import { QlsPluginOptions, QlsProductJobData } from '../types';
5
- import { FulfillmentProduct } from '../lib/client-types';
6
6
  type SyncProductsJobResult = {
7
7
  updatedInQls: number;
8
8
  createdInQls: number;
9
9
  updatedStock: number;
10
+ failed: number;
10
11
  };
11
12
  export declare class QlsProductService implements OnModuleInit, OnApplicationBootstrap {
12
13
  private connection;
@@ -16,8 +17,10 @@ export declare class QlsProductService implements OnModuleInit, OnApplicationBoo
16
17
  private readonly variantService;
17
18
  private readonly stockLocationService;
18
19
  private readonly eventBus;
20
+ private readonly listQueryBuilder;
21
+ private readonly productPriceApplicator;
19
22
  private productJobQueue;
20
- constructor(connection: TransactionalConnection, options: QlsPluginOptions, jobQueueService: JobQueueService, stockLevelService: StockLevelService, variantService: ProductVariantService, stockLocationService: StockLocationService, eventBus: EventBus);
23
+ constructor(connection: TransactionalConnection, options: QlsPluginOptions, jobQueueService: JobQueueService, stockLevelService: StockLevelService, variantService: ProductVariantService, stockLocationService: StockLocationService, eventBus: EventBus, listQueryBuilder: ListQueryBuilder, productPriceApplicator: ProductPriceApplicator);
21
24
  onApplicationBootstrap(): void;
22
25
  onModuleInit(): Promise<void>;
23
26
  /**
@@ -53,6 +56,10 @@ export declare class QlsProductService implements OnModuleInit, OnApplicationBoo
53
56
  * Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
54
57
  */
55
58
  createOrUpdateProductInQls(ctx: RequestContext, client: QlsClient, variant: ProductVariant, existingProduct: FulfillmentProduct | null): Promise<'created' | 'updated' | 'not-changed'>;
59
+ /**
60
+ * Update the additional EANs/barcodes for a product in QLS if needed
61
+ */
62
+ private updateAdditionalEANs;
56
63
  /**
57
64
  * Determine if a product needs to be updated in QLS based on the given variant and QLS product.
58
65
  */
@@ -19,11 +19,13 @@ exports.QlsProductService = void 0;
19
19
  const common_1 = require("@nestjs/common");
20
20
  const core_1 = require("@vendure/core");
21
21
  const catch_unknown_1 = require("catch-unknown");
22
+ const typeorm_1 = require("typeorm");
22
23
  const util_1 = __importDefault(require("util"));
23
24
  const constants_1 = require("../constants");
24
25
  const qls_client_1 = require("../lib/qls-client");
26
+ const util_2 = require("./util");
25
27
  let QlsProductService = class QlsProductService {
26
- constructor(connection, options, jobQueueService, stockLevelService, variantService, stockLocationService, eventBus) {
28
+ constructor(connection, options, jobQueueService, stockLevelService, variantService, stockLocationService, eventBus, listQueryBuilder, productPriceApplicator) {
27
29
  this.connection = connection;
28
30
  this.options = options;
29
31
  this.jobQueueService = jobQueueService;
@@ -31,6 +33,8 @@ let QlsProductService = class QlsProductService {
31
33
  this.variantService = variantService;
32
34
  this.stockLocationService = stockLocationService;
33
35
  this.eventBus = eventBus;
36
+ this.listQueryBuilder = listQueryBuilder;
37
+ this.productPriceApplicator = productPriceApplicator;
34
38
  }
35
39
  onApplicationBootstrap() {
36
40
  // Listen for ProductVariantEvent and add a job to the queue
@@ -107,22 +111,36 @@ let QlsProductService = class QlsProductService {
107
111
  // Create or update products in QLS
108
112
  let createdQlsProductsCount = 0;
109
113
  let updatedQlsProductsCount = 0;
114
+ let failedCount = 0;
110
115
  for (const variant of allVariants) {
111
- const existingQlsProduct = allQlsProducts.find((p) => p.sku == variant.sku);
112
- const result = await this.createOrUpdateProductInQls(ctx, client, variant, existingQlsProduct ?? null);
113
- if (result === 'created') {
114
- createdQlsProductsCount += 1;
116
+ try {
117
+ const existingQlsProduct = allQlsProducts.find((p) => p.sku == variant.sku);
118
+ const result = await this.createOrUpdateProductInQls(ctx, client, variant, existingQlsProduct ?? null);
119
+ if (result === 'created') {
120
+ createdQlsProductsCount += 1;
121
+ }
122
+ else if (result === 'updated') {
123
+ updatedQlsProductsCount += 1;
124
+ }
125
+ if (result === 'created' || result === 'updated') {
126
+ // Wait for 700ms to avoid rate limit of 500/5 minutes, but only if we created or updated a product, otherwise no calls have been made yet.
127
+ await new Promise((resolve) => setTimeout(resolve, 700));
128
+ }
115
129
  }
116
- else if (result === 'updated') {
117
- updatedQlsProductsCount += 1;
130
+ catch (e) {
131
+ const error = (0, catch_unknown_1.asError)(e);
132
+ core_1.Logger.error(`Error creating or updating variant '${variant.sku}' in QLS: ${error.message}`, constants_1.loggerCtx, error.stack);
133
+ failedCount += 1;
118
134
  }
119
135
  }
120
136
  core_1.Logger.info(`Created ${createdQlsProductsCount} products in QLS`, constants_1.loggerCtx);
121
137
  core_1.Logger.info(`Updated ${updatedQlsProductsCount} products in QLS`, constants_1.loggerCtx);
138
+ core_1.Logger.info(`Finished full sync with ${failedCount} failures`, constants_1.loggerCtx);
122
139
  return {
123
140
  updatedInQls: updatedQlsProductsCount,
124
141
  createdInQls: createdQlsProductsCount,
125
142
  updatedStock: updateStockCount,
143
+ failed: failedCount,
126
144
  };
127
145
  }
128
146
  catch (e) {
@@ -142,29 +160,44 @@ let QlsProductService = class QlsProductService {
142
160
  updatedInQls: 0,
143
161
  createdInQls: 0,
144
162
  updatedStock: 0,
163
+ failed: 0,
145
164
  };
146
165
  }
147
166
  let updatedInQls = 0;
148
167
  let createdInQls = 0;
168
+ let failedCount = 0;
149
169
  for (const variantId of productVariantIds) {
150
- const variant = await this.variantService.findOne(ctx, variantId);
151
- if (!variant) {
152
- core_1.Logger.error(`Variant with id ${variantId} not found. Not creating or updating product in QLS.`, constants_1.loggerCtx);
153
- continue;
154
- }
155
- const existingQlsProduct = await client.getFulfillmentProductBySku(variant.sku);
156
- const result = await this.createOrUpdateProductInQls(ctx, client, variant, existingQlsProduct ?? null);
157
- if (result === 'created') {
158
- createdInQls += 1;
170
+ try {
171
+ const variant = await this.variantService.findOne(ctx, variantId, [
172
+ 'featuredAsset',
173
+ 'taxCategory',
174
+ 'channels',
175
+ 'product.featuredAsset',
176
+ ]);
177
+ if (!variant) {
178
+ core_1.Logger.error(`Variant with id ${variantId} not found. Not creating or updating product in QLS.`, constants_1.loggerCtx);
179
+ continue;
180
+ }
181
+ const existingQlsProduct = await client.getFulfillmentProductBySku(variant.sku);
182
+ const result = await this.createOrUpdateProductInQls(ctx, client, variant, existingQlsProduct ?? null);
183
+ if (result === 'created') {
184
+ createdInQls += 1;
185
+ }
186
+ else if (result === 'updated') {
187
+ updatedInQls += 1;
188
+ }
159
189
  }
160
- else if (result === 'updated') {
161
- updatedInQls += 1;
190
+ catch (e) {
191
+ const error = (0, catch_unknown_1.asError)(e);
192
+ core_1.Logger.error(`Error syncing variant ${variantId} to QLS: ${error.message}`, constants_1.loggerCtx, error.stack);
193
+ failedCount += 1;
162
194
  }
163
195
  }
164
196
  return {
165
197
  updatedInQls,
166
198
  createdInQls,
167
199
  updatedStock: 0,
200
+ failed: failedCount,
168
201
  };
169
202
  }
170
203
  /**
@@ -211,45 +244,86 @@ let QlsProductService = class QlsProductService {
211
244
  * Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
212
245
  */
213
246
  async createOrUpdateProductInQls(ctx, client, variant, existingProduct) {
214
- let qlsProductId = existingProduct?.id;
247
+ let qlsProduct = existingProduct;
215
248
  let createdOrUpdated = 'not-changed';
249
+ const { additionalEANs, ...additionalVariantFields } = this.options.getAdditionalVariantFields(ctx, variant);
216
250
  if (!existingProduct) {
217
251
  const result = await client.createFulfillmentProduct({
218
252
  name: variant.name,
219
253
  sku: variant.sku,
220
- ...this.options.getAdditionalVariantFields?.(ctx, variant),
254
+ ...additionalVariantFields,
221
255
  });
222
- qlsProductId = result.id;
256
+ qlsProduct = result;
223
257
  core_1.Logger.info(`Created product '${variant.sku}' in QLS`, constants_1.loggerCtx);
224
258
  createdOrUpdated = 'created';
225
259
  }
226
- else if (this.shouldUpdateProductInQls(ctx, variant, existingProduct)) {
260
+ else if (this.shouldUpdateProductInQls(variant, existingProduct, additionalVariantFields)) {
227
261
  await client.updateFulfillmentProduct(existingProduct.id, {
228
262
  sku: variant.sku,
229
263
  name: variant.name,
230
- ...this.options.getAdditionalVariantFields?.(ctx, variant),
264
+ ...additionalVariantFields,
231
265
  });
232
266
  core_1.Logger.info(`Updated product '${variant.sku}' in QLS`, constants_1.loggerCtx);
233
267
  createdOrUpdated = 'updated';
234
268
  }
235
- if (qlsProductId !== variant.customFields.qlsProductId) {
269
+ if (qlsProduct && qlsProduct?.id !== variant.customFields.qlsProductId) {
236
270
  // Update variant with QLS product ID if it changed
237
271
  // Do not use variantService.update because it will trigger a change event and cause an infinite loop
238
272
  await this.connection
239
273
  .getRepository(ctx, core_1.ProductVariant)
240
- .update({ id: variant.id }, { customFields: { qlsProductId } });
241
- core_1.Logger.info(`Set QLS product ID for variant '${variant.sku}' to ${qlsProductId}`, constants_1.loggerCtx);
274
+ .update({ id: variant.id }, { customFields: { qlsProductId: qlsProduct.id } });
275
+ core_1.Logger.info(`Set QLS product ID for variant '${variant.sku}' to ${qlsProduct.id}`, constants_1.loggerCtx);
276
+ }
277
+ if (qlsProduct) {
278
+ const updatedEANs = await this.updateAdditionalEANs(ctx, client, qlsProduct, additionalEANs);
279
+ if (createdOrUpdated === 'not-changed' && updatedEANs) {
280
+ // If nothing changed so far, but EANs changed, then we did update the product in QLS
281
+ createdOrUpdated = 'updated';
282
+ }
283
+ }
284
+ if (createdOrUpdated === 'not-changed') {
285
+ core_1.Logger.info(`Variant '${variant.sku}' not updated in QLS, because no changes were found.`, constants_1.loggerCtx);
242
286
  }
243
287
  return createdOrUpdated;
244
288
  }
289
+ /**
290
+ * Update the additional EANs/barcodes for a product in QLS if needed
291
+ */
292
+ async updateAdditionalEANs(ctx, client, qlsProduct, additionalEANs) {
293
+ const existingAdditionalEANs = qlsProduct?.barcodes_and_ean.filter((ean) => ean !== qlsProduct.ean); // Remove the main EAN
294
+ const eansToUpdate = (0, util_2.getEansToUpdate)({
295
+ existingEans: existingAdditionalEANs,
296
+ desiredEans: additionalEANs,
297
+ });
298
+ if (!eansToUpdate || eansToUpdate.eansToAdd.length === 0 && eansToUpdate.eansToRemove.length === 0) {
299
+ // No updates needed
300
+ return false;
301
+ }
302
+ // Add additional EANs
303
+ eansToUpdate.eansToAdd = (0, util_2.normalizeEans)(eansToUpdate.eansToAdd);
304
+ await Promise.all(eansToUpdate.eansToAdd.map((ean) => client.addBarcode(qlsProduct.id, ean)));
305
+ if (eansToUpdate.eansToAdd.length > 0) {
306
+ core_1.Logger.info(`Added additional EANs: ${eansToUpdate.eansToAdd.join(',')} to product '${qlsProduct.sku}' in QLS`, constants_1.loggerCtx);
307
+ }
308
+ // Remove EANs, normalize first
309
+ eansToUpdate.eansToRemove = (0, util_2.normalizeEans)(eansToUpdate.eansToRemove);
310
+ // get barcode ID's to remove, because deletion goes via barcode ID, not barcode value
311
+ const barcodeIdsToRemove = qlsProduct.barcodes
312
+ .filter((barcode) => eansToUpdate.eansToRemove.includes(barcode.barcode))
313
+ .map((barcode) => barcode.id);
314
+ await Promise.all(barcodeIdsToRemove.map((barcodeId) => client.removeBarcode(qlsProduct.id, barcodeId)));
315
+ if (eansToUpdate.eansToRemove.length > 0) {
316
+ core_1.Logger.info(`Removed EANs '${eansToUpdate.eansToRemove.join(',')} from product '${qlsProduct.sku}' in QLS`, constants_1.loggerCtx);
317
+ }
318
+ return true;
319
+ }
245
320
  /**
246
321
  * Determine if a product needs to be updated in QLS based on the given variant and QLS product.
247
322
  */
248
- shouldUpdateProductInQls(ctx, variant, qlsProduct) {
249
- const additionalFields = this.options.getAdditionalVariantFields?.(ctx, variant);
323
+ shouldUpdateProductInQls(variant, qlsProduct, additionalVariantFields) {
250
324
  if (qlsProduct.name !== variant.name ||
251
- qlsProduct.ean !== additionalFields?.ean ||
252
- qlsProduct.image_url !== additionalFields?.image_url) {
325
+ qlsProduct.ean !== additionalVariantFields?.ean ||
326
+ qlsProduct.image_url !== additionalVariantFields?.image_url) {
253
327
  // If name or ean has changed, product should be updated in QLS
254
328
  return true;
255
329
  }
@@ -264,16 +338,27 @@ let QlsProductService = class QlsProductService {
264
338
  const take = 100;
265
339
  let hasMore = true;
266
340
  while (hasMore) {
267
- const result = await this.variantService.findAll(ctx, {
268
- filter: { deletedAt: { isNull: true } },
341
+ const relations = [
342
+ 'featuredAsset',
343
+ 'taxCategory',
344
+ 'channels',
345
+ 'product.featuredAsset',
346
+ ];
347
+ const [items, totalItems] = await this.listQueryBuilder
348
+ .build(core_1.ProductVariant, {
269
349
  skip,
270
350
  take,
271
- });
272
- if (!result.items.length) {
273
- break;
274
- }
275
- allVariants.push(...result.items);
276
- if (allVariants.length >= result.totalItems) {
351
+ }, {
352
+ relations,
353
+ channelId: ctx.channelId,
354
+ where: { deletedAt: (0, typeorm_1.IsNull)() },
355
+ ctx,
356
+ })
357
+ .getManyAndCount();
358
+ let variants = await Promise.all(items.map(async (item) => this.productPriceApplicator.applyChannelPriceAndTax(item, ctx, undefined, false)));
359
+ variants = variants.map((v) => (0, core_1.translateDeep)(v, ctx.languageCode));
360
+ allVariants.push(...variants);
361
+ if (allVariants.length >= totalItems) {
277
362
  hasMore = false;
278
363
  }
279
364
  skip += take;
@@ -284,6 +369,10 @@ let QlsProductService = class QlsProductService {
284
369
  * Update stock level for a variant based on the given available stock
285
370
  */
286
371
  async updateStock(ctx, variantId, availableStock) {
372
+ if (this.options.disableStockSync) {
373
+ core_1.Logger.warn(`Stock sync disabled. Not updating stock for variant '${variantId}'`, constants_1.loggerCtx);
374
+ return;
375
+ }
287
376
  // Find default Stock Location
288
377
  const defaultStockLocation = await this.stockLocationService.defaultStockLocation(ctx);
289
378
  // Get current stock level id
@@ -305,5 +394,7 @@ exports.QlsProductService = QlsProductService = __decorate([
305
394
  core_1.StockLevelService,
306
395
  core_1.ProductVariantService,
307
396
  core_1.StockLocationService,
308
- core_1.EventBus])
397
+ core_1.EventBus,
398
+ core_1.ListQueryBuilder,
399
+ core_1.ProductPriceApplicator])
309
400
  ], QlsProductService);
@@ -0,0 +1,15 @@
1
+ export interface EanUpdate {
2
+ eansToAdd: string[];
3
+ eansToRemove: string[];
4
+ }
5
+ /**
6
+ * Get the EANs to add or remove
7
+ */
8
+ export declare function getEansToUpdate({ existingEans, desiredEans, }: {
9
+ existingEans?: (string | undefined | null)[];
10
+ desiredEans?: (string | undefined | null)[];
11
+ }): EanUpdate | false;
12
+ /**
13
+ * Normalize the EANs by filtering out null, undefined and empty strings and sorting them
14
+ */
15
+ export declare function normalizeEans(eans?: (string | undefined | null)[]): string[];
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getEansToUpdate = getEansToUpdate;
4
+ exports.normalizeEans = normalizeEans;
5
+ /**
6
+ * Get the EANs to add or remove
7
+ */
8
+ function getEansToUpdate({ existingEans, desiredEans, }) {
9
+ const normalizedExisting = normalizeEans(existingEans);
10
+ const normalizedDesired = normalizeEans(desiredEans);
11
+ // Find out what EANs to add
12
+ const eansToAdd = normalizedDesired.filter((ean) => !normalizedExisting.includes(ean));
13
+ // Find out what EANs to remove
14
+ const eansToRemove = normalizedExisting.filter((ean) => !normalizedDesired.includes(ean));
15
+ if (eansToAdd.length === 0 && eansToRemove.length === 0) {
16
+ return false;
17
+ }
18
+ return {
19
+ eansToAdd,
20
+ eansToRemove,
21
+ };
22
+ }
23
+ /**
24
+ * Normalize the EANs by filtering out null, undefined and empty strings and sorting them
25
+ */
26
+ function normalizeEans(eans) {
27
+ return (eans ?? [])
28
+ .filter((ean) => !!ean && ean.trim().length > 0)
29
+ .map((ean) => ean.trim())
30
+ .sort();
31
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const util_1 = require("./util");
5
+ (0, vitest_1.describe)('getEansToUpdate', () => {
6
+ (0, vitest_1.it)('returns false for identical EAN arrays', () => {
7
+ (0, vitest_1.expect)((0, util_1.getEansToUpdate)({
8
+ existingEans: ['123', '456'],
9
+ desiredEans: ['456', '123'],
10
+ })).toBe(false);
11
+ });
12
+ (0, vitest_1.it)('ignores ordering, whitespace, null/undefined and empty stringswhen determining additions', () => {
13
+ (0, vitest_1.expect)((0, util_1.getEansToUpdate)({
14
+ existingEans: [null, '123', ''],
15
+ desiredEans: [' 123', '456 ', undefined, null],
16
+ })).toEqual({
17
+ eansToAdd: ['456'],
18
+ eansToRemove: [],
19
+ });
20
+ });
21
+ (0, vitest_1.it)('returns removals when desired array is missing values', () => {
22
+ (0, vitest_1.expect)((0, util_1.getEansToUpdate)({
23
+ existingEans: ['123', '456', null],
24
+ desiredEans: ['123'],
25
+ })).toEqual({
26
+ eansToAdd: [],
27
+ eansToRemove: ['456'],
28
+ });
29
+ });
30
+ (0, vitest_1.it)('returns both additions and removals when values differ', () => {
31
+ (0, vitest_1.expect)((0, util_1.getEansToUpdate)({
32
+ existingEans: ['123', '456'],
33
+ desiredEans: ['123', '789'],
34
+ })).toEqual({
35
+ eansToAdd: ['789'],
36
+ eansToRemove: ['456'],
37
+ });
38
+ });
39
+ (0, vitest_1.it)('treats undefined arrays as equal', () => {
40
+ (0, vitest_1.expect)((0, util_1.getEansToUpdate)({ existingEans: undefined, desiredEans: undefined })).toBe(false);
41
+ });
42
+ (0, vitest_1.it)('treats undefined and empty array as equal', () => {
43
+ // This is the case when creating a new product in QLS
44
+ (0, vitest_1.expect)((0, util_1.getEansToUpdate)({ existingEans: undefined, desiredEans: [] })).toBe(false);
45
+ });
46
+ });
47
+ (0, vitest_1.describe)('normalizeEans', () => {
48
+ (0, vitest_1.it)('filters and trims invalid values', () => {
49
+ (0, vitest_1.expect)((0, util_1.normalizeEans)([' 123', '', undefined, null, '456 '])).toEqual([
50
+ '123',
51
+ '456',
52
+ ]);
53
+ });
54
+ (0, vitest_1.it)('returns empty array for undefined input', () => {
55
+ (0, vitest_1.expect)((0, util_1.normalizeEans)(undefined)).toEqual([]);
56
+ });
57
+ (0, vitest_1.it)('sorts resulting values alphabetically', () => {
58
+ (0, vitest_1.expect)((0, util_1.normalizeEans)(['789', '123'])).toEqual(['123', '789']);
59
+ });
60
+ });
package/dist/types.d.ts CHANGED
@@ -9,7 +9,7 @@ export interface QlsPluginOptions {
9
9
  * Required to get the EAN and image URL for a product variant.
10
10
  * Also allows you to override other product attributes like name, price, etc.
11
11
  */
12
- getAdditionalVariantFields?: (ctx: RequestContext, variant: ProductVariant) => Partial<FulfillmentProductInput>;
12
+ getAdditionalVariantFields: (ctx: RequestContext, variant: ProductVariant) => AdditionalVariantFields;
13
13
  /**
14
14
  * Function to get the set service point code for an order.
15
15
  * Return undefined to not use a service point at all.
@@ -20,7 +20,19 @@ export interface QlsPluginOptions {
20
20
  * Set this to a random string
21
21
  */
22
22
  webhookSecret: string;
23
+ /**
24
+ * Disable the pulling in of stock levels from QLS. When disabled, stock in Vendure will not be modified based on QLS stock levels.
25
+ * Useful for testing out order sync separately, or testing against a QLS test env that has no stock for example
26
+ */
27
+ disableStockSync?: boolean;
23
28
  }
29
+ /**
30
+ * Additional fields for a product variant that are used to create or update a product in QLS
31
+ */
32
+ export type AdditionalVariantFields = Partial<FulfillmentProductInput & {
33
+ ean: string;
34
+ additionalEANs?: string[];
35
+ }>;
24
36
  export interface AdditionalOrderFields {
25
37
  servicepoint_code?: string;
26
38
  delivery_options?: string[];
@@ -0,0 +1,74 @@
1
+ import { addActionBarDropdownMenuItem } from '@vendure/admin-ui/core';
2
+ import gql from 'graphql-tag';
3
+
4
+ export default [
5
+ // Product sync button in product list
6
+ addActionBarDropdownMenuItem({
7
+ id: 'qls-fulfillment-sync',
8
+ label: 'Synchronize with QLS',
9
+ locationId: 'product-list',
10
+ icon: 'resistor',
11
+ requiresPermission: ['QLSFullSync'],
12
+ onClick: (_, { dataService, notificationService }) => {
13
+ dataService
14
+ .mutate(
15
+ gql`
16
+ mutation TriggerQlsProductSync {
17
+ triggerQlsProductSync
18
+ }
19
+ `
20
+ )
21
+ .subscribe({
22
+ next: () => {
23
+ notificationService.notify({
24
+ message: `Triggered QLS full product sync...`,
25
+ type: 'success',
26
+ });
27
+ },
28
+ error: (err) => {
29
+ notificationService.notify({
30
+ message: err.message,
31
+ type: 'error',
32
+ });
33
+ },
34
+ });
35
+ },
36
+ }),
37
+ // Push order to QLS button on order detail page
38
+ addActionBarDropdownMenuItem({
39
+ id: 'qls-fulfillment-push-order',
40
+ label: 'Push order to QLS',
41
+ locationId: 'order-detail',
42
+ icon: 'resistor',
43
+ requiresPermission: ['QLSFullSync'],
44
+ hasDivider: true,
45
+ onClick: (_, { route, dataService, notificationService }) => {
46
+ dataService
47
+ .mutate(
48
+ gql`
49
+ mutation PushOrderToQls($orderId: ID!) {
50
+ pushOrderToQls(orderId: $orderId)
51
+ }
52
+ `,
53
+ {
54
+ orderId: route.snapshot.params.id,
55
+ }
56
+ )
57
+ .subscribe({
58
+ next: (result) => {
59
+ console.log(result);
60
+ notificationService.notify({
61
+ message: (result as any).pushOrderToQls,
62
+ type: 'success',
63
+ });
64
+ },
65
+ error: (err) => {
66
+ notificationService.notify({
67
+ message: err.message,
68
+ type: 'error',
69
+ });
70
+ },
71
+ });
72
+ },
73
+ }),
74
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pinelab/vendure-plugin-qls-fulfillment",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.10",
4
4
  "description": "Vendure plugin to fulfill orders via QLS.",
5
5
  "keywords": [
6
6
  "fulfillment",
@@ -23,7 +23,7 @@
23
23
  "CHANGELOG.md"
24
24
  ],
25
25
  "scripts": {
26
- "build": "rimraf dist && yarn generate && tsc",
26
+ "build": "rimraf dist && yarn generate && tsc && copyfiles -u 1 'src/ui/**/*' dist/",
27
27
  "start": "nodemon --watch src --exec ts-node test/dev-server.ts",
28
28
  "generate": "graphql-codegen",
29
29
  "test": "vitest run --bail 1",