@pinelab/vendure-plugin-qls-fulfillment 2.0.2 → 2.1.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,10 @@
1
+ # 2.1.0 (2026-07-08)
2
+
3
+ - Added `addAdditionalEANSToQLS` admin mutation to manually add additional EANs/barcodes to existing QLS products.
4
+ - Added admin UI action on the product variant detail page to prompt for and add a single additional EAN.
5
+ - Removed automatic syncing of additional EANs during product sync; EANs must now be added manually via the mutation.
6
+ - Removed obsolete `util.ts`, `util.spec.ts`, and `removeBarcode` QLS client method.
7
+
1
8
  # 2.0.2 (2026-07-01)
2
9
 
3
10
  - Verify barcode deletion in QLS by fetching remaining barcodes after removal attempt
package/README.md CHANGED
@@ -15,6 +15,20 @@ This plugin contains:
15
15
  - Partial product sync: Creates/updates products in QLS when products change in Vendure
16
16
  - Partial stock sync: Updates Vendure stock based on incoming QLS webhooks
17
17
 
18
+ ## Additional EANs
19
+
20
+ Additional EANs (or barcodes) are not automatically synced from Vendure to QLS. They can only be added manually via the `addAdditionalEANSToQLS` admin mutation. This mutation only adds EANs that do not already exist in QLS; existing EANs are skipped.
21
+
22
+ Removal of additional EANs is not supported by this plugin and must be done inside the QLS platform itself. QLS may have assigned stock to those barcodes, so removing them should be handled in QLS directly.
23
+
24
+ ```graphql
25
+ mutation AddAdditionalEANSToQLS($variantId: ID!, $additionalEANS: [String!]!) {
26
+ addAdditionalEANSToQLS(variantId: $variantId, additionalEANS: $additionalEANS)
27
+ }
28
+ ```
29
+
30
+ The mutation returns the list of all existing EANs for the product in QLS after the operation. The variant must already have a `qlsProductId` stored in its custom fields (i.e. it must have been synced to QLS first), otherwise an error is thrown. After adding EANs, the mutation also triggers a sync of the variant to QLS to ensure product fields are up to date.
31
+
18
32
  ## Order Sync
19
33
 
20
34
  - Pushes orders to QLS automatically on order placement
