@pinelab/vendure-plugin-qls-fulfillment 1.9.0 → 2.0.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,9 @@
1
+ # 2.0.0 (2026-06-10)
2
+
3
+ - Restructure plugin options into nested `orderSync` and `productSync` objects. This is a **breaking change**: all plugin options must now be organized under `orderSync` (for order-related hooks) and `productSync` (for product-related hooks).
4
+ - Renamed `getAdditionalOrderFields` to `pushAdditionalOrderFields` in `orderSync` options.
5
+ - Added `pullAdditionalOrderFields` hook in `orderSync` options, called after an order is successfully created in QLS. Errors in this hook are caught and logged, and will not fail the order push job.
6
+
1
7
  # 1.9.0 (2026-08-05)
2
8
 
3
9
  - Upgraded to Vendure 3.6.3
package/README.md CHANGED
@@ -23,7 +23,52 @@ This plugin contains:
23
23
 
24
24
  ## Getting started
25
25
 
26
- // TODO
26
+ ```ts
27
+ import { QlsPlugin } from '@pinelab/vendure-plugin-qls-fulfillment';
28
+
29
+ // In your VendureConfig:
30
+ plugins: [
31
+ QlsPlugin.init({
32
+ getConfig: (ctx) => {
33
+ // Return config for the current channel, or undefined to disable QLS for this channel
34
+ return {
35
+ username: 'qls-username',
36
+ password: 'qls-password',
37
+ companyId: 'your-company-id',
38
+ brandId: 'your-brand-id',
39
+ };
40
+ },
41
+ webhookSecret: 'your-random-secret',
42
+ productSync: {
43
+ // Required: map Vendure variants to QLS product fields
44
+ getAdditionalVariantFields: (ctx, variant) => ({
45
+ ean: variant.sku,
46
+ image_url: `https://your-cdn.com/${variant.featuredAsset?.preview}`,
47
+ additionalEANs: ['1234567890'],
48
+ }),
49
+ // Optional: exclude certain variants from being synced to QLS
50
+ excludeVariantFromSync: (ctx, injector, variant) => {
51
+ return variant.sku.startsWith('DONOTSYNC');
52
+ },
53
+ },
54
+ orderSync: {
55
+ // Optional: push additional fields to QLS when creating an order
56
+ pushAdditionalOrderFields: (ctx, injector, order) => ({
57
+ custom_values: [
58
+ {
59
+ key: 'vendureOrder',
60
+ value: `https://your-admin.com/admin/orders/${order.code}`,
61
+ },
62
+ ],
63
+ }),
64
+ // Optional: delay order processing in QLS by 2 hours
65
+ processOrderFrom: (ctx, order) => {
66
+ return new Date(Date.now() + 1000 * 60 * 60 * 2);
67
+ },
68
+ },
69
+ }),
70
+ ];
71
+ ```
27
72
 
28
73
  This plugin requires the default order process to be configured with `checkFulfillmentStates: false`, so that orders can be transitioned to Shipped and Delivered without the need of fulfillment. Fulfillment is the responsibility of Picqer, so we won't handle that in Vendure when using this plugin.
29
74
 
@@ -165,3 +165,395 @@ export interface FulfillmentOrder {
165
165
  }
166
166
  export type IncomingStockWebhook = Pick<FulfillmentProduct, 'sku' | 'amount_available'>;
167
167
  export type IncomingOrderWebhook = Pick<FulfillmentOrder, 'customer_reference' | 'status' | 'cancelled' | 'amount_delivered' | 'amount_total'>;
168
+ /**
169
+ * This is the full data returend by the QLS API when fetching a fulfillment product by ID
170
+ * Getting via list, sku or filtering does NOT return this full object
171
+ */
172
+ export interface FulfillmentProductDetail {
173
+ data: Data;
174
+ meta: Meta;
175
+ errors: any[];
176
+ pagination: any;
177
+ }
178
+ export interface Data {
179
+ id: string;
180
+ company_id: string;
181
+ collection_id: any;
182
+ dimensions: Dimensions;
183
+ article_number: any;
184
+ ean: string;
185
+ name: string;
186
+ sku: string;
187
+ image_url: string;
188
+ country_code_of_origin: any;
189
+ hs_code: any;
190
+ need_barcode_picking: boolean;
191
+ need_best_before_stock: boolean;
192
+ need_serial_number: boolean;
193
+ amount_available: number;
194
+ amount_reserved: number;
195
+ amount_total: number;
196
+ price_cost: number;
197
+ price_store: any;
198
+ weight: number;
199
+ created: string;
200
+ modified: string;
201
+ foldable: boolean;
202
+ fulfillment_product_brand_id: string;
203
+ description: any;
204
+ amount_blocked: number;
205
+ status: any;
206
+ bundles: any[];
207
+ product_brand: ProductBrand;
208
+ product_values: ProductValue[];
209
+ suppliers: Supplier[];
210
+ vas: Va[];
211
+ product_master_cartons: any[];
212
+ product_measurements: ProductMeasurement[];
213
+ barcodes: any[];
214
+ warehouse_stock_transfers: WarehouseStockTransfer[];
215
+ bundle_products: any[];
216
+ warehouse_stocks: WarehouseStock[];
217
+ mappings: Mapping[];
218
+ order_unit: any;
219
+ image_url_handheld: string;
220
+ barcodes_and_ean: string[];
221
+ }
222
+ export interface Dimensions {
223
+ length: string;
224
+ width: string;
225
+ height: string;
226
+ }
227
+ export interface ProductBrand {
228
+ id: string;
229
+ company_id: string;
230
+ name: string;
231
+ }
232
+ export interface ProductValue {
233
+ id: number;
234
+ fulfillment_product_id: string;
235
+ key: string;
236
+ value: string;
237
+ }
238
+ export interface Supplier {
239
+ id: string;
240
+ company_id: string;
241
+ name: string;
242
+ _joinData: JoinData;
243
+ }
244
+ export interface JoinData {
245
+ id: number;
246
+ fulfillment_product_id: string;
247
+ supplier_id: string;
248
+ supplier_code: any;
249
+ }
250
+ export interface Va {
251
+ id: number;
252
+ fulfillment_product_id: string;
253
+ vas_id: number;
254
+ value_added_service: ValueAddedService;
255
+ }
256
+ export interface ValueAddedService {
257
+ id: number;
258
+ short: string;
259
+ name: string;
260
+ type_id: number;
261
+ price: number;
262
+ description: string;
263
+ entity_type?: string;
264
+ entity_id?: number;
265
+ company_id: any;
266
+ enabled: boolean;
267
+ parent_id: any;
268
+ sort: number;
269
+ visible: boolean;
270
+ setting: Setting;
271
+ vas_type: VasType;
272
+ }
273
+ export interface Setting {
274
+ use_for_tote_hash: number;
275
+ }
276
+ export interface VasType {
277
+ id: number;
278
+ short: string;
279
+ name: string;
280
+ description: any;
281
+ type: string;
282
+ info_text?: string;
283
+ }
284
+ export interface ProductMeasurement {
285
+ id: string;
286
+ product_id: string;
287
+ dimensions: Dimensions2;
288
+ weight: number;
289
+ product_measurementscol: any;
290
+ creator: string;
291
+ creator_company_id?: string;
292
+ creator_user_id?: string;
293
+ created: string;
294
+ creator_user?: CreatorUser;
295
+ }
296
+ export interface Dimensions2 {
297
+ length: string;
298
+ width: string;
299
+ height: string;
300
+ }
301
+ export interface CreatorUser {
302
+ id: string;
303
+ email: string;
304
+ firstname: string;
305
+ lastname: string;
306
+ role: string;
307
+ permissions: any;
308
+ created: string;
309
+ modified: string;
310
+ language_id: number;
311
+ personnel_number: string;
312
+ has_beta_version: boolean;
313
+ password_changed: string;
314
+ remember_token: string;
315
+ email_verified_at: any;
316
+ }
317
+ export interface WarehouseStockTransfer {
318
+ id: string;
319
+ company_id: string;
320
+ product_id: string;
321
+ source_stock_id: any;
322
+ amount_total: number;
323
+ amount_transferred: number;
324
+ remarks: any;
325
+ source: string;
326
+ status: string;
327
+ created: string;
328
+ modified: string;
329
+ best_before: any;
330
+ fulfillment_product_batch_id: any;
331
+ creator_user_id: string;
332
+ purchase_order_label_product_id: any;
333
+ purchase_order_label_product: any;
334
+ warehouse_stock_transfer_box_mappings: WarehouseStockTransferBoxMapping[];
335
+ creator: Creator;
336
+ company: Company;
337
+ amount_pending: number;
338
+ }
339
+ export interface WarehouseStockTransferBoxMapping {
340
+ id: string;
341
+ box_id: string;
342
+ transfer_id: string;
343
+ created: string;
344
+ modified: string;
345
+ deleted: any;
346
+ warehouse_box: WarehouseBox;
347
+ }
348
+ export interface WarehouseBox {
349
+ id: string;
350
+ company_id: string;
351
+ trolley_id: any;
352
+ warehouse_id: number;
353
+ type_id: any;
354
+ code: string;
355
+ position: any;
356
+ created: string;
357
+ modified: string;
358
+ deleted: any;
359
+ prefix: string;
360
+ }
361
+ export interface Creator {
362
+ id: string;
363
+ email: string;
364
+ firstname: string;
365
+ lastname: string;
366
+ role: string;
367
+ permissions: any;
368
+ created: string;
369
+ modified: string;
370
+ language_id: number;
371
+ personnel_number: any;
372
+ has_beta_version: boolean;
373
+ password_changed: string;
374
+ remember_token: string;
375
+ email_verified_at: any;
376
+ }
377
+ export interface Company {
378
+ id: string;
379
+ tenant_id: string;
380
+ affiliate_id: any;
381
+ name: string;
382
+ anonymous_name: string;
383
+ invoice_contact_id: string;
384
+ active: boolean;
385
+ shipments_last_7_days: number;
386
+ legacy_shipments_last_7_days: number;
387
+ heavy_shipments: boolean;
388
+ default_user_role_id: string;
389
+ financial_relation_id: string;
390
+ has_employee: boolean;
391
+ has_fulfillment: boolean;
392
+ has_route: boolean;
393
+ has_signed_gdpr: boolean;
394
+ pickup_timeframe_start: any;
395
+ pickup_timeframe_end: any;
396
+ fulfillment_average_handling_time: any;
397
+ outstanding_invoices_status: string;
398
+ outstanding_invoices_amount: number;
399
+ legal_companyname: any;
400
+ legal_coc: any;
401
+ legal_vat: any;
402
+ onboarding_status: string;
403
+ status: string;
404
+ created: string;
405
+ modified: string;
406
+ deleted: any;
407
+ price_change: any;
408
+ auto_secondwave: boolean;
409
+ priority_support: boolean;
410
+ mollie_customer_id: any;
411
+ company_group_id: any;
412
+ test: boolean;
413
+ enable_software_charge: boolean;
414
+ }
415
+ export interface WarehouseStock {
416
+ id: number;
417
+ product_id: string;
418
+ autostore_bin_id: string;
419
+ zone_id: number;
420
+ row_number: string;
421
+ rack_number: string;
422
+ shelf_number: string;
423
+ number: string;
424
+ amount_current: number;
425
+ amount_reserved: number;
426
+ best_before: any;
427
+ created: string;
428
+ modified: string;
429
+ deleted: any;
430
+ best_before_cutoff: any;
431
+ blocked: any;
432
+ autostore_bin: AutostoreBin;
433
+ warehouse_zone: WarehouseZone;
434
+ product_batch: any[];
435
+ amount_available: number;
436
+ code: string;
437
+ code_formatted: string;
438
+ is_changed_for_replenishment: boolean;
439
+ location: string;
440
+ }
441
+ export interface AutostoreBin {
442
+ id: string;
443
+ autostore_id: string;
444
+ autostore_bin_id: number;
445
+ autostore_bin_layout_id: string;
446
+ zone_id: number;
447
+ content_code: string;
448
+ bin_type: number;
449
+ created?: string;
450
+ modified: string;
451
+ deleted: any;
452
+ depth: number;
453
+ xpos: number;
454
+ ypos: number;
455
+ transferred_in: string;
456
+ mode: string;
457
+ }
458
+ export interface WarehouseZone {
459
+ id: number;
460
+ company_id: string;
461
+ name: string;
462
+ pickable: boolean;
463
+ flip_row_numbers: boolean;
464
+ warehouse_id: number;
465
+ batchable: boolean;
466
+ isolated: boolean;
467
+ autostore_id: string;
468
+ has_pallets: boolean;
469
+ block_reservation_after: any;
470
+ warehouse_zone_group_id: any;
471
+ has_quarantine: boolean;
472
+ partially_replenishment_allowed: boolean;
473
+ allow_automatic_replenishment: boolean;
474
+ company: Company2;
475
+ }
476
+ export interface Company2 {
477
+ id: string;
478
+ tenant_id: string;
479
+ affiliate_id: any;
480
+ name: string;
481
+ anonymous_name: string;
482
+ invoice_contact_id: string;
483
+ active: boolean;
484
+ shipments_last_7_days: number;
485
+ legacy_shipments_last_7_days: number;
486
+ heavy_shipments: boolean;
487
+ default_user_role_id: string;
488
+ financial_relation_id: string;
489
+ has_employee: boolean;
490
+ has_fulfillment: boolean;
491
+ has_route: boolean;
492
+ has_signed_gdpr: boolean;
493
+ pickup_timeframe_start: any;
494
+ pickup_timeframe_end: any;
495
+ fulfillment_average_handling_time: any;
496
+ outstanding_invoices_status: string;
497
+ outstanding_invoices_amount: number;
498
+ legal_companyname: any;
499
+ legal_coc: any;
500
+ legal_vat: any;
501
+ onboarding_status: string;
502
+ status: string;
503
+ created: string;
504
+ modified: string;
505
+ deleted: any;
506
+ price_change: any;
507
+ auto_secondwave: boolean;
508
+ priority_support: boolean;
509
+ mollie_customer_id: any;
510
+ company_group_id: any;
511
+ test: boolean;
512
+ enable_software_charge: boolean;
513
+ }
514
+ export interface Mapping {
515
+ id: string;
516
+ product_id: string;
517
+ shop_integration_id: string;
518
+ shop_integration_reference: string;
519
+ shop_integration_reference2: string;
520
+ stock: number;
521
+ sync_stock: boolean;
522
+ created: string;
523
+ modified: string;
524
+ deleted: any;
525
+ synced: string;
526
+ exception_count: number;
527
+ name: string;
528
+ ean: string;
529
+ sku: string;
530
+ shop_integration: ShopIntegration;
531
+ }
532
+ export interface ShopIntegration {
533
+ id: string;
534
+ company_id: string;
535
+ brand_id: string;
536
+ type_id: number;
537
+ name: string;
538
+ created: string;
539
+ modified: string;
540
+ deleted: any;
541
+ last_imported: string;
542
+ last_imported_order_insights: any;
543
+ last_imported_shipment_insights: any;
544
+ last_imported_purchase_order: any;
545
+ fulfillment_sync_orders: boolean;
546
+ fulfillment_sync_products: boolean;
547
+ fulfillment_sync_stock: boolean;
548
+ fulfillment_sync_purchase_orders: boolean;
549
+ fulfillment_product_create_new: boolean;
550
+ fulfillment_product_match_by_name: boolean;
551
+ fulfillment_product_match_by_ean: boolean;
552
+ fulfillment_product_match_by_sku: boolean;
553
+ fulfillment_product_match_fields: string;
554
+ last_imported_products: any;
555
+ exception_count_importing_products: number;
556
+ }
557
+ export interface Meta {
558
+ code: number;
559
+ }
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
3
  /**
3
4
  * QLS API types only
4
5
  */
@@ -1,6 +1,6 @@
1
1
  import { RequestContext } from '@vendure/core';
2
2
  import { QlsClientConfig, QlsPluginOptions } from '../types';
3
- import type { FulfillmentOrder, FulfillmentOrderInput, FulfillmentProduct, FulfillmentProductInput, QlsApiResponse } from './client-types';
3
+ import type { FulfillmentOrder, FulfillmentOrderInput, FulfillmentProduct, FulfillmentProductDetail, FulfillmentProductInput, QlsApiResponse } from './client-types';
4
4
  import { QlsServicePoint } from '../api/generated/graphql';
5
5
  export declare function getQlsClient(ctx: RequestContext, pluginOptions: QlsPluginOptions): Promise<QlsClient | undefined>;
6
6
  /**
@@ -15,7 +15,7 @@ export declare class QlsClient {
15
15
  * Returns the first product found, or undefined if no product is found.
16
16
  */
17
17
  getFulfillmentProductBySku(sku: string): Promise<FulfillmentProduct | undefined>;
18
- getFulfillmentProductById(fulfillmentProductId: string): Promise<FulfillmentProduct | undefined>;
18
+ getFulfillmentProductById(fulfillmentProductId: string): Promise<FulfillmentProductDetail | undefined>;
19
19
  /**
20
20
  * Get stock for all fulfillment products.
21
21
  * Might require multiple requests if the result is paginated.
@@ -26,10 +26,16 @@ const qls_order_entity_entity_1 = require("./entities/qls-order-entity.entity");
26
26
  let QlsPlugin = QlsPlugin_1 = class QlsPlugin {
27
27
  static init(options) {
28
28
  this.options = {
29
- synchronizeStockLevels: true,
30
- autoPushOrders: true,
31
- qlsProductIdUiTab: 'QLS',
32
29
  ...options,
30
+ orderSync: {
31
+ autoPushOrders: true,
32
+ ...options.orderSync,
33
+ },
34
+ productSync: {
35
+ synchronizeStockLevels: true,
36
+ qlsProductIdUiTab: 'QLS',
37
+ ...options.productSync,
38
+ },
33
39
  };
34
40
  return QlsPlugin_1;
35
41
  }
@@ -55,7 +61,7 @@ exports.QlsPlugin = QlsPlugin = QlsPlugin_1 = __decorate([
55
61
  configuration: (config) => {
56
62
  config.authOptions.customPermissions.push(permissions_1.qlsFullSyncPermission);
57
63
  config.authOptions.customPermissions.push(permissions_1.qlsPushOrderPermission);
58
- config.customFields.ProductVariant.push(...(0, custom_fields_1.getVariantCustomFields)(QlsPlugin.options?.qlsProductIdUiTab));
64
+ config.customFields.ProductVariant.push(...(0, custom_fields_1.getVariantCustomFields)(QlsPlugin.options?.productSync?.qlsProductIdUiTab));
59
65
  config.customFields.Order.push(...custom_fields_1.orderCustomFields);
60
66
  return config;
61
67
  },
@@ -37,7 +37,7 @@ let QlsOrderService = class QlsOrderService {
37
37
  onApplicationBootstrap() {
38
38
  // Listen for OrderPlacedEvent and add a job to the queue
39
39
  this.eventBus.ofType(core_2.OrderPlacedEvent).subscribe((event) => {
40
- if (!this.options.autoPushOrders) {
40
+ if (!this.options.orderSync.autoPushOrders) {
41
41
  core_2.Logger.info(`Auto push orders disabled, not triggering push order job for order ${event.order.code}`, constants_1.loggerCtx);
42
42
  return;
43
43
  }
@@ -111,7 +111,7 @@ let QlsOrderService = class QlsOrderService {
111
111
  const qlsProducts = [];
112
112
  await Promise.all(order.lines.map(async (line) => {
113
113
  // Check if product variant should be excluded from sync
114
- if (await this.options.excludeVariantFromSync?.(ctx, new core_2.Injector(this.moduleRef), line.productVariant)) {
114
+ if (await this.options.productSync.excludeVariantFromSync?.(ctx, new core_2.Injector(this.moduleRef), line.productVariant)) {
115
115
  core_2.Logger.info(`Product variant '${line.productVariant.sku}' not sent to QLS in order '${order.code}' because it is excluded from sync.`, constants_1.loggerCtx);
116
116
  return;
117
117
  }
@@ -127,7 +127,7 @@ let QlsOrderService = class QlsOrderService {
127
127
  }));
128
128
  // Add additional order items, if any
129
129
  try {
130
- const additionalOrderItems = await this.options.addAdditionalOrderItems?.(ctx, new core_2.Injector(this.moduleRef), order);
130
+ const additionalOrderItems = await this.options.orderSync.addAdditionalOrderItems?.(ctx, new core_2.Injector(this.moduleRef), order);
131
131
  if (additionalOrderItems) {
132
132
  qlsProducts.push(...additionalOrderItems);
133
133
  }
@@ -142,7 +142,7 @@ let QlsOrderService = class QlsOrderService {
142
142
  core_2.Logger.info(message, constants_1.loggerCtx);
143
143
  return message;
144
144
  }
145
- const additionalOrderFields = await this.options.getAdditionalOrderFields?.(ctx, new core_2.Injector(this.moduleRef), order);
145
+ const additionalOrderFields = await this.options.orderSync.pushAdditionalOrderFields?.(ctx, new core_2.Injector(this.moduleRef), order);
146
146
  const customerName = [order.customer?.firstName, order.customer?.lastName]
147
147
  .filter(Boolean)
148
148
  .join(' ');
@@ -160,8 +160,9 @@ let QlsOrderService = class QlsOrderService {
160
160
  !order.shippingAddress.countryCode) {
161
161
  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.`);
