@pinelab/vendure-plugin-qls-fulfillment 1.0.0-beta.15 → 1.0.0-beta.16
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/qls-client.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export declare class QlsClient {
|
|
|
22
22
|
getAllFulfillmentProducts(): Promise<FulfillmentProduct[]>;
|
|
23
23
|
createFulfillmentProduct(data: FulfillmentProductInput): Promise<FulfillmentProduct>;
|
|
24
24
|
updateFulfillmentProduct(fulfillmentProductId: string, data: FulfillmentProductInput): Promise<FulfillmentProduct>;
|
|
25
|
+
deleteFulfillmentProduct(fulfillmentProductId: string): Promise<void>;
|
|
25
26
|
createFulfillmentOrder(data: Omit<FulfillmentOrderInput, 'brand_id'>): Promise<FulfillmentOrder>;
|
|
26
27
|
/**
|
|
27
28
|
* Add an extra barcode to a fulfillment product in QLS
|
package/dist/lib/qls-client.js
CHANGED
|
@@ -69,6 +69,9 @@ class QlsClient {
|
|
|
69
69
|
}
|
|
70
70
|
return response.data;
|
|
71
71
|
}
|
|
72
|
+
async deleteFulfillmentProduct(fulfillmentProductId) {
|
|
73
|
+
await this.rawRequest('DELETE', `fulfillment/products/${fulfillmentProductId}`);
|
|
74
|
+
}
|
|
72
75
|
async createFulfillmentOrder(data) {
|
|
73
76
|
const response = await this.rawRequest('POST', 'fulfillment/orders', {
|
|
74
77
|
...data,
|
|
@@ -115,15 +115,16 @@ let QlsOrderService = class QlsOrderService {
|
|
|
115
115
|
!order.shippingAddress.countryCode) {
|
|
116
116
|
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.`);
|
|
117
117
|
}
|
|
118
|
+
const receiverContact = this.options.getReceiverContact?.(ctx, order);
|
|
118
119
|
const qlsOrder = {
|
|
119
120
|
customer_reference: order.code,
|
|
120
121
|
processable: new Date().toISOString(), // Processable starting now
|
|
121
122
|
servicepoint_code: order.customFields?.qlsServicePointId,
|
|
122
123
|
delivery_options: additionalOrderFields?.delivery_options ?? [],
|
|
123
124
|
total_price: order.totalWithTax,
|
|
124
|
-
receiver_contact: {
|
|
125
|
+
receiver_contact: receiverContact ?? {
|
|
125
126
|
name: order.shippingAddress.fullName || customerName,
|
|
126
|
-
companyname: order.shippingAddress.company ??
|
|
127
|
+
companyname: order.shippingAddress.company ?? '',
|
|
127
128
|
street: order.shippingAddress.streetLine1,
|
|
128
129
|
housenumber: order.shippingAddress.streetLine2,
|
|
129
130
|
postalcode: order.shippingAddress.postalCode,
|
|
@@ -3,11 +3,11 @@ import { EventBus, ID, Job, JobQueueService, ListQueryBuilder, ProductPriceAppli
|
|
|
3
3
|
import { FulfillmentProduct } from '../lib/client-types';
|
|
4
4
|
import { QlsClient } from '../lib/qls-client';
|
|
5
5
|
import { QlsPluginOptions, QlsProductJobData } from '../types';
|
|
6
|
-
type
|
|
7
|
-
updatedInQls:
|
|
8
|
-
createdInQls:
|
|
9
|
-
updatedStock:
|
|
10
|
-
failed:
|
|
6
|
+
type SyncProductsResult = {
|
|
7
|
+
updatedInQls: Partial<ProductVariant>[];
|
|
8
|
+
createdInQls: Partial<ProductVariant>[];
|
|
9
|
+
updatedStock: Partial<ProductVariant>[];
|
|
10
|
+
failed: Partial<ProductVariant>[];
|
|
11
11
|
};
|
|
12
12
|
export declare class QlsProductService implements OnModuleInit, OnApplicationBootstrap {
|
|
13
13
|
private connection;
|
|
@@ -35,11 +35,15 @@ export declare class QlsProductService implements OnModuleInit, OnApplicationBoo
|
|
|
35
35
|
* 3. Creates products in QLS if needed
|
|
36
36
|
* 4. Updates products in QLS if needed
|
|
37
37
|
*/
|
|
38
|
-
runFullSync(ctx: RequestContext): Promise<
|
|
38
|
+
runFullSync(ctx: RequestContext): Promise<SyncProductsResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Utility function to remove all products from QLS
|
|
41
|
+
*/
|
|
42
|
+
removeAllProductsFromQls(ctx: RequestContext): Promise<void>;
|
|
39
43
|
/**
|
|
40
44
|
* Creates or updates the fulfillment products in QLS for the given product variants.
|
|
41
45
|
*/
|
|
42
|
-
syncVariants(ctx: RequestContext, productVariantIds: ID[]): Promise<
|
|
46
|
+
syncVariants(ctx: RequestContext, productVariantIds: ID[]): Promise<SyncProductsResult>;
|
|
43
47
|
/**
|
|
44
48
|
* Trigger a full product sync job
|
|
45
49
|
*/
|
|
@@ -24,6 +24,8 @@ const util_1 = __importDefault(require("util"));
|
|
|
24
24
|
const constants_1 = require("../constants");
|
|
25
25
|
const qls_client_1 = require("../lib/qls-client");
|
|
26
26
|
const util_2 = require("./util");
|
|
27
|
+
// Wait for 700ms to avoid rate limit of 500/5 minutes
|
|
28
|
+
const waitToPreventRateLimit = () => new Promise((resolve) => setTimeout(resolve, 700));
|
|
27
29
|
let QlsProductService = class QlsProductService {
|
|
28
30
|
constructor(connection, options, jobQueueService, stockLevelService, variantService, stockLocationService, eventBus, listQueryBuilder, productPriceApplicator) {
|
|
29
31
|
this.connection = connection;
|
|
@@ -64,7 +66,13 @@ let QlsProductService = class QlsProductService {
|
|
|
64
66
|
try {
|
|
65
67
|
const ctx = core_1.RequestContext.deserialize(job.data.ctx);
|
|
66
68
|
if (job.data.action === 'full-sync-products') {
|
|
67
|
-
|
|
69
|
+
const result = await this.runFullSync(ctx);
|
|
70
|
+
return {
|
|
71
|
+
updatedInQls: result.updatedInQls.length,
|
|
72
|
+
createdInQls: result.createdInQls.length,
|
|
73
|
+
updatedStock: result.updatedStock.length,
|
|
74
|
+
failed: result.failed.length,
|
|
75
|
+
};
|
|
68
76
|
}
|
|
69
77
|
else if (job.data.action === 'sync-products') {
|
|
70
78
|
return await this.syncVariants(ctx, job.data.productVariantIds);
|
|
@@ -89,8 +97,6 @@ let QlsProductService = class QlsProductService {
|
|
|
89
97
|
* 4. Updates products in QLS if needed
|
|
90
98
|
*/
|
|
91
99
|
async runFullSync(ctx) {
|
|
92
|
-
// Wait for 700ms to avoid rate limit of 500/5 minutes
|
|
93
|
-
const waitToPreventRateLimit = () => new Promise((resolve) => setTimeout(resolve, 700));
|
|
94
100
|
try {
|
|
95
101
|
const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
|
|
96
102
|
if (!client) {
|
|
@@ -101,28 +107,28 @@ let QlsProductService = class QlsProductService {
|
|
|
101
107
|
const allVariants = await this.getAllVariants(ctx);
|
|
102
108
|
core_1.Logger.info(`Running full sync for ${allQlsProducts.length} QLS products and ${allVariants.length} Vendure variants`, constants_1.loggerCtx);
|
|
103
109
|
// Update stock in Vendure based on QLS products
|
|
104
|
-
|
|
110
|
+
const updatedStock = [];
|
|
105
111
|
for (const variant of allVariants) {
|
|
106
112
|
const qlsProduct = allQlsProducts.find((p) => p.sku == variant.sku);
|
|
107
113
|
if (qlsProduct) {
|
|
108
114
|
await this.updateStock(ctx, variant.id, qlsProduct.amount_available);
|
|
109
|
-
|
|
115
|
+
updatedStock.push(variant);
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
|
-
core_1.Logger.info(`Updated stock for ${
|
|
118
|
+
core_1.Logger.info(`Updated stock for ${updatedStock.length} variants based on QLS stock levels`, constants_1.loggerCtx);
|
|
113
119
|
// Create or update products in QLS
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
const createdQlsProducts = [];
|
|
121
|
+
const updatedQlsProducts = [];
|
|
122
|
+
const failed = [];
|
|
117
123
|
for (const variant of allVariants) {
|
|
118
124
|
try {
|
|
119
125
|
const existingQlsProduct = allQlsProducts.find((p) => p.sku == variant.sku);
|
|
120
126
|
const result = await this.createOrUpdateProductInQls(ctx, client, variant, existingQlsProduct ?? null);
|
|
121
127
|
if (result === 'created') {
|
|
122
|
-
|
|
128
|
+
createdQlsProducts.push(variant);
|
|
123
129
|
}
|
|
124
130
|
else if (result === 'updated') {
|
|
125
|
-
|
|
131
|
+
updatedQlsProducts.push(variant);
|
|
126
132
|
}
|
|
127
133
|
if (result === 'created' || result === 'updated') {
|
|
128
134
|
// Wait only if we created or updated a product, otherwise no calls have been made yet.
|
|
@@ -132,18 +138,18 @@ let QlsProductService = class QlsProductService {
|
|
|
132
138
|
catch (e) {
|
|
133
139
|
const error = (0, catch_unknown_1.asError)(e);
|
|
134
140
|
core_1.Logger.error(`Error creating or updating variant '${variant.sku}' in QLS: ${error.message}`, constants_1.loggerCtx, error.stack);
|
|
135
|
-
|
|
141
|
+
failed.push(variant);
|
|
136
142
|
await waitToPreventRateLimit();
|
|
137
143
|
}
|
|
138
144
|
}
|
|
139
|
-
core_1.Logger.info(`Created ${
|
|
140
|
-
core_1.Logger.info(`Updated ${
|
|
141
|
-
core_1.Logger.info(`Finished full sync with ${
|
|
145
|
+
core_1.Logger.info(`Created ${createdQlsProducts.length} products in QLS`, constants_1.loggerCtx);
|
|
146
|
+
core_1.Logger.info(`Updated ${updatedQlsProducts.length} products in QLS`, constants_1.loggerCtx);
|
|
147
|
+
core_1.Logger.info(`Finished full sync with ${failed.length} failures`, constants_1.loggerCtx);
|
|
142
148
|
return {
|
|
143
|
-
updatedInQls:
|
|
144
|
-
createdInQls:
|
|
145
|
-
updatedStock
|
|
146
|
-
failed
|
|
149
|
+
updatedInQls: updatedQlsProducts,
|
|
150
|
+
createdInQls: createdQlsProducts,
|
|
151
|
+
updatedStock,
|
|
152
|
+
failed,
|
|
147
153
|
};
|
|
148
154
|
}
|
|
149
155
|
catch (e) {
|
|
@@ -152,6 +158,22 @@ let QlsProductService = class QlsProductService {
|
|
|
152
158
|
throw error;
|
|
153
159
|
}
|
|
154
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Utility function to remove all products from QLS
|
|
163
|
+
*/
|
|
164
|
+
async removeAllProductsFromQls(ctx) {
|
|
165
|
+
const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
|
|
166
|
+
if (!client) {
|
|
167
|
+
throw new Error(`QLS not enabled for channel ${ctx.channel.token}`);
|
|
168
|
+
}
|
|
169
|
+
core_1.Logger.warn(`Removed all products from QLS for channel ${ctx.channel.token}`, constants_1.loggerCtx);
|
|
170
|
+
const allProducts = await client.getAllFulfillmentProducts();
|
|
171
|
+
for (const product of allProducts) {
|
|
172
|
+
await client.deleteFulfillmentProduct(product.id);
|
|
173
|
+
await waitToPreventRateLimit();
|
|
174
|
+
}
|
|
175
|
+
core_1.Logger.warn(`Removed ${allProducts.length} products from QLS for channel ${ctx.channel.token}`, constants_1.loggerCtx);
|
|
176
|
+
}
|
|
155
177
|
/**
|
|
156
178
|
* Creates or updates the fulfillment products in QLS for the given product variants.
|
|
157
179
|
*/
|
|
@@ -160,15 +182,15 @@ let QlsProductService = class QlsProductService {
|
|
|
160
182
|
if (!client) {
|
|
161
183
|
core_1.Logger.debug(`QLS not enabled for channel ${ctx.channel.token}. Not handling product update/create.`, constants_1.loggerCtx);
|
|
162
184
|
return {
|
|
163
|
-
updatedInQls:
|
|
164
|
-
createdInQls:
|
|
165
|
-
updatedStock:
|
|
166
|
-
failed:
|
|
185
|
+
updatedInQls: [],
|
|
186
|
+
createdInQls: [],
|
|
187
|
+
updatedStock: [],
|
|
188
|
+
failed: [],
|
|
167
189
|
};
|
|
168
190
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
191
|
+
const updatedInQls = [];
|
|
192
|
+
const createdInQls = [];
|
|
193
|
+
const failed = [];
|
|
172
194
|
for (const variantId of productVariantIds) {
|
|
173
195
|
try {
|
|
174
196
|
const variant = await this.variantService.findOne(ctx, variantId, [
|
|
@@ -184,23 +206,23 @@ let QlsProductService = class QlsProductService {
|
|
|
184
206
|
const existingQlsProduct = await client.getFulfillmentProductBySku(variant.sku);
|
|
185
207
|
const result = await this.createOrUpdateProductInQls(ctx, client, variant, existingQlsProduct ?? null);
|
|
186
208
|
if (result === 'created') {
|
|
187
|
-
createdInQls
|
|
209
|
+
createdInQls.push(variant);
|
|
188
210
|
}
|
|
189
211
|
else if (result === 'updated') {
|
|
190
|
-
updatedInQls
|
|
212
|
+
updatedInQls.push(variant);
|
|
191
213
|
}
|
|
192
214
|
}
|
|
193
215
|
catch (e) {
|
|
194
216
|
const error = (0, catch_unknown_1.asError)(e);
|
|
195
217
|
core_1.Logger.error(`Error syncing variant ${variantId} to QLS: ${error.message}`, constants_1.loggerCtx, error.stack);
|
|
196
|
-
|
|
218
|
+
failed.push({ id: variantId });
|
|
197
219
|
}
|
|
198
220
|
}
|
|
199
221
|
return {
|
|
200
222
|
updatedInQls,
|
|
201
223
|
createdInQls,
|
|
202
|
-
updatedStock:
|
|
203
|
-
failed
|
|
224
|
+
updatedStock: [],
|
|
225
|
+
failed,
|
|
204
226
|
};
|
|
205
227
|
}
|
|
206
228
|
/**
|
|
@@ -247,6 +269,10 @@ let QlsProductService = class QlsProductService {
|
|
|
247
269
|
* Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
|
|
248
270
|
*/
|
|
249
271
|
async createOrUpdateProductInQls(ctx, client, variant, existingProduct) {
|
|
272
|
+
if (this.options.excludeVariantFromSync?.(ctx, variant)) {
|
|
273
|
+
core_1.Logger.info(`Variant '${variant.sku}' excluded from sync to QLS.`, constants_1.loggerCtx);
|
|
274
|
+
return 'not-changed';
|
|
275
|
+
}
|
|
250
276
|
let qlsProduct = existingProduct;
|
|
251
277
|
let createdOrUpdated = 'not-changed';
|
|
252
278
|
const { additionalEANs, ...additionalVariantFields } = this.options.getAdditionalVariantFields(ctx, variant);
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ID, Injector, Order, ProductVariant, RequestContext, SerializedRequestContext } from '@vendure/core';
|
|
2
|
-
import { CustomValue, FulfillmentProductInput } from './lib/client-types';
|
|
2
|
+
import { CustomValue, FulfillmentOrderReceiverContactInput, FulfillmentProductInput } from './lib/client-types';
|
|
3
3
|
export interface QlsPluginOptions {
|
|
4
4
|
/**
|
|
5
5
|
* Get the QLS client config for the current channel based on given context
|
|
@@ -25,6 +25,17 @@ export interface QlsPluginOptions {
|
|
|
25
25
|
* Useful for testing out order sync separately, or testing against a QLS test env that has no stock for example
|
|
26
26
|
*/
|
|
27
27
|
disableStockSync?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Optional function to determine if a product variant should be excluded from syncing to QLS.
|
|
30
|
+
* Return true to exclude the variant from sync, false or undefined to include it.
|
|
31
|
+
*/
|
|
32
|
+
excludeVariantFromSync?: (ctx: RequestContext, variant: ProductVariant) => boolean | Promise<boolean>;
|
|
33
|
+
/**
|
|
34
|
+
* Optional function to customize the receiver contact details when creating a QLS order.
|
|
35
|
+
* Allows you to set different fields or override default mapping from the order's shipping address and customer.
|
|
36
|
+
* If not provided, default mapping will be used.
|
|
37
|
+
*/
|
|
38
|
+
getReceiverContact?: (ctx: RequestContext, order: Order) => FulfillmentOrderReceiverContactInput | undefined;
|
|
28
39
|
}
|
|
29
40
|
/**
|
|
30
41
|
* Additional fields for a product variant that are used to create or update a product in QLS
|