@@ -44,7 +58,6 @@ plugins: [
44
58
  getAdditionalVariantFields: (ctx, variant) => ({
45
59
  ean: variant.sku,
46
60
  image_url: `https://your-cdn.com/${variant.featuredAsset?.preview}`,
47
- additionalEANs: ['1234567890'],
48
61
  }),
49
62
  // Optional: exclude certain variants from being synced to QLS
50
63
  excludeVariantFromSync: (ctx, injector, variant) => {
@@ -19,6 +19,15 @@ exports.adminApiExtensions = (0, graphql_tag_1.default) `
19
19
  """
20
20
  triggerQlsProductSync: Boolean!
21
21
 
22
+ """
23
+ Add additional EANs/barcodes to an existing product in QLS.
24
+ Only EANs that do not already exist in QLS are added. Removal of EANs must be done in QLS itself.
25
+ """
26
+ addAdditionalEANSToQLS(
27
+ variantId: ID!
28
+ additionalEANS: [String!]!
29
+ ): [String!]!
30
+
22
31
  """
23
32
  Manually push an order to QLS (again)
24
33
  """
@@ -21,11 +21,20 @@ export type Scalars = {
21
21
  };
22
22
  export type Mutation = {
23
23
  __typename?: 'Mutation';
24
+ /**
25
+ * Add additional EANs/barcodes to an existing product in QLS.
26
+ * Only EANs that do not already exist in QLS are added. Removal of EANs must be done in QLS itself.
27
+ */
28
+ addAdditionalEANSToQLS: Array<Scalars['String']>;
24
29
  /** Manually push an order to QLS (again) */
25
30
  pushOrderToQls: Scalars['String'];
26
31
  /** Trigger a sync to create or update all products in Vendure to QLS, and pull in stock levels from QLS. */
27
32
  triggerQlsProductSync: Scalars['Boolean'];
28
33
  };
34
+ export type MutationAddAdditionalEansToQlsArgs = {
35
+ additionalEANS: Array<Scalars['String']>;
36
+ variantId: Scalars['ID'];
37
+ };
29
38
  export type MutationPushOrderToQlsArgs = {
30
39
  orderId: Scalars['ID'];
31
40
  };
@@ -1,12 +1,13 @@
1
1
  import { Order, RequestContext } from '@vendure/core';
2
2
  import { QlsOrderService } from '../services/qls-order.service';
3
3
  import { QlsProductService } from '../services/qls-product.service';
4
- import { MutationPushOrderToQlsArgs } from './generated/graphql';
4
+ import { MutationAddAdditionalEansToQlsArgs, MutationPushOrderToQlsArgs } from './generated/graphql';
5
5
  export declare class QlsAdminResolver {
6
6
  private qlsProductService;
7
7
  private qlsOrderService;
8
8
  constructor(qlsProductService: QlsProductService, qlsOrderService: QlsOrderService);
9
9
  qlsOrderIds(ctx: RequestContext, order: Order): Promise<string[]>;
10
10
  triggerQlsProductSync(ctx: RequestContext): Promise<boolean>;
11
+ addAdditionalEANSToQLS(ctx: RequestContext, input: MutationAddAdditionalEansToQlsArgs): Promise<string[]>;
11
12
  pushOrderToQls(ctx: RequestContext, input: MutationPushOrderToQlsArgs): Promise<string>;
12
13
  }
@@ -30,6 +30,9 @@ let QlsAdminResolver = class QlsAdminResolver {
30
30
  await this.qlsProductService.triggerFullSync(ctx);
31
31
  return true;
32
32
  }
33
+ async addAdditionalEANSToQLS(ctx, input) {
34
+ return await this.qlsProductService.addAdditionalEANsToQls(ctx, input.variantId, input.additionalEANS);
35
+ }
33
36
  async pushOrderToQls(ctx, input) {
34
37
  return await this.qlsOrderService.pushOrderToQls(ctx, input.orderId, true);
35
38
  }
@@ -56,6 +59,16 @@ __decorate([
56
59
  __metadata("design:paramtypes", [core_1.RequestContext]),
57
60
  __metadata("design:returntype", Promise)
58
61
  ], QlsAdminResolver.prototype, "triggerQlsProductSync", null);
62
+ __decorate([
63
+ (0, graphql_1.Mutation)(),
64
+ (0, core_1.Transaction)(),
65
+ (0, core_1.Allow)(permissions_1.qlsFullSyncPermission.Permission),
66
+ __param(0, (0, core_1.Ctx)()),
67
+ __param(1, (0, graphql_1.Args)()),
68
+ __metadata("design:type", Function),
69
+ __metadata("design:paramtypes", [core_1.RequestContext, Object]),
70
+ __metadata("design:returntype", Promise)
71
+ ], QlsAdminResolver.prototype, "addAdditionalEANSToQLS", null);
59
72
  __decorate([
60
73
  (0, graphql_1.Mutation)(),
61
74
  (0, core_1.Transaction)(),
@@ -33,10 +33,6 @@ export declare class QlsClient {
33
33
  * Add an extra barcode to a fulfillment product in QLS
34
34
  */
35
35
  addBarcode(productId: string, barcode: string): Promise<void>;
36
- /**
37
- * Add an extra barcode to a fulfillment product in QLS
38
- */
39
- removeBarcode(productId: string, barcodeId: number): Promise<void>;
40
36
  getServicePoints(countryCode: string, postalCode: string): Promise<QlsServicePoint[]>;
41
37
  rawRequest<T>(method: 'POST' | 'GET' | 'PUT' | 'DELETE', action: string, data?: unknown): Promise<QlsApiResponse<T | undefined>>;
42
38
  }
@@ -100,12 +100,6 @@ class QlsClient {
100
100
  barcode,
101
101
  });
102
102
  }
103
- /**
104
- * Add an extra barcode to a fulfillment product in QLS
105
- */
106
- async removeBarcode(productId, barcodeId) {
107
- await this.rawRequest('DELETE', `fulfillment/products/${productId}/barcodes/${barcodeId}`);
108
- }
109
103
  async getServicePoints(countryCode, postalCode) {
110
104
  countryCode = countryCode.toUpperCase();
111
105
  const result = await this.rawRequest('GET', `service-points/${countryCode}/${postalCode}`);
@@ -55,6 +55,11 @@ export declare class QlsProductService implements OnModuleInit, OnApplicationBoo
55
55
  * Trigger a product sync job for particular product variants
56
56
  */
57
57
  triggerSyncVariants(ctx: RequestContext, productVariantIds: ID[]): Promise<import("@vendure/core/dist/job-queue/subscribable-job").SubscribableJob<QlsProductJobData> | undefined>;
58
+ /**
59
+ * Add additional EANs/barcodes to an existing QLS product.
60
+ * Removal of EANs must be done in QLS itself.
61
+ */
62
+ addAdditionalEANsToQls(ctx: RequestContext, variantId: ID, additionalEANs: string[]): Promise<string[]>;
58
63
  /**
59
64
  * Update the stock level for a variant based on the given available stock
60
65
  */
@@ -66,10 +71,6 @@ export declare class QlsProductService implements OnModuleInit, OnApplicationBoo
66
71
  status: 'created' | 'updated' | 'not-changed';
67
72
  qlsProductId?: string;
68
73
  }>;
69
- /**
70
- * Update the additional EANs/barcodes for a product in QLS if needed
71
- */
72
- private updateAdditionalEANs;
73
74
  /**
74
75
  * Determine if a product needs to be updated in QLS based on the given variant and QLS product.
75
76
  */
@@ -23,7 +23,6 @@ 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");
27
26
  const qls_variant_sync_failed_event_1 = require("./qls-variant-sync-failed-event");
28
27
  const core_2 = require("@nestjs/core");
29
28
  // Wait for 700ms to avoid rate limit of 500/5 minutes
@@ -270,6 +269,33 @@ let QlsProductService = class QlsProductService {
270
269
  productVariantIds,
271
270
  }, { retries: 1 });
272
271
  }
272
+ /**
273
+ * Add additional EANs/barcodes to an existing QLS product.
274
+ * Removal of EANs must be done in QLS itself.
275
+ */
276
+ async addAdditionalEANsToQls(ctx, variantId, additionalEANs) {
277
+ const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
278
+ if (!client) {
279
+ throw new Error(`QLS not enabled for channel ${ctx.channel.token}`);
280
+ }
281
+ const variant = await this.variantService.findOne(ctx, variantId, []);
282
+ if (!variant) {
283
+ throw new Error(`Variant with id ${variantId} not found`);
284
+ }
285
+ const qlsProductId = variant.customFields.qlsProductId;
286
+ if (!qlsProductId) {
287
+ throw new Error(`Variant '${variant.sku}' does not have a QLS product ID. Sync the variant to QLS first.`);
288
+ }
289
+ const eansToAdd = additionalEANs
290
+ .map((ean) => ean.trim())
291
+ .filter((ean) => ean.length > 0);
292
+ await Promise.all(eansToAdd.map((ean) => client.addBarcode(qlsProductId, ean)));
293
+ if (eansToAdd.length > 0) {
294
+ core_1.Logger.info(`Added additional EANs: ${eansToAdd.join(',')} to variant '${variant.sku}' in QLS`, constants_1.loggerCtx);
295
+ }
296
+ await this.triggerSyncVariants(ctx, [variantId]);
297
+ return eansToAdd;
298
+ }
273
299
  /**
274
300
  * Update the stock level for a variant based on the given available stock
275
301
  */
@@ -306,7 +332,7 @@ let QlsProductService = class QlsProductService {
306
332
  }
307
333
  let qlsProduct = existingProduct;
308
334
  let createdOrUpdated = 'not-changed';
309
- const { additionalEANs, ...additionalVariantFields } = this.options.productSync.getAdditionalVariantFields(ctx, variant);
335
+ const additionalVariantFields = this.options.productSync.getAdditionalVariantFields(ctx, variant);
310
336
  if (!existingProduct) {
311
337
  const result = await client.createFulfillmentProduct({
312
338
  name: variant.name,
@@ -334,59 +360,13 @@ let QlsProductService = class QlsProductService {
334
360
  .update({ id: variant.id }, { customFields: { qlsProductId: qlsProduct.id } });
335
361
  core_1.Logger.info(`Set QLS product ID for variant '${variant.sku}' to ${qlsProduct.id}`, constants_1.loggerCtx);
336
362
  }
337
- if (qlsProduct) {
338
- const updatedEANs = await this.updateAdditionalEANs(ctx, client, qlsProduct, additionalEANs, variant);
339
- if (createdOrUpdated === 'not-changed' && updatedEANs) {
340
- // If nothing changed so far, but EANs changed, then we did update the product in QLS
341
- createdOrUpdated = 'updated';
342
- }
343
- }
363
+ // Note: additional EANs are no longer synced automatically during product sync.
364
+ // Use the addAdditionalEANsToQls mutation to add EANs manually.
344
365
  if (createdOrUpdated === 'not-changed') {
345
366
  core_1.Logger.info(`Variant '${variant.sku}' not updated in QLS, because no changes were found.`, constants_1.loggerCtx);
346
367
  }
347
368
  return { status: createdOrUpdated, qlsProductId: qlsProduct?.id };
348
369
  }
349
- /**
350
- * Update the additional EANs/barcodes for a product in QLS if needed
351
- */
352
- async updateAdditionalEANs(ctx, client, qlsProduct, additionalEANs, variant) {
353
- const existingAdditionalEANs = qlsProduct?.barcodes_and_ean.filter((ean) => ean !== qlsProduct.ean); // Remove the main EAN
354
- const eansToUpdate = (0, util_2.getEansToUpdate)({
355
- existingEans: existingAdditionalEANs,
356
- desiredEans: additionalEANs,
357
- });
358
- if (!eansToUpdate ||
359
- (eansToUpdate.eansToAdd.length === 0 &&
360
- eansToUpdate.eansToRemove.length === 0)) {
361
- // No updates needed
362
- return false;
363
- }
364
- // Add additional EANs
365
- eansToUpdate.eansToAdd = (0, util_2.normalizeEans)(eansToUpdate.eansToAdd);
366
- await Promise.all(eansToUpdate.eansToAdd.map((ean) => client.addBarcode(qlsProduct.id, ean)));
367
- if (eansToUpdate.eansToAdd.length > 0) {
368
- core_1.Logger.info(`Added additional EANs: ${eansToUpdate.eansToAdd.join(',')} to product '${qlsProduct.sku}' in QLS`, constants_1.loggerCtx);
369
- }
370
- // Remove EANs, normalize first
371
- eansToUpdate.eansToRemove = (0, util_2.normalizeEans)(eansToUpdate.eansToRemove);
372
- // get barcode ID's to remove, because deletion goes via barcode ID, not barcode value
373
- const barcodeIdsToRemove = qlsProduct.barcodes
374
- .filter((barcode) => eansToUpdate.eansToRemove.includes(barcode.barcode))
375
- .map((barcode) => barcode.id);
376
- await Promise.all(barcodeIdsToRemove.map((barcodeId) => client.removeBarcode(qlsProduct.id, barcodeId)));
377
- // Fetch remaining barcodes from QLS to verify deletion worked
378
- const remainingBarcodes = await client.getBarcodes(qlsProduct.id);
379
- const remainingEansToRemove = eansToUpdate.eansToRemove.filter((ean) => remainingBarcodes.some((barcode) => barcode.barcode === ean));
380
- if (remainingEansToRemove.length > 0) {
381
- const errorMessage = `Failed to remove EANs '${remainingEansToRemove.join(',')}' from product '${qlsProduct.sku}' in QLS. Barcodes still present after deletion attempt.`;
382
- core_1.Logger.error(errorMessage, constants_1.loggerCtx);
383
- await this.eventBus.publish(new qls_variant_sync_failed_event_1.QlsVariantSyncFailedEvent(ctx, variant, new Date(), errorMessage));
384
- }
385
- else if (eansToUpdate.eansToRemove.length > 0) {
386
- core_1.Logger.info(`Removed EANs '${eansToUpdate.eansToRemove.join(',')}' from product '${qlsProduct.sku}' in QLS`, constants_1.loggerCtx);
387
- }
388
- return true;
389
- }
390
370
  /**
391
371
  * Determine if a product needs to be updated in QLS based on the given variant and QLS product.
392
372
  */
package/dist/types.d.ts CHANGED
@@ -87,7 +87,6 @@ export interface QlsPluginOptions {
87
87
  */
88
88
  export type AdditionalVariantFields = Partial<FulfillmentProductInput & {
89
89
  ean: string;
90
- additionalEANs?: string[];
91
90
  }>;
92
91
  export interface AdditionalOrderFields {
93
92
  servicepoint_code?: string;
@@ -104,4 +104,47 @@ export default [
104
104
  });
105
105
  },
106
106
  }),
107
+ addActionBarDropdownMenuItem({
108
+ id: 'qls-fulfillment-add-ean',
109
+ label: 'Add additional EAN to QLS',
110
+ locationId: 'product-variant-detail',
111
+ icon: 'resistor',
112
+ requiresPermission: ['QLSFullSync'],
113
+ onClick: (_, { route, dataService, notificationService }) => {
114
+ const ean = window.prompt('Enter additional EAN to add to QLS:');
115
+ if (!ean) {
116
+ return;
117
+ }
118
+ const variantId = route.snapshot.params.id;
119
+ dataService
120
+ .mutate(
121
+ gql`
122
+ mutation AddAdditionalEANSToQLS(
123
+ $variantId: ID!
124
+ $additionalEANS: [String!]!
125
+ ) {
126
+ addAdditionalEANSToQLS(
127
+ variantId: $variantId
128
+ additionalEANS: $additionalEANS
129
+ )
130
+ }
131
+ `,
132
+ { variantId, additionalEANS: [ean] }
133
+ )
134
+ .subscribe({
135
+ next: () => {
136
+ notificationService.notify({
137
+ message: `Added EAN ${ean} to QLS`,
138
+ type: 'success',
139
+ });
140
+ },
141
+ error: (err) => {
142
+ notificationService.notify({
143
+ message: err.message,
144
+ type: 'error',
145
+ });
146
+ },
147
+ });
148
+ },
149
+ }),
107
150
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pinelab/vendure-plugin-qls-fulfillment",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "Vendure plugin to fulfill orders via QLS.",
5
5
  "keywords": [
6
6
  "fulfillment",
@@ -1,15 +0,0 @@
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[];
@@ -1,31 +0,0 @@
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
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,60 +0,0 @@
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
- });