162
162
  }
163
- const processable = (await this.options.processOrderFrom?.(ctx, order)) ?? new Date();
164
- const receiverContact = this.options.getReceiverContact?.(ctx, order);
163
+ const processable = (await this.options.orderSync.processOrderFrom?.(ctx, order)) ??
164
+ new Date();
165
+ const receiverContact = this.options.orderSync.getReceiverContact?.(ctx, order);
165
166
  const qlsOrder = {
166
167
  customer_reference: order.code,
167
168
  processable: processable.toISOString(),
@@ -206,6 +207,13 @@ let QlsOrderService = class QlsOrderService {
206
207
  const error = (0, catch_unknown_1.asError)(e);
207
208
  core_2.Logger.error(`Error saving QLS order entity for order '${order.code}': ${error.message}`, constants_1.loggerCtx, error.stack);
208
209
  });
210
+ try {
211
+ await this.options.orderSync.pullAdditionalOrderFields?.(ctx, new core_2.Injector(this.moduleRef), order, result);
212
+ }
213
+ catch (e) {
214
+ const error = (0, catch_unknown_1.asError)(e);
215
+ core_2.Logger.error(`Error in pullAdditionalOrderFields for order '${order.code}': ${error.message}`, constants_1.loggerCtx, error.stack);
216
+ }
209
217
  return `Order '${order.code}' created in QLS with id '${result.id}'`;
