@pinelab/vendure-plugin-qls-fulfillment 1.0.0-beta.6 → 1.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/client-types.d.ts +14 -1
- package/dist/lib/qls-client.d.ts +8 -0
- package/dist/lib/qls-client.js +16 -1
- package/dist/services/qls-product.service.d.ts +4 -0
- package/dist/services/qls-product.service.js +46 -12
- package/dist/services/util.d.ts +15 -0
- package/dist/services/util.js +31 -0
- package/dist/services/util.spec.d.ts +1 -0
- package/dist/services/util.spec.js +60 -0
- package/dist/types.d.ts +13 -1
- package/package.json +1 -1
|
@@ -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:
|
|
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 {
|
package/dist/lib/qls-client.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/lib/qls-client.js
CHANGED
|
@@ -50,7 +50,8 @@ class QlsClient {
|
|
|
50
50
|
allProducts.push(...result.data);
|
|
51
51
|
hasNextPage = result.pagination?.nextPage ?? false;
|
|
52
52
|
page++;
|
|
53
|
-
|
|
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 = {
|
|
@@ -56,6 +56,10 @@ export declare class QlsProductService implements OnModuleInit, OnApplicationBoo
|
|
|
56
56
|
* Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
|
|
57
57
|
*/
|
|
58
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;
|
|
59
63
|
/**
|
|
60
64
|
* Determine if a product needs to be updated in QLS based on the given variant and QLS product.
|
|
61
65
|
*/
|
|
@@ -23,6 +23,7 @@ const typeorm_1 = require("typeorm");
|
|
|
23
23
|
const util_1 = __importDefault(require("util"));
|
|
24
24
|
const constants_1 = require("../constants");
|
|
25
25
|
const qls_client_1 = require("../lib/qls-client");
|
|
26
|
+
const util_2 = require("./util");
|
|
26
27
|
let QlsProductService = class QlsProductService {
|
|
27
28
|
constructor(connection, options, jobQueueService, stockLevelService, variantService, stockLocationService, eventBus, listQueryBuilder, productPriceApplicator) {
|
|
28
29
|
this.connection = connection;
|
|
@@ -239,45 +240,74 @@ let QlsProductService = class QlsProductService {
|
|
|
239
240
|
* Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
|
|
240
241
|
*/
|
|
241
242
|
async createOrUpdateProductInQls(ctx, client, variant, existingProduct) {
|
|
242
|
-
let
|
|
243
|
+
let qlsProduct = existingProduct;
|
|
243
244
|
let createdOrUpdated = 'not-changed';
|
|
245
|
+
const { additionalEANs, ...additionalVariantFields } = this.options.getAdditionalVariantFields(ctx, variant);
|
|
244
246
|
if (!existingProduct) {
|
|
245
247
|
const result = await client.createFulfillmentProduct({
|
|
246
248
|
name: variant.name,
|
|
247
249
|
sku: variant.sku,
|
|
248
|
-
...
|
|
250
|
+
...additionalVariantFields,
|
|
249
251
|
});
|
|
250
|
-
|
|
252
|
+
qlsProduct = result;
|
|
251
253
|
core_1.Logger.info(`Created product '${variant.sku}' in QLS`, constants_1.loggerCtx);
|
|
252
254
|
createdOrUpdated = 'created';
|
|
253
255
|
}
|
|
254
|
-
else if (this.shouldUpdateProductInQls(
|
|
256
|
+
else if (this.shouldUpdateProductInQls(variant, existingProduct, additionalVariantFields)) {
|
|
255
257
|
await client.updateFulfillmentProduct(existingProduct.id, {
|
|
256
258
|
sku: variant.sku,
|
|
257
259
|
name: variant.name,
|
|
258
|
-
...
|
|
260
|
+
...additionalVariantFields,
|
|
259
261
|
});
|
|
260
262
|
core_1.Logger.info(`Updated product '${variant.sku}' in QLS`, constants_1.loggerCtx);
|
|
261
263
|
createdOrUpdated = 'updated';
|
|
262
264
|
}
|
|
263
|
-
if (
|
|
265
|
+
if (qlsProduct && qlsProduct?.id !== variant.customFields.qlsProductId) {
|
|
264
266
|
// Update variant with QLS product ID if it changed
|
|
265
267
|
// Do not use variantService.update because it will trigger a change event and cause an infinite loop
|
|
266
268
|
await this.connection
|
|
267
269
|
.getRepository(ctx, core_1.ProductVariant)
|
|
268
|
-
.update({ id: variant.id }, { customFields: { qlsProductId } });
|
|
269
|
-
core_1.Logger.info(`Set QLS product ID for variant '${variant.sku}' to ${
|
|
270
|
+
.update({ id: variant.id }, { customFields: { qlsProductId: qlsProduct.id } });
|
|
271
|
+
core_1.Logger.info(`Set QLS product ID for variant '${variant.sku}' to ${qlsProduct.id}`, constants_1.loggerCtx);
|
|
272
|
+
}
|
|
273
|
+
if (qlsProduct) {
|
|
274
|
+
const updatedEANs = await this.updateAdditionalEANs(ctx, client, qlsProduct, additionalEANs);
|
|
275
|
+
if (createdOrUpdated === 'not-changed' && updatedEANs) {
|
|
276
|
+
// If nothing changed so far, but EANs changed, then we did update the product in QLS
|
|
277
|
+
createdOrUpdated = 'updated';
|
|
278
|
+
}
|
|
270
279
|
}
|
|
271
280
|
return createdOrUpdated;
|
|
272
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Update the additional EANs/barcodes for a product in QLS if needed
|
|
284
|
+
*/
|
|
285
|
+
async updateAdditionalEANs(ctx, client, qlsProduct, additionalEANs) {
|
|
286
|
+
const existingAdditionalEANs = qlsProduct?.barcodes_and_ean.filter((ean) => ean !== qlsProduct.ean); // Remove the main EAN
|
|
287
|
+
const eansToUpdate = (0, util_2.getEansToUpdate)({
|
|
288
|
+
existingEans: existingAdditionalEANs,
|
|
289
|
+
desiredEans: additionalEANs,
|
|
290
|
+
});
|
|
291
|
+
if (!eansToUpdate) {
|
|
292
|
+
// No updates needed
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
await Promise.all(eansToUpdate.eansToAdd.map((ean) => client.addBarcode(qlsProduct.id, ean)));
|
|
296
|
+
// get barcode ID's to remove, because deletion goes via barcode ID, not barcode value
|
|
297
|
+
const barcodeIdsToRemove = qlsProduct.barcodes
|
|
298
|
+
.filter((barcode) => eansToUpdate.eansToRemove.includes(barcode.barcode))
|
|
299
|
+
.map((barcode) => barcode.id);
|
|
300
|
+
await Promise.all(barcodeIdsToRemove.map((barcodeId) => client.removeBarcode(qlsProduct.id, barcodeId)));
|
|
301
|
+
core_1.Logger.info(`Added additional EANs '${eansToUpdate.eansToAdd.join(',')}', and removed EANs '${eansToUpdate.eansToRemove.join(',')}' for product '${qlsProduct.sku}' in QLS`, constants_1.loggerCtx);
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
273
304
|
/**
|
|
274
305
|
* Determine if a product needs to be updated in QLS based on the given variant and QLS product.
|
|
275
306
|
*/
|
|
276
|
-
shouldUpdateProductInQls(
|
|
277
|
-
const additionalFields = this.options.getAdditionalVariantFields?.(ctx, variant);
|
|
307
|
+
shouldUpdateProductInQls(variant, qlsProduct, additionalVariantFields) {
|
|
278
308
|
if (qlsProduct.name !== variant.name ||
|
|
279
|
-
qlsProduct.ean !==
|
|
280
|
-
qlsProduct.image_url !==
|
|
309
|
+
qlsProduct.ean !== additionalVariantFields?.ean ||
|
|
310
|
+
qlsProduct.image_url !== additionalVariantFields?.image_url) {
|
|
281
311
|
// If name or ean has changed, product should be updated in QLS
|
|
282
312
|
return true;
|
|
283
313
|
}
|
|
@@ -323,6 +353,10 @@ let QlsProductService = class QlsProductService {
|
|
|
323
353
|
* Update stock level for a variant based on the given available stock
|
|
324
354
|
*/
|
|
325
355
|
async updateStock(ctx, variantId, availableStock) {
|
|
356
|
+
if (this.options.disableStockSync) {
|
|
357
|
+
core_1.Logger.warn(`Stock sync disabled. Not updating stock for variant '${variantId}'`, constants_1.loggerCtx);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
326
360
|
// Find default Stock Location
|
|
327
361
|
const defaultStockLocation = await this.stockLocationService.defaultStockLocation(ctx);
|
|
328
362
|
// Get current stock level id
|
|
@@ -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 and null/undefined when 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
|
|
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[];
|