@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.
@@ -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
@@ -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 ?? customerName,
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 SyncProductsJobResult = {
7
- updatedInQls: number;
8
- createdInQls: number;
9
- updatedStock: number;
10
- failed: number;
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<SyncProductsJobResult>;
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<SyncProductsJobResult>;
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
- return await this.runFullSync(ctx);
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
- let updateStockCount = 0;
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
- updateStockCount += 1;
115
+ updatedStock.push(variant);
110
116
  }
111
117
  }
112
- core_1.Logger.info(`Updated stock for ${updateStockCount} variants based on QLS stock levels`, constants_1.loggerCtx);
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
- let createdQlsProductsCount = 0;
115
- let updatedQlsProductsCount = 0;
116
- let failedCount = 0;
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
- createdQlsProductsCount += 1;
128
+ createdQlsProducts.push(variant);
123
129
  }
124
130
  else if (result === 'updated') {
125
- updatedQlsProductsCount += 1;
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
- failedCount += 1;
141
+ failed.push(variant);
136
142
  await waitToPreventRateLimit();
137
143
  }
138
144
  }
139
- core_1.Logger.info(`Created ${createdQlsProductsCount} products in QLS`, constants_1.loggerCtx);
140
- core_1.Logger.info(`Updated ${updatedQlsProductsCount} products in QLS`, constants_1.loggerCtx);
141
- core_1.Logger.info(`Finished full sync with ${failedCount} failures`, constants_1.loggerCtx);
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: updatedQlsProductsCount,
144
- createdInQls: createdQlsProductsCount,
145
- updatedStock: updateStockCount,
146
- failed: failedCount,
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: 0,
164
- createdInQls: 0,
165
- updatedStock: 0,
166
- failed: 0,
185
+ updatedInQls: [],
186
+ createdInQls: [],
187
+ updatedStock: [],
188
+ failed: [],
167
189
  };
168
190
  }
169
- let updatedInQls = 0;
170
- let createdInQls = 0;
171
- let failedCount = 0;
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 += 1;
209
+ createdInQls.push(variant);
188
210
  }
189
211
  else if (result === 'updated') {
190
- updatedInQls += 1;
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
- failedCount += 1;
218
+ failed.push({ id: variantId });
197
219
  }
198
220
  }
199
221
  return {
200
222
  updatedInQls,
201
223
  createdInQls,
202
- updatedStock: 0,
203
- failed: failedCount,
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pinelab/vendure-plugin-qls-fulfillment",
3
- "version": "1.0.0-beta.15",
3
+ "version": "1.0.0-beta.16",
4
4
  "description": "Vendure plugin to fulfill orders via QLS.",
5
5
  "keywords": [
6
6
  "fulfillment",