210
218
  }
211
219
  catch (e) {
@@ -218,11 +218,12 @@ let QlsProductService = class QlsProductService {
218
218
  else if (result.status === 'updated') {
219
219
  updatedInQls.push(variant);
220
220
  }
221
- if (result.qlsProductId && this.options.saveAdditionalData) {
221
+ if (result.qlsProductId &&
222
+ this.options.productSync.saveAdditionalVariantData) {
222
223
  try {
223
224
  const qlsProduct = await client.getFulfillmentProductById(result.qlsProductId);
224
225
  if (qlsProduct) {
225
- await this.options.saveAdditionalData(ctx, new core_1.Injector(this.moduleRef), qlsProduct, variant);
226
+ await this.options.productSync.saveAdditionalVariantData(ctx, new core_1.Injector(this.moduleRef), qlsProduct, variant);
226
227
  }
227
228
  }
228
229
  catch (e) {
@@ -289,7 +290,7 @@ let QlsProductService = class QlsProductService {
289
290
  * Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
290
291
  */
291
292
  async createOrUpdateProductInQls(ctx, client, variant, existingProduct) {
292
- if (await this.options.excludeVariantFromSync?.(ctx, new core_1.Injector(this.moduleRef), variant)) {
293
+ if (await this.options.productSync.excludeVariantFromSync?.(ctx, new core_1.Injector(this.moduleRef), variant)) {
293
294
  core_1.Logger.info(`Variant '${variant.sku}' excluded from sync to QLS.`, constants_1.loggerCtx);
294
295
  return { status: 'not-changed' };
295
296
  }
@@ -305,7 +306,7 @@ let QlsProductService = class QlsProductService {
305
306
  }
306
307
  let qlsProduct = existingProduct;
307
308
  let createdOrUpdated = 'not-changed';
308
- const { additionalEANs, ...additionalVariantFields } = this.options.getAdditionalVariantFields(ctx, variant);
309
+ const { additionalEANs, ...additionalVariantFields } = this.options.productSync.getAdditionalVariantFields(ctx, variant);
309
310
  if (!existingProduct) {
310
311
  const result = await client.createFulfillmentProduct({
311
312
  name: variant.name,
@@ -430,7 +431,7 @@ let QlsProductService = class QlsProductService {
430
431
  * Update stock level for a variant based on the given available stock
431
432
  */
432
433
  async updateStock(ctx, variantId, availableStock) {
433
- if (!this.options.synchronizeStockLevels) {
434
+ if (!this.options.productSync.synchronizeStockLevels) {
434
435
  core_1.Logger.warn(`Stock sync disabled. Not updating stock for variant '${variantId}'`, constants_1.loggerCtx);
435
436
  return;
436
437
  }
package/dist/types.d.ts CHANGED
@@ -1,69 +1,86 @@
1
1
  import { ID, Injector, Order, ProductVariant, RequestContext, SerializedRequestContext } from '@vendure/core';
2
- import { CustomValue, FulfillmentOrderInput, FulfillmentOrderLineInput, FulfillmentProduct, FulfillmentProductInput } from './lib/client-types';
2
+ import { CustomValue, FulfillmentOrder, FulfillmentOrderInput, FulfillmentOrderLineInput, FulfillmentProductDetail, 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
6
6
  */
7
7
  getConfig: (ctx: RequestContext) => QlsClientConfig | undefined | Promise<QlsClientConfig | undefined>;
8
- /**
9
- * Required to get the EAN and image URL for a product variant.
10
- * Also allows you to override other product attributes like name, price, etc.
11
- */
12
- getAdditionalVariantFields: (ctx: RequestContext, variant: ProductVariant) => AdditionalVariantFields;
13
- /**
14
- * Function to get the set service point code for an order.
15
- * Return undefined to not use a service point at all.
16
- */
17
- getAdditionalOrderFields?: (ctx: RequestContext, injector: Injector, order: Order) => Promise<AdditionalOrderFields | undefined> | AdditionalOrderFields | undefined;
18
8
  /**
19
9
  * A key used to verify if the caller is authorized to call the webhook.
20
10
  * Set this to a random string
21
11
  */
22
12
  webhookSecret: string;
23
13
  /**
24
- * Allows you to disable the pulling in of stock levels from QLS. When disabled, stock in Vendure will not be modified based on QLS stock levels.
25
- * Defaults to true.
26
- */
27
- synchronizeStockLevels?: boolean;
28
- /**
29
- * Allows you to disable the automatic pushing of orders to QLS. You can still push orders manually via the Admin UI.
30
- * Defaults to true.
31
- */
32
- autoPushOrders?: boolean;
33
- /**
34
- * Allows you to define a date from when the order should be processed by QLS.
35
- * You can for example make orders processable 2 hours from now, so that you can still edit the order in QLS
36
- * Defaults to now.
37
- */
38
- processOrderFrom?: (ctx: RequestContext, order: Order) => Date | Promise<Date>;
39
- /**
40
- * Optional function to determine if a product variant should be excluded from syncing to QLS.
41
- * Return true to exclude the variant from sync, false or undefined to include it.
42
- */
43
- excludeVariantFromSync?: (ctx: RequestContext, injector: Injector, variant: ProductVariant) => boolean | Promise<boolean>;
44
- /**
45
- * Optional function to customize the receiver contact details when creating a QLS order.
46
- * Allows you to set different fields or override default mapping from the order's shipping address and customer.
47
- * If not provided, default mapping will be used.
48
- */
49
- getReceiverContact?: (ctx: RequestContext, order: Order) => FulfillmentOrderInput['receiver_contact'] | undefined;
50
- /**
51
- * Admin UI tab name where the QLS Product ID custom field is shown on ProductVariant.
52
- * `null` will show the custom field on the default tab.
53
- * Defaults to 'QLS'.
54
- */
55
- qlsProductIdUiTab?: string | null;
56
- /**
57
- * Optional hook called after syncing a variant to QLS.
58
- * Receives the full QLS product, a RequestContext, and an Injector, allowing
59
- * you to save any additional data to your own database. The saving itself
60
- * happens inside this hook — if not provided, nothing is persisted.
14
+ * Options related to order pushing to QLS
61
15
  */
62
- saveAdditionalData?: (ctx: RequestContext, injector: Injector, qlsProduct: FulfillmentProduct, variant: ProductVariant) => Promise<void> | void;
16
+ orderSync: {
17
+ /**
18
+ * Function to get additional order fields when pushing an order to QLS.
19
+ * Return undefined to not use additional fields at all.
20
+ */
21
+ pushAdditionalOrderFields?: (ctx: RequestContext, injector: Injector, order: Order) => Promise<AdditionalOrderFields | undefined> | AdditionalOrderFields | undefined;
22
+ /**
23
+ * Allows you to disable the automatic pushing of orders to QLS. You can still push orders manually via the Admin UI.
24
+ * Defaults to true.
25
+ */
26
+ autoPushOrders?: boolean;
27
+ /**
28
+ * Allows you to define a date from when the order should be processed by QLS.
29
+ * You can for example make orders processable 2 hours from now, so that you can still edit the order in QLS
30
+ * Defaults to now.
31
+ */
32
+ processOrderFrom?: (ctx: RequestContext, order: Order) => Date | Promise<Date>;
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) => FulfillmentOrderInput['receiver_contact'] | undefined;
39
+ /**
40
+ * Optional hook called after an order has been successfully created in QLS.
41
+ * Receives the Vendure order, the created QLS order, a RequestContext, and an Injector.
42
+ * Errors in this hook are caught and logged, and will not fail the order push job.
43
+ */
44
+ pullAdditionalOrderFields?: (ctx: RequestContext, injector: Injector, order: Order, createdQlsOrder: FulfillmentOrder) => Promise<void> | void;
45
+ /**
46
+ * Additional order items to add to the QLS order.
47
+ */
48
+ addAdditionalOrderItems?: (ctx: RequestContext, injector: Injector, order: Order) => FulfillmentOrderLineInput[] | Promise<FulfillmentOrderLineInput[]>;
49
+ };
63
50
  /**
64
- * Additional order items to add to the QLS order.
51
+ * Options related to product syncing to QLS
65
52
  */
66
- addAdditionalOrderItems?: (ctx: RequestContext, injector: Injector, order: Order) => FulfillmentOrderLineInput[] | Promise<FulfillmentOrderLineInput[]>;
53
+ productSync: {
54
+ /**
55
+ * Required to get the EAN and image URL for a product variant.
56
+ * Also allows you to override other product attributes like name, price, etc.
57
+ */
58
+ getAdditionalVariantFields: (ctx: RequestContext, variant: ProductVariant) => AdditionalVariantFields;
59
+ /**
60
+ * Allows you to disable the pulling in of stock levels from QLS. When disabled, stock in Vendure will not be modified based on QLS stock levels.
61
+ * Defaults to true.
62
+ */
63
+ synchronizeStockLevels?: boolean;
64
+ /**
65
+ * Optional function to determine if a product variant should be excluded from syncing to QLS.
66
+ * Return true to exclude the variant from sync, false or undefined to include it.
67
+ * This is also used during order pushing: variants that are excluded from sync will not be pushed to QLS in an order.
68
+ */
69
+ excludeVariantFromSync?: (ctx: RequestContext, injector: Injector, variant: ProductVariant) => boolean | Promise<boolean>;
70
+ /**
71
+ * Optional hook called after syncing a variant to QLS.
72
+ * Receives the full QLS product, a RequestContext, and an Injector, allowing
73
+ * you to save any additional data to your own database. The saving itself
74
+ * happens inside this hook — if not provided, nothing is persisted.
75
+ */
76
+ saveAdditionalVariantData?: (ctx: RequestContext, injector: Injector, qlsProduct: FulfillmentProductDetail, variant: ProductVariant) => Promise<void> | void;
77
+ /**
78
+ * Admin UI tab name where the QLS Product ID custom field is shown on ProductVariant.
79
+ * `null` will show the custom field on the default tab.
80
+ * Defaults to 'QLS'.
81
+ */
82
+ qlsProductIdUiTab?: string | null;
83
+ };
67
84
  }
68
85
  /**
69
86
  * 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.9.0",
3
+ "version": "2.0.0",
4
4
  "description": "Vendure plugin to fulfill orders via QLS.",
5
5
  "keywords": [
6
6
  "fulfillment",