@pinelab/vendure-plugin-qls-fulfillment 2.0.2 → 2.2.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,14 @@
1
+ # 2.2.0 (2026-07-15)
2
+
3
+ - Added buttons on variant and order detail to view the product and order in QLS dashboard.
4
+
5
+ # 2.1.0 (2026-07-08)
6
+
7
+ - Added `addAdditionalEANSToQLS` admin mutation to manually add additional EANs/barcodes to existing QLS products.
8
+ - Added admin UI action on the product variant detail page to prompt for and add a single additional EAN.
9
+ - Removed automatic syncing of additional EANs during product sync; EANs must now be added manually via the mutation.
10
+ - Removed obsolete `util.ts`, `util.spec.ts`, and `removeBarcode` QLS client method.
11
+
1
12
  # 2.0.2 (2026-07-01)
2
13
 
3
14
  - 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) => {
@@ -11,6 +11,18 @@ exports.adminApiExtensions = (0, graphql_tag_1.default) `
11
11
  QLS order id(s) for this Vendure order.
12
12
  """
13
13
  qlsOrderIds: [String!]!
14
+
15
+ """
16
+ Direct URL to the most recent QLS order for this Vendure order, or null if none exists.
17
+ """
18
+ qlsOrderUrl: String
19
+ }
20
+
21
+ extend type ProductVariant {
22
+ """
23
+ Direct URL to the QLS product for this Vendure variant, or null if none exists.
24
+ """
25
+ qlsProductUrl: String
14
26
  }
15
27
 
16
28
  extend type Mutation {
@@ -19,6 +31,15 @@ exports.adminApiExtensions = (0, graphql_tag_1.default) `
19
31
  """
20
32
  triggerQlsProductSync: Boolean!
21
33
 
34
+ """
35
+ Add additional EANs/barcodes to an existing product in QLS.
36
+ Only EANs that do not already exist in QLS are added. Removal of EANs must be done in QLS itself.
37
+ """
38
+ addAdditionalEANSToQLS(
39
+ variantId: ID!
40
+ additionalEANS: [String!]!
41
+ ): [String!]!
42
+
22
43
  """
23
44
  Manually push an order to QLS (again)
24
45
  """
@@ -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
  };
@@ -33,6 +42,13 @@ export type Order = {
33
42
  __typename?: 'Order';
34
43
  /** QLS order id(s) for this Vendure order. */
35
44
  qlsOrderIds: Array<Scalars['String']>;
45
+ /** Direct URL to the most recent QLS order for this Vendure order, or null if none exists. */
46
+ qlsOrderUrl?: Maybe<Scalars['String']>;
47
+ };
48
+ export type ProductVariant = {
49
+ __typename?: 'ProductVariant';
50
+ /** Direct URL to the QLS product for this Vendure variant, or null if none exists. */
51
+ qlsProductUrl?: Maybe<Scalars['String']>;
36
52
  };
37
53
  export type QlsServicePoint = {
38
54
  __typename?: 'QlsServicePoint';
@@ -1,12 +1,15 @@
1
- import { Order, RequestContext } from '@vendure/core';
1
+ import { Order, ProductVariant, 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
+ qlsOrderUrl(ctx: RequestContext, order: Order): Promise<string | null>;
11
+ qlsProductUrl(ctx: RequestContext, productVariant: ProductVariant): Promise<string | null>;
10
12
  triggerQlsProductSync(ctx: RequestContext): Promise<boolean>;
13
+ addAdditionalEANSToQLS(ctx: RequestContext, input: MutationAddAdditionalEansToQlsArgs): Promise<string[]>;
11
14
  pushOrderToQls(ctx: RequestContext, input: MutationPushOrderToQlsArgs): Promise<string>;
12
15
  }
@@ -26,10 +26,19 @@ let QlsAdminResolver = class QlsAdminResolver {
26
26
  async qlsOrderIds(ctx, order) {
27
27
  return this.qlsOrderService.getQlsOrderIdsForOrder(ctx, order.id);
28
28
  }
29
+ async qlsOrderUrl(ctx, order) {
30
+ return this.qlsOrderService.getQlsOrderUrl(ctx, order.id);
31
+ }
32
+ async qlsProductUrl(ctx, productVariant) {
33
+ return this.qlsProductService.getQlsProductUrl(ctx, productVariant);
34
+ }
29
35
  async triggerQlsProductSync(ctx) {
30
36
  await this.qlsProductService.triggerFullSync(ctx);
31
37
  return true;
32
38
  }
39
+ async addAdditionalEANSToQLS(ctx, input) {
40
+ return await this.qlsProductService.addAdditionalEANsToQls(ctx, input.variantId, input.additionalEANS);
41
+ }
33
42
  async pushOrderToQls(ctx, input) {
34
43
  return await this.qlsOrderService.pushOrderToQls(ctx, input.orderId, true);
35
44
  }
@@ -47,6 +56,28 @@ __decorate([
47
56
  core_1.Order]),
48
57
  __metadata("design:returntype", Promise)
49
58
  ], QlsAdminResolver.prototype, "qlsOrderIds", null);
59
+ __decorate([
60
+ (0, graphql_1.ResolveField)(),
61
+ (0, graphql_1.Resolver)('Order'),
62
+ (0, core_1.Allow)(permissions_1.qlsPushOrderPermission.Permission),
63
+ (0, core_1.Allow)(core_1.Permission.UpdateAdministrator),
64
+ __param(0, (0, core_1.Ctx)()),
65
+ __param(1, (0, graphql_1.Parent)()),
66
+ __metadata("design:type", Function),
67
+ __metadata("design:paramtypes", [core_1.RequestContext,
68
+ core_1.Order]),
69
+ __metadata("design:returntype", Promise)
70
+ ], QlsAdminResolver.prototype, "qlsOrderUrl", null);
71
+ __decorate([
72
+ (0, graphql_1.ResolveField)(),
73
+ (0, graphql_1.Resolver)('ProductVariant'),
74
+ __param(0, (0, core_1.Ctx)()),
75
+ __param(1, (0, graphql_1.Parent)()),
76
+ __metadata("design:type", Function),
77
+ __metadata("design:paramtypes", [core_1.RequestContext,
78
+ core_1.ProductVariant]),
79
+ __metadata("design:returntype", Promise)
80
+ ], QlsAdminResolver.prototype, "qlsProductUrl", null);
50
81
  __decorate([
51
82
  (0, graphql_1.Mutation)(),
52
83
  (0, core_1.Transaction)(),
@@ -56,6 +87,16 @@ __decorate([
56
87
  __metadata("design:paramtypes", [core_1.RequestContext]),
57
88
  __metadata("design:returntype", Promise)
58
89
  ], QlsAdminResolver.prototype, "triggerQlsProductSync", null);
90
+ __decorate([
91
+ (0, graphql_1.Mutation)(),
92
+ (0, core_1.Transaction)(),
93
+ (0, core_1.Allow)(permissions_1.qlsFullSyncPermission.Permission),
94
+ __param(0, (0, core_1.Ctx)()),
95
+ __param(1, (0, graphql_1.Args)()),
96
+ __metadata("design:type", Function),
97
+ __metadata("design:paramtypes", [core_1.RequestContext, Object]),
98
+ __metadata("design:returntype", Promise)
99
+ ], QlsAdminResolver.prototype, "addAdditionalEANSToQLS", null);
59
100
  __decorate([
60
101
  (0, graphql_1.Mutation)(),
61
102
  (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}`);
@@ -36,6 +36,11 @@ export declare class QlsOrderService implements OnModuleInit, OnApplicationBoots
36
36
  * Get QLS order id(s) for a Vendure order (for Order.qlsOrderIds field).
37
37
  */
38
38
  getQlsOrderIdsForOrder(ctx: RequestContext, orderId: ID): Promise<string[]>;
39
+ /**
40
+ * Get the direct URL to the most recent QLS order for a Vendure order (for Order.qlsOrderUrl field).
41
+ * Returns the URL or null if no QLS order exists.
42
+ */
43
+ getQlsOrderUrl(ctx: RequestContext, orderId: ID): Promise<string | null>;
39
44
  getServicePoints(ctx: RequestContext, input: QlsServicePointSearchInput): Promise<QlsServicePoint[]>;
40
45
  private getVendureOrderState;
41
46
  }
@@ -281,9 +281,30 @@ let QlsOrderService = class QlsOrderService {
281
281
  .getRepository(ctx, qls_order_entity_entity_1.QlsOrderEntity)
282
282
  .find({
283
283
  where: { vendureOrderId: orderId },
284
+ order: { createdAt: 'DESC' },
284
285
  });
285
286
  return entities.map((e) => String(e.qlsOrderId));
286
287
  }
288
+ /**
289
+ * Get the direct URL to the most recent QLS order for a Vendure order (for Order.qlsOrderUrl field).
290
+ * Returns the URL or null if no QLS order exists.
291
+ */
292
+ async getQlsOrderUrl(ctx, orderId) {
293
+ const entities = await this.connection
294
+ .getRepository(ctx, qls_order_entity_entity_1.QlsOrderEntity)
295
+ .find({
296
+ where: { vendureOrderId: orderId },
297
+ order: { createdAt: 'DESC' },
298
+ });
299
+ if (entities.length === 0) {
300
+ return null;
301
+ }
302
+ const config = await Promise.resolve(this.options.getConfig(ctx));
303
+ if (!config) {
304
+ return null;
305
+ }
306
+ return `https://mijn.pakketdienstqls.nl/company/${config.companyId}/fulfillment/order/${entities[0].qlsOrderId}`;
307
+ }
287
308
  async getServicePoints(ctx, input) {
288
309
  const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
289
310
  if (!client) {
@@ -55,6 +55,16 @@ 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
+ * Get the direct URL to the QLS product for a Vendure product variant.
60
+ * Returns the URL or null if no QLS product exists.
61
+ */
62
+ getQlsProductUrl(ctx: RequestContext, productVariant: ProductVariant): Promise<string | null>;
63
+ /**
64
+ * Add additional EANs/barcodes to an existing QLS product.
65
+ * Removal of EANs must be done in QLS itself.
66
+ */
67
+ addAdditionalEANsToQls(ctx: RequestContext, variantId: ID, additionalEANs: string[]): Promise<string[]>;
58
68
  /**
59
69
  * Update the stock level for a variant based on the given available stock
60
70
  */
@@ -66,10 +76,6 @@ export declare class QlsProductService implements OnModuleInit, OnApplicationBoo
66
76
  status: 'created' | 'updated' | 'not-changed';
67
77
  qlsProductId?: string;
68
78
  }>;
69
- /**
70
- * Update the additional EANs/barcodes for a product in QLS if needed
71
- */
72
- private updateAdditionalEANs;
73
79
  /**
74
80
  * Determine if a product needs to be updated in QLS based on the given variant and QLS product.
75
81
  */
@@ -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,48 @@ let QlsProductService = class QlsProductService {
270
269
  productVariantIds,
271
270
  }, { retries: 1 });
272
271
  }
272
+ /**
273
+ * Get the direct URL to the QLS product for a Vendure product variant.
274
+ * Returns the URL or null if no QLS product exists.
275
+ */
276
+ async getQlsProductUrl(ctx, productVariant) {
277
+ const qlsProductId = productVariant.customFields?.qlsProductId;
278
+ if (!qlsProductId) {
279
+ return null;
280
+ }
281
+ const config = await Promise.resolve(this.options.getConfig(ctx));
282
+ if (!config) {
283
+ return null;
284
+ }
285
+ return `https://mijn.pakketdienstqls.nl/company/${config.companyId}/fulfillment/product/${qlsProductId}`;
286
+ }
287
+ /**
288
+ * Add additional EANs/barcodes to an existing QLS product.
289
+ * Removal of EANs must be done in QLS itself.
290
+ */
291
+ async addAdditionalEANsToQls(ctx, variantId, additionalEANs) {
292
+ const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
293
+ if (!client) {
294
+ throw new Error(`QLS not enabled for channel ${ctx.channel.token}`);
295
+ }
296
+ const variant = await this.variantService.findOne(ctx, variantId, []);
297
+ if (!variant) {
298
+ throw new Error(`Variant with id ${variantId} not found`);
299
+ }
300
+ const qlsProductId = variant.customFields.qlsProductId;
301
+ if (!qlsProductId) {
302
+ throw new Error(`Variant '${variant.sku}' does not have a QLS product ID. Sync the variant to QLS first.`);
303
+ }
304
+ const eansToAdd = additionalEANs
305
+ .map((ean) => ean.trim())
306
+ .filter((ean) => ean.length > 0);
307
+ await Promise.all(eansToAdd.map((ean) => client.addBarcode(qlsProductId, ean)));
308
+ if (eansToAdd.length > 0) {
309
+ core_1.Logger.info(`Added additional EANs: ${eansToAdd.join(',')} to variant '${variant.sku}' in QLS`, constants_1.loggerCtx);
310
+ }
311
+ await this.triggerSyncVariants(ctx, [variantId]);
312
+ return eansToAdd;
313
+ }
273
314
  /**
274
315
  * Update the stock level for a variant based on the given available stock
275
316
  */
@@ -306,7 +347,7 @@ let QlsProductService = class QlsProductService {
306
347
  }
307
348
  let qlsProduct = existingProduct;
308
349
  let createdOrUpdated = 'not-changed';
309
- const { additionalEANs, ...additionalVariantFields } = this.options.productSync.getAdditionalVariantFields(ctx, variant);
350
+ const additionalVariantFields = this.options.productSync.getAdditionalVariantFields(ctx, variant);
310
351
  if (!existingProduct) {
311
352
  const result = await client.createFulfillmentProduct({
312
353
  name: variant.name,
@@ -334,59 +375,13 @@ let QlsProductService = class QlsProductService {
334
375
  .update({ id: variant.id }, { customFields: { qlsProductId: qlsProduct.id } });
335
376
  core_1.Logger.info(`Set QLS product ID for variant '${variant.sku}' to ${qlsProduct.id}`, constants_1.loggerCtx);
336
377
  }
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
- }
378
+ // Note: additional EANs are no longer synced automatically during product sync.
379
+ // Use the addAdditionalEANsToQls mutation to add EANs manually.
344
380
  if (createdOrUpdated === 'not-changed') {
345
381
  core_1.Logger.info(`Variant '${variant.sku}' not updated in QLS, because no changes were found.`, constants_1.loggerCtx);
346
382
  }
347
383
  return { status: createdOrUpdated, qlsProductId: qlsProduct?.id };
348
384
  }
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
385
  /**
391
386
  * Determine if a product needs to be updated in QLS based on the given variant and QLS product.
392
387
  */
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;
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  addActionBarDropdownMenuItem,
3
+ addActionBarItem,
3
4
  ModalService,
4
5
  } from '@vendure/admin-ui/core';
5
- import { firstValueFrom } from 'rxjs';
6
+ import { firstValueFrom, of } from 'rxjs';
7
+ import { map, catchError } from 'rxjs/operators';
6
8
  import gql from 'graphql-tag';
7
9
 
8
10
  export default [
@@ -38,6 +40,82 @@ export default [
38
40
  });
39
41
  },
40
42
  }),
43
+ // QLS order link button on order detail page
44
+ addActionBarItem({
45
+ id: 'qls-visit-order',
46
+ label: 'QLS',
47
+ locationId: 'order-detail',
48
+ buttonColor: 'primary',
49
+ buttonStyle: 'outline',
50
+ icon: 'link',
51
+ buttonState: (context) => {
52
+ return context.entity$.pipe(
53
+ map((entity) => ({
54
+ visible: !!entity?.orderPlacedAt,
55
+ disabled: false,
56
+ }))
57
+ );
58
+ },
59
+ onClick: async (event, { route, dataService, notificationService }) => {
60
+ const orderId = route.snapshot.params.id;
61
+ const res = await firstValueFrom(
62
+ dataService.query(
63
+ gql`
64
+ query Order($orderId: ID!) {
65
+ order(id: $orderId) {
66
+ id
67
+ qlsOrderUrl
68
+ }
69
+ }
70
+ `,
71
+ { orderId }
72
+ ).single$
73
+ );
74
+ const url = (res as any).order.qlsOrderUrl;
75
+ if (url) {
76
+ window.open(url, '_blank');
77
+ } else {
78
+ notificationService.notify({
79
+ message: 'No QLS order found for this order.',
80
+ type: 'error',
81
+ });
82
+ }
83
+ },
84
+ }),
85
+ // QLS product link button on product variant detail page
86
+ addActionBarItem({
87
+ id: 'qls-visit-product',
88
+ label: 'QLS',
89
+ locationId: 'product-variant-detail',
90
+ buttonColor: 'primary',
91
+ buttonStyle: 'outline',
92
+ icon: 'link',
93
+ onClick: async (event, { route, dataService, notificationService }) => {
94
+ const variantId = route.snapshot.params.id;
95
+ const res = await firstValueFrom(
96
+ dataService.query(
97
+ gql`
98
+ query ProductVariant($id: ID!) {
99
+ productVariant(id: $id) {
100
+ id
101
+ qlsProductUrl
102
+ }
103
+ }
104
+ `,
105
+ { id: variantId }
106
+ ).single$
107
+ );
108
+ const url = (res as any).productVariant.qlsProductUrl;
109
+ if (url) {
110
+ window.open(url, '_blank');
111
+ } else {
112
+ notificationService.notify({
113
+ message: 'No QLS product found for this variant.',
114
+ type: 'error',
115
+ });
116
+ }
117
+ },
118
+ }),
41
119
  // Push order to QLS button on order detail page
42
120
  addActionBarDropdownMenuItem({
43
121
  id: 'qls-fulfillment-push-order',
@@ -104,4 +182,47 @@ export default [
104
182
  });
105
183
  },
106
184
  }),
185
+ addActionBarDropdownMenuItem({
186
+ id: 'qls-fulfillment-add-ean',
187
+ label: 'Add additional EAN to QLS',
188
+ locationId: 'product-variant-detail',
189
+ icon: 'resistor',
190
+ requiresPermission: ['QLSFullSync'],
191
+ onClick: (_, { route, dataService, notificationService }) => {
192
+ const ean = window.prompt('Enter additional EAN to add to QLS:');
193
+ if (!ean) {
194
+ return;
195
+ }
196
+ const variantId = route.snapshot.params.id;
197
+ dataService
198
+ .mutate(
199
+ gql`
200
+ mutation AddAdditionalEANSToQLS(
201
+ $variantId: ID!
202
+ $additionalEANS: [String!]!
203
+ ) {
204
+ addAdditionalEANSToQLS(
205
+ variantId: $variantId
206
+ additionalEANS: $additionalEANS
207
+ )
208
+ }
209
+ `,
210
+ { variantId, additionalEANS: [ean] }
211
+ )
212
+ .subscribe({
213
+ next: () => {
214
+ notificationService.notify({
215
+ message: `Added EAN ${ean} to QLS`,
216
+ type: 'success',
217
+ });
218
+ },
219
+ error: (err) => {
220
+ notificationService.notify({
221
+ message: err.message,
222
+ type: 'error',
223
+ });
224
+ },
225
+ });
226
+ },
227
+ }),
107
228
  ];
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.2.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
- });