@pinelab/vendure-plugin-qls-fulfillment 1.0.0-beta.10 → 1.0.0-beta.12

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/README.md CHANGED
@@ -51,6 +51,33 @@ This plugin only uses Vendure's default stock location, that means you should ei
51
51
 
52
52
  Vendure assumes the first created stock location is the default stock location.
53
53
 
54
+ ## Service Points
55
+
56
+ You can use the query `qlsServicePoints(postalCode: String!): [QlsServicePoint!]!` to get the service points for a given postal code. You can use the `setOrderCustomFields` mutation to set the service point on an order.
57
+
58
+ ```graphql
59
+ mutation {
60
+ setOrderCustomFields(
61
+ input: {
62
+ customFields: {
63
+ qlsServicePointId: "12232" # This is the ID of one of the points returned by the query above
64
+ qlsServicePointDetails: "Some details about the service point for admin users" # This is just for admin users in Vendure
65
+ }
66
+ }
67
+ ) {
68
+ __typename
69
+ ... on Order {
70
+ id
71
+ code
72
+ customFields {
73
+ qlsServicePointId
74
+ qlsServicePointDetails
75
+ }
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
54
81
  ## Monitoring
55
82
 
56
83
  Make sure to monitor failed jobs: A job that failed after its retries were exhausted, means:
@@ -1 +1,2 @@
1
1
  export declare const adminApiExtensions: import("graphql").DocumentNode;
2
+ export declare const shopApiExtensions: import("graphql").DocumentNode;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.adminApiExtensions = void 0;
6
+ exports.shopApiExtensions = exports.adminApiExtensions = void 0;
7
7
  const graphql_tag_1 = __importDefault(require("graphql-tag"));
8
8
  exports.adminApiExtensions = (0, graphql_tag_1.default) `
9
9
  extend type Mutation {
@@ -18,3 +18,47 @@ exports.adminApiExtensions = (0, graphql_tag_1.default) `
18
18
  pushOrderToQls(orderId: ID!): String!
19
19
  }
20
20
  `;
21
+ exports.shopApiExtensions = (0, graphql_tag_1.default) `
22
+ type QlsServicePoint {
23
+ servicepoint_code: String!
24
+ name: String!
25
+ address: QlsServicePointAddress!
26
+ geo: QlsServicePointGeo!
27
+ times: [QlsServicePointTime!]!
28
+ needsPostNumber: Boolean!
29
+ productId: Int!
30
+ productName: String!
31
+ }
32
+
33
+ type QlsServicePointAddress {
34
+ country: String!
35
+ postalcode: String!
36
+ locality: String!
37
+ street: String!
38
+ housenumber: String!
39
+ }
40
+
41
+ type QlsServicePointGeo {
42
+ lat: Float!
43
+ long: Float!
44
+ }
45
+
46
+ type QlsServicePointTime {
47
+ weekday: Int!
48
+ formatted: String!
49
+ from: String!
50
+ to: String!
51
+ }
52
+
53
+ input QlsServicePointSearchInput {
54
+ countryCode: String!
55
+ postalCode: String!
56
+ }
57
+
58
+ extend type Query {
59
+ """
60
+ Get the service points for a given postal code
61
+ """
62
+ qlsServicePoints(input: QlsServicePointSearchInput!): [QlsServicePoint!]!
63
+ }
64
+ `;
@@ -29,3 +29,46 @@ export type Mutation = {
29
29
  export type MutationPushOrderToQlsArgs = {
30
30
  orderId: Scalars['ID'];
31
31
  };
32
+ export type QlsServicePoint = {
33
+ __typename?: 'QlsServicePoint';
34
+ address: QlsServicePointAddress;
35
+ geo: QlsServicePointGeo;
36
+ name: Scalars['String'];
37
+ needsPostNumber: Scalars['Boolean'];
38
+ productId: Scalars['Int'];
39
+ productName: Scalars['String'];
40
+ servicepoint_code: Scalars['String'];
41
+ times: Array<QlsServicePointTime>;
42
+ };
43
+ export type QlsServicePointAddress = {
44
+ __typename?: 'QlsServicePointAddress';
45
+ country: Scalars['String'];
46
+ housenumber: Scalars['String'];
47
+ locality: Scalars['String'];
48
+ postalcode: Scalars['String'];
49
+ street: Scalars['String'];
50
+ };
51
+ export type QlsServicePointGeo = {
52
+ __typename?: 'QlsServicePointGeo';
53
+ lat: Scalars['Float'];
54
+ long: Scalars['Float'];
55
+ };
56
+ export type QlsServicePointSearchInput = {
57
+ countryCode: Scalars['String'];
58
+ postalCode: Scalars['String'];
59
+ };
60
+ export type QlsServicePointTime = {
61
+ __typename?: 'QlsServicePointTime';
62
+ formatted: Scalars['String'];
63
+ from: Scalars['String'];
64
+ to: Scalars['String'];
65
+ weekday: Scalars['Int'];
66
+ };
67
+ export type Query = {
68
+ __typename?: 'Query';
69
+ /** Get the service points for a given postal code */
70
+ qlsServicePoints: Array<QlsServicePoint>;
71
+ };
72
+ export type QueryQlsServicePointsArgs = {
73
+ input: QlsServicePointSearchInput;
74
+ };
@@ -3,9 +3,9 @@ import { QlsProductService } from '../services/qls-product.service';
3
3
  import { MutationPushOrderToQlsArgs } from './generated/graphql';
4
4
  import { QlsOrderService } from '../services/qls-order.service';
5
5
  export declare class QlsAdminResolver {
6
- private qlsService;
6
+ private qlsProductService;
7
7
  private qlsOrderService;
8
- constructor(qlsService: QlsProductService, qlsOrderService: QlsOrderService);
8
+ constructor(qlsProductService: QlsProductService, qlsOrderService: QlsOrderService);
9
9
  triggerQlsProductSync(ctx: RequestContext): Promise<boolean>;
10
10
  pushOrderToQls(ctx: RequestContext, input: MutationPushOrderToQlsArgs): Promise<string>;
11
11
  }
@@ -19,12 +19,12 @@ const qls_product_service_1 = require("../services/qls-product.service");
19
19
  const qls_order_service_1 = require("../services/qls-order.service");
20
20
  const permissions_1 = require("../config/permissions");
21
21
  let QlsAdminResolver = class QlsAdminResolver {
22
- constructor(qlsService, qlsOrderService) {
23
- this.qlsService = qlsService;
22
+ constructor(qlsProductService, qlsOrderService) {
23
+ this.qlsProductService = qlsProductService;
24
24
  this.qlsOrderService = qlsOrderService;
25
25
  }
26
26
  async triggerQlsProductSync(ctx) {
27
- await this.qlsService.triggerFullSync(ctx);
27
+ await this.qlsProductService.triggerFullSync(ctx);
28
28
  return true;
29
29
  }
30
30
  async pushOrderToQls(ctx, input) {
@@ -0,0 +1,8 @@
1
+ import { RequestContext } from '@vendure/core';
2
+ import { QlsOrderService } from '../services/qls-order.service';
3
+ import { QlsServicePoint, QlsServicePointSearchInput } from './generated/graphql';
4
+ export declare class QlsShopResolver {
5
+ private qlsOrderService;
6
+ constructor(qlsOrderService: QlsOrderService);
7
+ qlsServicePoints(ctx: RequestContext, input: QlsServicePointSearchInput): Promise<QlsServicePoint[]>;
8
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.QlsShopResolver = void 0;
16
+ const graphql_1 = require("@nestjs/graphql");
17
+ const core_1 = require("@vendure/core");
18
+ const qls_order_service_1 = require("../services/qls-order.service");
19
+ let QlsShopResolver = class QlsShopResolver {
20
+ constructor(qlsOrderService) {
21
+ this.qlsOrderService = qlsOrderService;
22
+ }
23
+ async qlsServicePoints(ctx, input) {
24
+ return await this.qlsOrderService.getServicePoints(ctx, input);
25
+ }
26
+ };
27
+ exports.QlsShopResolver = QlsShopResolver;
28
+ __decorate([
29
+ (0, graphql_1.Query)(),
30
+ __param(0, (0, core_1.Ctx)()),
31
+ __param(1, (0, graphql_1.Args)('input')),
32
+ __metadata("design:type", Function),
33
+ __metadata("design:paramtypes", [core_1.RequestContext, Object]),
34
+ __metadata("design:returntype", Promise)
35
+ ], QlsShopResolver.prototype, "qlsServicePoints", null);
36
+ exports.QlsShopResolver = QlsShopResolver = __decorate([
37
+ (0, graphql_1.Resolver)(),
38
+ __metadata("design:paramtypes", [qls_order_service_1.QlsOrderService])
39
+ ], QlsShopResolver);
@@ -5,3 +5,4 @@ declare module '@vendure/core' {
5
5
  }
6
6
  }
7
7
  export declare const variantCustomFields: CustomFieldConfig[];
8
+ export declare const orderCustomFields: CustomFieldConfig[];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.variantCustomFields = void 0;
3
+ exports.orderCustomFields = exports.variantCustomFields = void 0;
4
4
  const core_1 = require("@vendure/core");
5
5
  exports.variantCustomFields = [
6
6
  {
@@ -13,3 +13,31 @@ exports.variantCustomFields = [
13
13
  ui: { tab: 'QLS' },
14
14
  },
15
15
  ];
16
+ exports.orderCustomFields = [
17
+ {
18
+ name: 'qlsServicePointId',
19
+ type: 'string',
20
+ label: [{ value: 'QLS Service Point ID', languageCode: core_1.LanguageCode.en }],
21
+ nullable: true,
22
+ public: true,
23
+ readonly: false,
24
+ ui: { tab: 'QLS' },
25
+ },
26
+ {
27
+ name: 'qlsServicePointDetails',
28
+ type: 'string',
29
+ label: [
30
+ { value: 'QLS Service Point Details', languageCode: core_1.LanguageCode.en },
31
+ ],
32
+ description: [
33
+ {
34
+ value: 'Only used for display purposes.',
35
+ languageCode: core_1.LanguageCode.en,
36
+ },
37
+ ],
38
+ nullable: true,
39
+ public: true,
40
+ readonly: false,
41
+ ui: { tab: 'QLS' },
42
+ },
43
+ ];
@@ -1,6 +1,7 @@
1
1
  import { RequestContext } from '@vendure/core';
2
2
  import { QlsClientConfig, QlsPluginOptions } from '../types';
3
3
  import type { FulfillmentOrder, FulfillmentOrderInput, FulfillmentProduct, FulfillmentProductInput, QlsApiResponse } from './client-types';
4
+ import { QlsServicePoint } from '../api/generated/graphql';
4
5
  export declare function getQlsClient(ctx: RequestContext, pluginOptions: QlsPluginOptions): Promise<QlsClient | undefined>;
5
6
  /**
6
7
  * Wrapper around the QLS Rest API.
@@ -30,5 +31,6 @@ export declare class QlsClient {
30
31
  * Add an extra barcode to a fulfillment product in QLS
31
32
  */
32
33
  removeBarcode(productId: string, barcodeId: number): Promise<void>;
33
- rawRequest<T>(method: 'POST' | 'GET' | 'PUT' | 'DELETE', action: string, data?: unknown): Promise<QlsApiResponse<T>>;
34
+ getServicePoints(countryCode: string, postalCode: string): Promise<QlsServicePoint[]>;
35
+ rawRequest<T>(method: 'POST' | 'GET' | 'PUT' | 'DELETE', action: string, data?: unknown): Promise<QlsApiResponse<T | undefined>>;
34
36
  }
@@ -26,7 +26,7 @@ class QlsClient {
26
26
  */
27
27
  async getFulfillmentProductBySku(sku) {
28
28
  const result = await this.rawRequest('GET', `fulfillment/products?filter%5Bsku%5D=${encodeURIComponent(sku)}`);
29
- if (result.data.length === 0) {
29
+ if (!result.data || result.data.length === 0) {
30
30
  return undefined;
31
31
  }
32
32
  if (result.data.length > 1) {
@@ -57,10 +57,16 @@ class QlsClient {
57
57
  }
58
58
  async createFulfillmentProduct(data) {
59
59
  const response = await this.rawRequest('POST', 'fulfillment/products', data);
60
+ if (!response.data) {
61
+ throw new Error('Failed to create fulfillment product. Got empty response.');
62
+ }
60
63
  return response.data;
61
64
  }
62
65
  async updateFulfillmentProduct(fulfillmentProductId, data) {
63
66
  const response = await this.rawRequest('PUT', `fulfillment/products/${fulfillmentProductId}`, data);
67
+ if (!response.data) {
68
+ throw new Error('Failed to update fulfillment product. Got empty response.');
69
+ }
64
70
  return response.data;
65
71
  }
66
72
  async createFulfillmentOrder(data) {
@@ -68,6 +74,9 @@ class QlsClient {
68
74
  ...data,
69
75
  brand_id: this.config.brandId,
70
76
  });
77
+ if (!response.data) {
78
+ throw new Error('Failed to create fulfillment order. Got empty response.');
79
+ }
71
80
  return response.data;
72
81
  }
73
82
  /**
@@ -84,6 +93,11 @@ class QlsClient {
84
93
  async removeBarcode(productId, barcodeId) {
85
94
  await this.rawRequest('DELETE', `fulfillment/products/${productId}/barcodes/${barcodeId}`);
86
95
  }
96
+ async getServicePoints(countryCode, postalCode) {
97
+ countryCode = countryCode.toUpperCase();
98
+ const result = await this.rawRequest('GET', `service-points/${countryCode}/${postalCode}`);
99
+ return result.data ?? [];
100
+ }
87
101
  async rawRequest(method, action, data) {
88
102
  // Set headers
89
103
  const headers = {
@@ -98,6 +112,15 @@ class QlsClient {
98
112
  headers,
99
113
  body,
100
114
  });
115
+ if (response.status === 204) {
116
+ // 204 No Content
117
+ return {
118
+ data: undefined,
119
+ meta: {
120
+ code: response.status,
121
+ },
122
+ };
123
+ }
101
124
  if (!response.ok) {
102
125
  const errorText = await response.text();
103
126
  // Log error including the request body
@@ -15,6 +15,7 @@ const core_1 = require("@vendure/core");
15
15
  const path_1 = __importDefault(require("path"));
16
16
  const api_extensions_1 = require("./api/api-extensions");
17
17
  const qls_admin_resolver_1 = require("./api/qls-admin.resolver");
18
+ const qls_shop_resolver_1 = require("./api/qls-shop.resolver");
18
19
  const qls_webhooks_controller_1 = require("./api/qls-webhooks-controller");
19
20
  const permissions_1 = require("./config/permissions");
20
21
  const constants_1 = require("./constants");
@@ -49,6 +50,7 @@ exports.QlsPlugin = QlsPlugin = QlsPlugin_1 = __decorate([
49
50
  config.authOptions.customPermissions.push(permissions_1.qlsFullSyncPermission);
50
51
  config.authOptions.customPermissions.push(permissions_1.qlsPushOrderPermission);
51
52
  config.customFields.ProductVariant.push(...custom_fields_1.variantCustomFields);
53
+ config.customFields.Order.push(...custom_fields_1.orderCustomFields);
52
54
  return config;
53
55
  },
54
56
  compatibility: '>=3.2.0',
@@ -56,5 +58,9 @@ exports.QlsPlugin = QlsPlugin = QlsPlugin_1 = __decorate([
56
58
  schema: api_extensions_1.adminApiExtensions,
57
59
  resolvers: [qls_admin_resolver_1.QlsAdminResolver],
58
60
  },
61
+ shopApiExtensions: {
62
+ schema: api_extensions_1.shopApiExtensions,
63
+ resolvers: [qls_shop_resolver_1.QlsShopResolver],
64
+ },
59
65
  })
60
66
  ], QlsPlugin);
@@ -3,6 +3,7 @@ import { ModuleRef } from '@nestjs/core';
3
3
  import { EventBus, ID, Job, JobQueueService, OrderService, RequestContext, TransactionalConnection } from '@vendure/core';
4
4
  import { IncomingOrderWebhook } from '../lib/client-types';
5
5
  import { QlsOrderJobData, QlsPluginOptions } from '../types';
6
+ import { QlsServicePoint, QlsServicePointSearchInput } from '../api/generated/graphql';
6
7
  export declare class QlsOrderService implements OnModuleInit, OnApplicationBootstrap {
7
8
  private connection;
8
9
  private options;
@@ -25,5 +26,6 @@ export declare class QlsOrderService implements OnModuleInit, OnApplicationBoots
25
26
  */
26
27
  handleOrderStatusUpdate(ctx: RequestContext, body: IncomingOrderWebhook): Promise<void>;
27
28
  triggerPushOrder(ctx: RequestContext, orderId: ID, orderCode?: string): Promise<Job<QlsOrderJobData> | undefined>;
29
+ getServicePoints(ctx: RequestContext, input: QlsServicePointSearchInput): Promise<QlsServicePoint[]>;
28
30
  private getVendureOrderState;
29
31
  }
@@ -133,6 +133,7 @@ let QlsOrderService = class QlsOrderService {
133
133
  ...(additionalOrderFields ?? {}),
134
134
  };
135
135
  const result = await client.createFulfillmentOrder(qlsOrder);
136
+ core_2.Logger.info(`Successfully created order '${order.code}' in QLS with id '${result.id}'`, constants_1.loggerCtx);
136
137
  await this.orderService.addNoteToOrder(ctx, {
137
138
  id: orderId,
138
139
  isPublic: false,
@@ -184,6 +185,13 @@ let QlsOrderService = class QlsOrderService {
184
185
  orderId,
185
186
  }, { retries: 5 });
186
187
  }
188
+ async getServicePoints(ctx, input) {
189
+ const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
190
+ if (!client) {
191
+ throw new core_2.UserInputError(`QLS not enabled for channel ${ctx.channel.token}`);
192
+ }
193
+ return await client.getServicePoints(input.countryCode, input.postalCode);
194
+ }
187
195
  getVendureOrderState(body) {
188
196
  if (body.cancelled) {
189
197
  return 'Cancelled';
@@ -89,6 +89,8 @@ let QlsProductService = class QlsProductService {
89
89
  * 4. Updates products in QLS if needed
90
90
  */
91
91
  async runFullSync(ctx) {
92
+ // Wait for 700ms to avoid rate limit of 500/5 minutes
93
+ const waitToPreventRateLimit = () => new Promise((resolve) => setTimeout(resolve, 700));
92
94
  try {
93
95
  const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
94
96
  if (!client) {
@@ -123,14 +125,15 @@ let QlsProductService = class QlsProductService {
123
125
  updatedQlsProductsCount += 1;
124
126
  }
125
127
  if (result === 'created' || result === 'updated') {
126
- // Wait for 700ms to avoid rate limit of 500/5 minutes, but only if we created or updated a product, otherwise no calls have been made yet.
127
- await new Promise((resolve) => setTimeout(resolve, 700));
128
+ // Wait only if we created or updated a product, otherwise no calls have been made yet.
129
+ await waitToPreventRateLimit();
128
130
  }
129
131
  }
130
132
  catch (e) {
131
133
  const error = (0, catch_unknown_1.asError)(e);
132
134
  core_1.Logger.error(`Error creating or updating variant '${variant.sku}' in QLS: ${error.message}`, constants_1.loggerCtx, error.stack);
133
135
  failedCount += 1;
136
+ await waitToPreventRateLimit();
134
137
  }
135
138
  }
136
139
  core_1.Logger.info(`Created ${createdQlsProductsCount} products in QLS`, constants_1.loggerCtx);
@@ -295,7 +298,9 @@ let QlsProductService = class QlsProductService {
295
298
  existingEans: existingAdditionalEANs,
296
299
  desiredEans: additionalEANs,
297
300
  });
298
- if (!eansToUpdate || eansToUpdate.eansToAdd.length === 0 && eansToUpdate.eansToRemove.length === 0) {
301
+ if (!eansToUpdate ||
302
+ (eansToUpdate.eansToAdd.length === 0 &&
303
+ eansToUpdate.eansToRemove.length === 0)) {
299
304
  // No updates needed
300
305
  return false;
301
306
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pinelab/vendure-plugin-qls-fulfillment",
3
- "version": "1.0.0-beta.10",
3
+ "version": "1.0.0-beta.12",
4
4
  "description": "Vendure plugin to fulfill orders via QLS.",
5
5
  "keywords": [
6
6
  "fulfillment",