@pinelab/vendure-plugin-qls-fulfillment 1.0.0-beta.8 → 1.0.1
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 +27 -0
- package/dist/api/api-extensions.d.ts +1 -0
- package/dist/api/api-extensions.js +45 -1
- package/dist/api/generated/graphql.d.ts +43 -0
- package/dist/api/qls-admin.resolver.d.ts +2 -2
- package/dist/api/qls-admin.resolver.js +3 -3
- package/dist/api/qls-shop.resolver.d.ts +8 -0
- package/dist/api/qls-shop.resolver.js +39 -0
- package/dist/api/qls-webhooks-controller.js +2 -1
- package/dist/custom-fields.d.ts +6 -0
- package/dist/custom-fields.js +51 -1
- package/dist/lib/qls-client.d.ts +5 -2
- package/dist/lib/qls-client.js +26 -4
- package/dist/qls-plugin.js +11 -1
- package/dist/services/qls-order.service.d.ts +8 -3
- package/dist/services/qls-order.service.js +50 -14
- package/dist/services/qls-product.service.d.ts +15 -8
- package/dist/services/qls-product.service.js +77 -35
- package/dist/types.d.ts +26 -4
- package/package.json +3 -2
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:
|
|
@@ -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
|
|
6
|
+
private qlsProductService;
|
|
7
7
|
private qlsOrderService;
|
|
8
|
-
constructor(
|
|
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(
|
|
23
|
-
this.
|
|
22
|
+
constructor(qlsProductService, qlsOrderService) {
|
|
23
|
+
this.qlsProductService = qlsProductService;
|
|
24
24
|
this.qlsOrderService = qlsOrderService;
|
|
25
25
|
}
|
|
26
26
|
async triggerQlsProductSync(ctx) {
|
|
27
|
-
await this.
|
|
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);
|
|
@@ -31,7 +31,8 @@ let QlsWebhooksController = class QlsWebhooksController {
|
|
|
31
31
|
*/
|
|
32
32
|
async events(channelToken, webhookSecret, request, body) {
|
|
33
33
|
if (webhookSecret !== this.options.webhookSecret) {
|
|
34
|
-
|
|
34
|
+
core_1.Logger.warn(`Incoming webhook with invalid secret for channel '${channelToken}' to '${request.url}'`, constants_1.loggerCtx);
|
|
35
|
+
throw new core_1.ForbiddenError();
|
|
35
36
|
}
|
|
36
37
|
try {
|
|
37
38
|
const ctx = await this.getCtxForChannel(channelToken);
|
package/dist/custom-fields.d.ts
CHANGED
|
@@ -3,5 +3,11 @@ declare module '@vendure/core' {
|
|
|
3
3
|
interface CustomProductVariantFields {
|
|
4
4
|
qlsProductId?: string;
|
|
5
5
|
}
|
|
6
|
+
interface CustomOrderFields {
|
|
7
|
+
qlsServicePointId?: string;
|
|
8
|
+
qlsServicePointDetails?: string;
|
|
9
|
+
syncedToQls?: boolean;
|
|
10
|
+
}
|
|
6
11
|
}
|
|
7
12
|
export declare const variantCustomFields: CustomFieldConfig[];
|
|
13
|
+
export declare const orderCustomFields: CustomFieldConfig[];
|
package/dist/custom-fields.js
CHANGED
|
@@ -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,53 @@ 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: 'syncedToQls',
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
label: [
|
|
30
|
+
{ value: 'Created in QLS', languageCode: core_1.LanguageCode.en },
|
|
31
|
+
{ value: 'Aangemaakt in QLS', languageCode: core_1.LanguageCode.nl },
|
|
32
|
+
],
|
|
33
|
+
description: [
|
|
34
|
+
{
|
|
35
|
+
value: 'Uncheck this to be able to push the order to QLS again',
|
|
36
|
+
languageCode: core_1.LanguageCode.en,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
value: 'Vink dit uit om de order opnieuw naar QLS te sturen',
|
|
40
|
+
languageCode: core_1.LanguageCode.nl,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
nullable: true,
|
|
44
|
+
public: false,
|
|
45
|
+
readonly: false,
|
|
46
|
+
ui: { tab: 'QLS' },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'qlsServicePointDetails',
|
|
50
|
+
type: 'string',
|
|
51
|
+
label: [
|
|
52
|
+
{ value: 'QLS Service Point Details', languageCode: core_1.LanguageCode.en },
|
|
53
|
+
],
|
|
54
|
+
description: [
|
|
55
|
+
{
|
|
56
|
+
value: 'Only used for display purposes.',
|
|
57
|
+
languageCode: core_1.LanguageCode.en,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
nullable: true,
|
|
61
|
+
public: true,
|
|
62
|
+
readonly: false,
|
|
63
|
+
ui: { tab: 'QLS' },
|
|
64
|
+
},
|
|
65
|
+
];
|
package/dist/lib/qls-client.d.ts
CHANGED
|
@@ -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.
|
|
@@ -20,7 +21,8 @@ export declare class QlsClient {
|
|
|
20
21
|
*/
|
|
21
22
|
getAllFulfillmentProducts(): Promise<FulfillmentProduct[]>;
|
|
22
23
|
createFulfillmentProduct(data: FulfillmentProductInput): Promise<FulfillmentProduct>;
|
|
23
|
-
updateFulfillmentProduct(fulfillmentProductId: string, data: FulfillmentProductInput): Promise<
|
|
24
|
+
updateFulfillmentProduct(fulfillmentProductId: string, data: FulfillmentProductInput): Promise<void>;
|
|
25
|
+
deleteFulfillmentProduct(fulfillmentProductId: string): Promise<void>;
|
|
24
26
|
createFulfillmentOrder(data: Omit<FulfillmentOrderInput, 'brand_id'>): Promise<FulfillmentOrder>;
|
|
25
27
|
/**
|
|
26
28
|
* Add an extra barcode to a fulfillment product in QLS
|
|
@@ -30,5 +32,6 @@ export declare class QlsClient {
|
|
|
30
32
|
* Add an extra barcode to a fulfillment product in QLS
|
|
31
33
|
*/
|
|
32
34
|
removeBarcode(productId: string, barcodeId: number): Promise<void>;
|
|
33
|
-
|
|
35
|
+
getServicePoints(countryCode: string, postalCode: string): Promise<QlsServicePoint[]>;
|
|
36
|
+
rawRequest<T>(method: 'POST' | 'GET' | 'PUT' | 'DELETE', action: string, data?: unknown): Promise<QlsApiResponse<T | undefined>>;
|
|
34
37
|
}
|
package/dist/lib/qls-client.js
CHANGED
|
@@ -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,17 +57,25 @@ 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
|
-
|
|
64
|
-
|
|
66
|
+
await this.rawRequest('PUT', `fulfillment/products/${fulfillmentProductId}`, data);
|
|
67
|
+
}
|
|
68
|
+
async deleteFulfillmentProduct(fulfillmentProductId) {
|
|
69
|
+
await this.rawRequest('DELETE', `fulfillment/products/${fulfillmentProductId}`);
|
|
65
70
|
}
|
|
66
71
|
async createFulfillmentOrder(data) {
|
|
67
72
|
const response = await this.rawRequest('POST', 'fulfillment/orders', {
|
|
68
73
|
...data,
|
|
69
74
|
brand_id: this.config.brandId,
|
|
70
75
|
});
|
|
76
|
+
if (!response.data) {
|
|
77
|
+
throw new Error('Failed to create fulfillment order. Got empty response.');
|
|
78
|
+
}
|
|
71
79
|
return response.data;
|
|
72
80
|
}
|
|
73
81
|
/**
|
|
@@ -84,6 +92,11 @@ class QlsClient {
|
|
|
84
92
|
async removeBarcode(productId, barcodeId) {
|
|
85
93
|
await this.rawRequest('DELETE', `fulfillment/products/${productId}/barcodes/${barcodeId}`);
|
|
86
94
|
}
|
|
95
|
+
async getServicePoints(countryCode, postalCode) {
|
|
96
|
+
countryCode = countryCode.toUpperCase();
|
|
97
|
+
const result = await this.rawRequest('GET', `service-points/${countryCode}/${postalCode}`);
|
|
98
|
+
return result.data ?? [];
|
|
99
|
+
}
|
|
87
100
|
async rawRequest(method, action, data) {
|
|
88
101
|
// Set headers
|
|
89
102
|
const headers = {
|
|
@@ -98,10 +111,19 @@ class QlsClient {
|
|
|
98
111
|
headers,
|
|
99
112
|
body,
|
|
100
113
|
});
|
|
114
|
+
if (response.status === 204) {
|
|
115
|
+
// 204 No Content
|
|
116
|
+
return {
|
|
117
|
+
data: undefined,
|
|
118
|
+
meta: {
|
|
119
|
+
code: response.status,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
101
123
|
if (!response.ok) {
|
|
102
124
|
const errorText = await response.text();
|
|
103
125
|
// Log error including the request body
|
|
104
|
-
core_1.Logger.error(`QLS request to '${url}' failed: ${response.status} ${response.statusText} - ${errorText}`, constants_1.loggerCtx, data ? JSON.stringify(data, null, 2) : undefined);
|
|
126
|
+
core_1.Logger.error(`[QLS] '${method}' request to '${url}' failed: ${response.status} ${response.statusText} - ${errorText}`, constants_1.loggerCtx, data ? JSON.stringify(data, null, 2) : undefined);
|
|
105
127
|
throw new Error(errorText);
|
|
106
128
|
}
|
|
107
129
|
const contentType = response.headers.get('content-type') ?? '';
|
package/dist/qls-plugin.js
CHANGED
|
@@ -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");
|
|
@@ -23,7 +24,11 @@ const qls_order_service_1 = require("./services/qls-order.service");
|
|
|
23
24
|
const qls_product_service_1 = require("./services/qls-product.service");
|
|
24
25
|
let QlsPlugin = QlsPlugin_1 = class QlsPlugin {
|
|
25
26
|
static init(options) {
|
|
26
|
-
this.options =
|
|
27
|
+
this.options = {
|
|
28
|
+
synchronizeStockLevels: true,
|
|
29
|
+
autoPushOrders: true,
|
|
30
|
+
...options,
|
|
31
|
+
};
|
|
27
32
|
return QlsPlugin_1;
|
|
28
33
|
}
|
|
29
34
|
};
|
|
@@ -49,6 +54,7 @@ exports.QlsPlugin = QlsPlugin = QlsPlugin_1 = __decorate([
|
|
|
49
54
|
config.authOptions.customPermissions.push(permissions_1.qlsFullSyncPermission);
|
|
50
55
|
config.authOptions.customPermissions.push(permissions_1.qlsPushOrderPermission);
|
|
51
56
|
config.customFields.ProductVariant.push(...custom_fields_1.variantCustomFields);
|
|
57
|
+
config.customFields.Order.push(...custom_fields_1.orderCustomFields);
|
|
52
58
|
return config;
|
|
53
59
|
},
|
|
54
60
|
compatibility: '>=3.2.0',
|
|
@@ -56,5 +62,9 @@ exports.QlsPlugin = QlsPlugin = QlsPlugin_1 = __decorate([
|
|
|
56
62
|
schema: api_extensions_1.adminApiExtensions,
|
|
57
63
|
resolvers: [qls_admin_resolver_1.QlsAdminResolver],
|
|
58
64
|
},
|
|
65
|
+
shopApiExtensions: {
|
|
66
|
+
schema: api_extensions_1.shopApiExtensions,
|
|
67
|
+
resolvers: [qls_shop_resolver_1.QlsShopResolver],
|
|
68
|
+
},
|
|
59
69
|
})
|
|
60
70
|
], QlsPlugin);
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { OnApplicationBootstrap, OnModuleInit } from '@nestjs/common';
|
|
2
2
|
import { ModuleRef } from '@nestjs/core';
|
|
3
|
-
import { EventBus, ID, Job, JobQueueService, OrderService, RequestContext
|
|
3
|
+
import { EventBus, ID, Job, JobQueueService, OrderService, RequestContext } from '@vendure/core';
|
|
4
|
+
import { QlsServicePoint, QlsServicePointSearchInput } from '../api/generated/graphql';
|
|
4
5
|
import { IncomingOrderWebhook } from '../lib/client-types';
|
|
5
6
|
import { QlsOrderJobData, QlsPluginOptions } from '../types';
|
|
6
7
|
export declare class QlsOrderService implements OnModuleInit, OnApplicationBootstrap {
|
|
7
|
-
private connection;
|
|
8
8
|
private options;
|
|
9
9
|
private jobQueueService;
|
|
10
10
|
private eventBus;
|
|
11
11
|
private orderService;
|
|
12
12
|
private moduleRef;
|
|
13
13
|
private orderJobQueue;
|
|
14
|
-
constructor(
|
|
14
|
+
constructor(options: QlsPluginOptions, jobQueueService: JobQueueService, eventBus: EventBus, orderService: OrderService, moduleRef: ModuleRef);
|
|
15
15
|
onApplicationBootstrap(): void;
|
|
16
16
|
onModuleInit(): Promise<void>;
|
|
17
17
|
/**
|
|
@@ -19,11 +19,16 @@ export declare class QlsOrderService implements OnModuleInit, OnApplicationBoots
|
|
|
19
19
|
* Returns the result of the job, which will be stored in the job record.
|
|
20
20
|
*/
|
|
21
21
|
handleOrderJob(job: Job<QlsOrderJobData>): Promise<unknown>;
|
|
22
|
+
/**
|
|
23
|
+
* Push an order to QLS by id.
|
|
24
|
+
* Returns a human-readable message describing the result of the operation (Used as job result).
|
|
25
|
+
*/
|
|
22
26
|
pushOrderToQls(ctx: RequestContext, orderId: ID): Promise<string>;
|
|
23
27
|
/**
|
|
24
28
|
* Update the status of an order in QLS based on the given order code and status
|
|
25
29
|
*/
|
|
26
30
|
handleOrderStatusUpdate(ctx: RequestContext, body: IncomingOrderWebhook): Promise<void>;
|
|
27
31
|
triggerPushOrder(ctx: RequestContext, orderId: ID, orderCode?: string): Promise<Job<QlsOrderJobData> | undefined>;
|
|
32
|
+
getServicePoints(ctx: RequestContext, input: QlsServicePointSearchInput): Promise<QlsServicePoint[]>;
|
|
28
33
|
private getVendureOrderState;
|
|
29
34
|
}
|
|
@@ -24,8 +24,7 @@ const util_1 = __importDefault(require("util"));
|
|
|
24
24
|
const constants_1 = require("../constants");
|
|
25
25
|
const qls_client_1 = require("../lib/qls-client");
|
|
26
26
|
let QlsOrderService = class QlsOrderService {
|
|
27
|
-
constructor(
|
|
28
|
-
this.connection = connection;
|
|
27
|
+
constructor(options, jobQueueService, eventBus, orderService, moduleRef) {
|
|
29
28
|
this.options = options;
|
|
30
29
|
this.jobQueueService = jobQueueService;
|
|
31
30
|
this.eventBus = eventBus;
|
|
@@ -35,6 +34,10 @@ let QlsOrderService = class QlsOrderService {
|
|
|
35
34
|
onApplicationBootstrap() {
|
|
36
35
|
// Listen for OrderPlacedEvent and add a job to the queue
|
|
37
36
|
this.eventBus.ofType(core_2.OrderPlacedEvent).subscribe((event) => {
|
|
37
|
+
if (!this.options.autoPushOrders) {
|
|
38
|
+
core_2.Logger.info(`Auto push orders disabled, not triggering push order job for order ${event.order.code}`, constants_1.loggerCtx);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
38
41
|
this.triggerPushOrder(event.ctx, event.order.id, event.order.code).catch((e) => {
|
|
39
42
|
const error = (0, catch_unknown_1.asError)(e);
|
|
40
43
|
core_2.Logger.error(`Failed to trigger push order job for order ${event.order.code}: ${error.message}`, constants_1.loggerCtx, error.stack);
|
|
@@ -72,6 +75,10 @@ let QlsOrderService = class QlsOrderService {
|
|
|
72
75
|
throw error;
|
|
73
76
|
}
|
|
74
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Push an order to QLS by id.
|
|
80
|
+
* Returns a human-readable message describing the result of the operation (Used as job result).
|
|
81
|
+
*/
|
|
75
82
|
async pushOrderToQls(ctx, orderId) {
|
|
76
83
|
const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
|
|
77
84
|
if (!client) {
|
|
@@ -82,18 +89,33 @@ let QlsOrderService = class QlsOrderService {
|
|
|
82
89
|
if (!order) {
|
|
83
90
|
throw new Error(`No order with id ${orderId} not found`);
|
|
84
91
|
}
|
|
92
|
+
if (order.customFields.syncedToQls) {
|
|
93
|
+
throw new core_2.UserInputError(`Order '${order.code}' has already been synced to QLS`);
|
|
94
|
+
}
|
|
85
95
|
try {
|
|
86
|
-
//
|
|
87
|
-
const qlsProducts =
|
|
96
|
+
// Map variants to QLS products
|
|
97
|
+
const qlsProducts = [];
|
|
98
|
+
await Promise.all(order.lines.map(async (line) => {
|
|
99
|
+
// Check if product variant should be excluded from sync
|
|
100
|
+
if (await this.options.excludeVariantFromSync?.(ctx, new core_2.Injector(this.moduleRef), line.productVariant)) {
|
|
101
|
+
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);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Check if product is available in QLS
|
|
88
105
|
if (!line.productVariant.customFields.qlsProductId) {
|
|
89
106
|
throw new Error(`Product variant '${line.productVariant.sku}' does not have a QLS product ID set. Unable to push order '${order.code}' to QLS.`);
|
|
90
107
|
}
|
|
91
|
-
|
|
108
|
+
qlsProducts.push({
|
|
92
109
|
amount_ordered: line.quantity,
|
|
93
110
|
product_id: line.productVariant.customFields.qlsProductId,
|
|
94
111
|
name: line.productVariant.name,
|
|
95
|
-
};
|
|
96
|
-
});
|
|
112
|
+
});
|
|
113
|
+
}));
|
|
114
|
+
if (qlsProducts.length === 0) {
|
|
115
|
+
const message = `No products to push to QLS for order '${order.code}'. Ignoring order.`;
|
|
116
|
+
core_2.Logger.info(message, constants_1.loggerCtx);
|
|
117
|
+
return message;
|
|
118
|
+
}
|
|
97
119
|
const additionalOrderFields = await this.options.getAdditionalOrderFields?.(ctx, new core_2.Injector(this.moduleRef), order);
|
|
98
120
|
const customerName = [order.customer?.firstName, order.customer?.lastName]
|
|
99
121
|
.filter(Boolean)
|
|
@@ -112,15 +134,17 @@ let QlsOrderService = class QlsOrderService {
|
|
|
112
134
|
!order.shippingAddress.countryCode) {
|
|
113
135
|
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.`);
|
|
114
136
|
}
|
|
137
|
+
const processable = (await this.options.processOrderFrom?.(ctx, order)) ?? new Date();
|
|
138
|
+
const receiverContact = this.options.getReceiverContact?.(ctx, order);
|
|
115
139
|
const qlsOrder = {
|
|
116
140
|
customer_reference: order.code,
|
|
117
|
-
processable:
|
|
118
|
-
servicepoint_code:
|
|
141
|
+
processable: processable.toISOString(),
|
|
142
|
+
servicepoint_code: order.customFields?.qlsServicePointId,
|
|
119
143
|
delivery_options: additionalOrderFields?.delivery_options ?? [],
|
|
120
144
|
total_price: order.totalWithTax,
|
|
121
|
-
receiver_contact: {
|
|
145
|
+
receiver_contact: receiverContact ?? {
|
|
122
146
|
name: order.shippingAddress.fullName || customerName,
|
|
123
|
-
companyname: order.shippingAddress.company,
|
|
147
|
+
companyname: order.shippingAddress.company ?? '',
|
|
124
148
|
street: order.shippingAddress.streetLine1,
|
|
125
149
|
housenumber: order.shippingAddress.streetLine2,
|
|
126
150
|
postalcode: order.shippingAddress.postalCode,
|
|
@@ -133,6 +157,10 @@ let QlsOrderService = class QlsOrderService {
|
|
|
133
157
|
...(additionalOrderFields ?? {}),
|
|
134
158
|
};
|
|
135
159
|
const result = await client.createFulfillmentOrder(qlsOrder);
|
|
160
|
+
await this.orderService.updateCustomFields(ctx, orderId, {
|
|
161
|
+
syncedToQls: true,
|
|
162
|
+
});
|
|
163
|
+
core_2.Logger.info(`Successfully created order '${order.code}' in QLS with id '${result.id}'`, constants_1.loggerCtx);
|
|
136
164
|
await this.orderService.addNoteToOrder(ctx, {
|
|
137
165
|
id: orderId,
|
|
138
166
|
isPublic: false,
|
|
@@ -154,6 +182,7 @@ let QlsOrderService = class QlsOrderService {
|
|
|
154
182
|
* Update the status of an order in QLS based on the given order code and status
|
|
155
183
|
*/
|
|
156
184
|
async handleOrderStatusUpdate(ctx, body) {
|
|
185
|
+
core_2.Logger.info(`Handling QLS order status update for order '${body.customer_reference}' with status '${body.status} and amount_delivered '${body.amount_delivered}' and amount_total '${body.amount_total}'`, constants_1.loggerCtx);
|
|
157
186
|
const orderCode = body.customer_reference;
|
|
158
187
|
const order = await this.orderService.findOneByCode(ctx, orderCode, []);
|
|
159
188
|
if (!order) {
|
|
@@ -182,7 +211,14 @@ let QlsOrderService = class QlsOrderService {
|
|
|
182
211
|
action: 'push-order',
|
|
183
212
|
ctx: ctx.serialize(),
|
|
184
213
|
orderId,
|
|
185
|
-
}, { retries:
|
|
214
|
+
}, { retries: 3 });
|
|
215
|
+
}
|
|
216
|
+
async getServicePoints(ctx, input) {
|
|
217
|
+
const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
|
|
218
|
+
if (!client) {
|
|
219
|
+
throw new core_2.UserInputError(`QLS not enabled for channel ${ctx.channel.token}`);
|
|
220
|
+
}
|
|
221
|
+
return await client.getServicePoints(input.countryCode, input.postalCode);
|
|
186
222
|
}
|
|
187
223
|
getVendureOrderState(body) {
|
|
188
224
|
if (body.cancelled) {
|
|
@@ -202,8 +238,8 @@ let QlsOrderService = class QlsOrderService {
|
|
|
202
238
|
exports.QlsOrderService = QlsOrderService;
|
|
203
239
|
exports.QlsOrderService = QlsOrderService = __decorate([
|
|
204
240
|
(0, common_1.Injectable)(),
|
|
205
|
-
__param(
|
|
206
|
-
__metadata("design:paramtypes", [
|
|
241
|
+
__param(0, (0, common_1.Inject)(constants_1.PLUGIN_INIT_OPTIONS)),
|
|
242
|
+
__metadata("design:paramtypes", [Object, core_2.JobQueueService,
|
|
207
243
|
core_2.EventBus,
|
|
208
244
|
core_2.OrderService,
|
|
209
245
|
core_1.ModuleRef])
|
|
@@ -3,11 +3,12 @@ import { EventBus, ID, Job, JobQueueService, ListQueryBuilder, ProductPriceAppli
|
|
|
3
3
|
import { FulfillmentProduct } from '../lib/client-types';
|
|
4
4
|
import { QlsClient } from '../lib/qls-client';
|
|
5
5
|
import { QlsPluginOptions, QlsProductJobData } from '../types';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
import { ModuleRef } from '@nestjs/core';
|
|
7
|
+
type SyncProductsResult = {
|
|
8
|
+
updatedInQls: Partial<ProductVariant>[];
|
|
9
|
+
createdInQls: Partial<ProductVariant>[];
|
|
10
|
+
updatedStock: Partial<ProductVariant>[];
|
|
11
|
+
failed: Partial<ProductVariant>[];
|
|
11
12
|
};
|
|
12
13
|
export declare class QlsProductService implements OnModuleInit, OnApplicationBootstrap {
|
|
13
14
|
private connection;
|
|
@@ -19,8 +20,9 @@ export declare class QlsProductService implements OnModuleInit, OnApplicationBoo
|
|
|
19
20
|
private readonly eventBus;
|
|
20
21
|
private readonly listQueryBuilder;
|
|
21
22
|
private readonly productPriceApplicator;
|
|
23
|
+
private readonly moduleRef;
|
|
22
24
|
private productJobQueue;
|
|
23
|
-
constructor(connection: TransactionalConnection, options: QlsPluginOptions, jobQueueService: JobQueueService, stockLevelService: StockLevelService, variantService: ProductVariantService, stockLocationService: StockLocationService, eventBus: EventBus, listQueryBuilder: ListQueryBuilder, productPriceApplicator: ProductPriceApplicator);
|
|
25
|
+
constructor(connection: TransactionalConnection, options: QlsPluginOptions, jobQueueService: JobQueueService, stockLevelService: StockLevelService, variantService: ProductVariantService, stockLocationService: StockLocationService, eventBus: EventBus, listQueryBuilder: ListQueryBuilder, productPriceApplicator: ProductPriceApplicator, moduleRef: ModuleRef);
|
|
24
26
|
onApplicationBootstrap(): void;
|
|
25
27
|
onModuleInit(): Promise<void>;
|
|
26
28
|
/**
|
|
@@ -35,11 +37,16 @@ export declare class QlsProductService implements OnModuleInit, OnApplicationBoo
|
|
|
35
37
|
* 3. Creates products in QLS if needed
|
|
36
38
|
* 4. Updates products in QLS if needed
|
|
37
39
|
*/
|
|
38
|
-
runFullSync(ctx: RequestContext): Promise<
|
|
40
|
+
runFullSync(ctx: RequestContext): Promise<SyncProductsResult>;
|
|
41
|
+
/**
|
|
42
|
+
* Utility function to remove all products from QLS.
|
|
43
|
+
* You might need to cancel active orders in QLS first, because products cannot be deleted if they are in an active order.
|
|
44
|
+
*/
|
|
45
|
+
removeAllProductsFromQls(ctx: RequestContext): Promise<void>;
|
|
39
46
|
/**
|
|
40
47
|
* Creates or updates the fulfillment products in QLS for the given product variants.
|
|
41
48
|
*/
|
|
42
|
-
syncVariants(ctx: RequestContext, productVariantIds: ID[]): Promise<
|
|
49
|
+
syncVariants(ctx: RequestContext, productVariantIds: ID[]): Promise<SyncProductsResult>;
|
|
43
50
|
/**
|
|
44
51
|
* Trigger a full product sync job
|
|
45
52
|
*/
|
|
@@ -24,8 +24,11 @@ const util_1 = __importDefault(require("util"));
|
|
|
24
24
|
const constants_1 = require("../constants");
|
|
25
25
|
const qls_client_1 = require("../lib/qls-client");
|
|
26
26
|
const util_2 = require("./util");
|
|
27
|
+
const core_2 = require("@nestjs/core");
|
|
28
|
+
// Wait for 700ms to avoid rate limit of 500/5 minutes
|
|
29
|
+
const waitToPreventRateLimit = () => new Promise((resolve) => setTimeout(resolve, 700));
|
|
27
30
|
let QlsProductService = class QlsProductService {
|
|
28
|
-
constructor(connection, options, jobQueueService, stockLevelService, variantService, stockLocationService, eventBus, listQueryBuilder, productPriceApplicator) {
|
|
31
|
+
constructor(connection, options, jobQueueService, stockLevelService, variantService, stockLocationService, eventBus, listQueryBuilder, productPriceApplicator, moduleRef) {
|
|
29
32
|
this.connection = connection;
|
|
30
33
|
this.options = options;
|
|
31
34
|
this.jobQueueService = jobQueueService;
|
|
@@ -35,6 +38,7 @@ let QlsProductService = class QlsProductService {
|
|
|
35
38
|
this.eventBus = eventBus;
|
|
36
39
|
this.listQueryBuilder = listQueryBuilder;
|
|
37
40
|
this.productPriceApplicator = productPriceApplicator;
|
|
41
|
+
this.moduleRef = moduleRef;
|
|
38
42
|
}
|
|
39
43
|
onApplicationBootstrap() {
|
|
40
44
|
// Listen for ProductVariantEvent and add a job to the queue
|
|
@@ -64,7 +68,13 @@ let QlsProductService = class QlsProductService {
|
|
|
64
68
|
try {
|
|
65
69
|
const ctx = core_1.RequestContext.deserialize(job.data.ctx);
|
|
66
70
|
if (job.data.action === 'full-sync-products') {
|
|
67
|
-
|
|
71
|
+
const result = await this.runFullSync(ctx);
|
|
72
|
+
return {
|
|
73
|
+
updatedInQls: result.updatedInQls.length,
|
|
74
|
+
createdInQls: result.createdInQls.length,
|
|
75
|
+
updatedStock: result.updatedStock.length,
|
|
76
|
+
failed: result.failed.length,
|
|
77
|
+
};
|
|
68
78
|
}
|
|
69
79
|
else if (job.data.action === 'sync-products') {
|
|
70
80
|
return await this.syncVariants(ctx, job.data.productVariantIds);
|
|
@@ -99,45 +109,49 @@ let QlsProductService = class QlsProductService {
|
|
|
99
109
|
const allVariants = await this.getAllVariants(ctx);
|
|
100
110
|
core_1.Logger.info(`Running full sync for ${allQlsProducts.length} QLS products and ${allVariants.length} Vendure variants`, constants_1.loggerCtx);
|
|
101
111
|
// Update stock in Vendure based on QLS products
|
|
102
|
-
|
|
112
|
+
const updatedStock = [];
|
|
103
113
|
for (const variant of allVariants) {
|
|
104
114
|
const qlsProduct = allQlsProducts.find((p) => p.sku == variant.sku);
|
|
105
115
|
if (qlsProduct) {
|
|
106
116
|
await this.updateStock(ctx, variant.id, qlsProduct.amount_available);
|
|
107
|
-
|
|
117
|
+
updatedStock.push(variant);
|
|
108
118
|
}
|
|
109
119
|
}
|
|
110
|
-
core_1.Logger.info(`Updated stock for ${
|
|
120
|
+
core_1.Logger.info(`Updated stock for ${updatedStock.length} variants based on QLS stock levels`, constants_1.loggerCtx);
|
|
111
121
|
// Create or update products in QLS
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
122
|
+
const createdQlsProducts = [];
|
|
123
|
+
const updatedQlsProducts = [];
|
|
124
|
+
const failed = [];
|
|
115
125
|
for (const variant of allVariants) {
|
|
116
126
|
try {
|
|
117
127
|
const existingQlsProduct = allQlsProducts.find((p) => p.sku == variant.sku);
|
|
118
128
|
const result = await this.createOrUpdateProductInQls(ctx, client, variant, existingQlsProduct ?? null);
|
|
119
129
|
if (result === 'created') {
|
|
120
|
-
|
|
130
|
+
createdQlsProducts.push(variant);
|
|
121
131
|
}
|
|
122
132
|
else if (result === 'updated') {
|
|
123
|
-
|
|
133
|
+
updatedQlsProducts.push(variant);
|
|
134
|
+
}
|
|
135
|
+
if (result === 'created' || result === 'updated') {
|
|
136
|
+
// Wait only if we created or updated a product, otherwise no calls have been made yet.
|
|
137
|
+
await waitToPreventRateLimit();
|
|
124
138
|
}
|
|
125
139
|
}
|
|
126
140
|
catch (e) {
|
|
127
141
|
const error = (0, catch_unknown_1.asError)(e);
|
|
128
142
|
core_1.Logger.error(`Error creating or updating variant '${variant.sku}' in QLS: ${error.message}`, constants_1.loggerCtx, error.stack);
|
|
129
|
-
|
|
143
|
+
failed.push(variant);
|
|
144
|
+
await waitToPreventRateLimit();
|
|
130
145
|
}
|
|
131
|
-
await new Promise((resolve) => setTimeout(resolve, 700)); // Avoid rate limit of 500/5 minutes (700ms delay = 85/minute)
|
|
132
146
|
}
|
|
133
|
-
core_1.Logger.info(`Created ${
|
|
134
|
-
core_1.Logger.info(`Updated ${
|
|
135
|
-
core_1.Logger.info(`Finished full sync with ${
|
|
147
|
+
core_1.Logger.info(`Created ${createdQlsProducts.length} products in QLS`, constants_1.loggerCtx);
|
|
148
|
+
core_1.Logger.info(`Updated ${updatedQlsProducts.length} products in QLS`, constants_1.loggerCtx);
|
|
149
|
+
core_1.Logger.info(`Finished full sync with ${failed.length} failures`, constants_1.loggerCtx);
|
|
136
150
|
return {
|
|
137
|
-
updatedInQls:
|
|
138
|
-
createdInQls:
|
|
139
|
-
updatedStock
|
|
140
|
-
failed
|
|
151
|
+
updatedInQls: updatedQlsProducts,
|
|
152
|
+
createdInQls: createdQlsProducts,
|
|
153
|
+
updatedStock,
|
|
154
|
+
failed,
|
|
141
155
|
};
|
|
142
156
|
}
|
|
143
157
|
catch (e) {
|
|
@@ -146,6 +160,24 @@ let QlsProductService = class QlsProductService {
|
|
|
146
160
|
throw error;
|
|
147
161
|
}
|
|
148
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Utility function to remove all products from QLS.
|
|
165
|
+
* You might need to cancel active orders in QLS first, because products cannot be deleted if they are in an active order.
|
|
166
|
+
*/
|
|
167
|
+
async removeAllProductsFromQls(ctx) {
|
|
168
|
+
const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
|
|
169
|
+
if (!client) {
|
|
170
|
+
throw new Error(`QLS not enabled for channel ${ctx.channel.token}`);
|
|
171
|
+
}
|
|
172
|
+
core_1.Logger.warn(`Removing all products from QLS for channel ${ctx.channel.token}...`, constants_1.loggerCtx);
|
|
173
|
+
const allProducts = await client.getAllFulfillmentProducts();
|
|
174
|
+
for (const product of allProducts) {
|
|
175
|
+
await client.deleteFulfillmentProduct(product.id);
|
|
176
|
+
core_1.Logger.info(`Removed product '${product.sku}' (${product.id}) from QLS`, constants_1.loggerCtx);
|
|
177
|
+
await waitToPreventRateLimit();
|
|
178
|
+
}
|
|
179
|
+
core_1.Logger.warn(`Removed ${allProducts.length} products from QLS for channel ${ctx.channel.token}`, constants_1.loggerCtx);
|
|
180
|
+
}
|
|
149
181
|
/**
|
|
150
182
|
* Creates or updates the fulfillment products in QLS for the given product variants.
|
|
151
183
|
*/
|
|
@@ -154,15 +186,15 @@ let QlsProductService = class QlsProductService {
|
|
|
154
186
|
if (!client) {
|
|
155
187
|
core_1.Logger.debug(`QLS not enabled for channel ${ctx.channel.token}. Not handling product update/create.`, constants_1.loggerCtx);
|
|
156
188
|
return {
|
|
157
|
-
updatedInQls:
|
|
158
|
-
createdInQls:
|
|
159
|
-
updatedStock:
|
|
160
|
-
failed:
|
|
189
|
+
updatedInQls: [],
|
|
190
|
+
createdInQls: [],
|
|
191
|
+
updatedStock: [],
|
|
192
|
+
failed: [],
|
|
161
193
|
};
|
|
162
194
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
195
|
+
const updatedInQls = [];
|
|
196
|
+
const createdInQls = [];
|
|
197
|
+
const failed = [];
|
|
166
198
|
for (const variantId of productVariantIds) {
|
|
167
199
|
try {
|
|
168
200
|
const variant = await this.variantService.findOne(ctx, variantId, [
|
|
@@ -178,23 +210,23 @@ let QlsProductService = class QlsProductService {
|
|
|
178
210
|
const existingQlsProduct = await client.getFulfillmentProductBySku(variant.sku);
|
|
179
211
|
const result = await this.createOrUpdateProductInQls(ctx, client, variant, existingQlsProduct ?? null);
|
|
180
212
|
if (result === 'created') {
|
|
181
|
-
createdInQls
|
|
213
|
+
createdInQls.push(variant);
|
|
182
214
|
}
|
|
183
215
|
else if (result === 'updated') {
|
|
184
|
-
updatedInQls
|
|
216
|
+
updatedInQls.push(variant);
|
|
185
217
|
}
|
|
186
218
|
}
|
|
187
219
|
catch (e) {
|
|
188
220
|
const error = (0, catch_unknown_1.asError)(e);
|
|
189
221
|
core_1.Logger.error(`Error syncing variant ${variantId} to QLS: ${error.message}`, constants_1.loggerCtx, error.stack);
|
|
190
|
-
|
|
222
|
+
failed.push({ id: variantId });
|
|
191
223
|
}
|
|
192
224
|
}
|
|
193
225
|
return {
|
|
194
226
|
updatedInQls,
|
|
195
227
|
createdInQls,
|
|
196
|
-
updatedStock:
|
|
197
|
-
failed
|
|
228
|
+
updatedStock: [],
|
|
229
|
+
failed,
|
|
198
230
|
};
|
|
199
231
|
}
|
|
200
232
|
/**
|
|
@@ -219,7 +251,7 @@ let QlsProductService = class QlsProductService {
|
|
|
219
251
|
action: 'sync-products',
|
|
220
252
|
ctx: ctx.serialize(),
|
|
221
253
|
productVariantIds,
|
|
222
|
-
}, { retries:
|
|
254
|
+
}, { retries: 1 });
|
|
223
255
|
}
|
|
224
256
|
/**
|
|
225
257
|
* Update the stock level for a variant based on the given available stock
|
|
@@ -241,6 +273,10 @@ let QlsProductService = class QlsProductService {
|
|
|
241
273
|
* Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
|
|
242
274
|
*/
|
|
243
275
|
async createOrUpdateProductInQls(ctx, client, variant, existingProduct) {
|
|
276
|
+
if (await this.options.excludeVariantFromSync?.(ctx, new core_1.Injector(this.moduleRef), variant)) {
|
|
277
|
+
core_1.Logger.info(`Variant '${variant.sku}' excluded from sync to QLS.`, constants_1.loggerCtx);
|
|
278
|
+
return 'not-changed';
|
|
279
|
+
}
|
|
244
280
|
let qlsProduct = existingProduct;
|
|
245
281
|
let createdOrUpdated = 'not-changed';
|
|
246
282
|
const { additionalEANs, ...additionalVariantFields } = this.options.getAdditionalVariantFields(ctx, variant);
|
|
@@ -278,6 +314,9 @@ let QlsProductService = class QlsProductService {
|
|
|
278
314
|
createdOrUpdated = 'updated';
|
|
279
315
|
}
|
|
280
316
|
}
|
|
317
|
+
if (createdOrUpdated === 'not-changed') {
|
|
318
|
+
core_1.Logger.info(`Variant '${variant.sku}' not updated in QLS, because no changes were found.`, constants_1.loggerCtx);
|
|
319
|
+
}
|
|
281
320
|
return createdOrUpdated;
|
|
282
321
|
}
|
|
283
322
|
/**
|
|
@@ -289,7 +328,9 @@ let QlsProductService = class QlsProductService {
|
|
|
289
328
|
existingEans: existingAdditionalEANs,
|
|
290
329
|
desiredEans: additionalEANs,
|
|
291
330
|
});
|
|
292
|
-
if (!eansToUpdate
|
|
331
|
+
if (!eansToUpdate ||
|
|
332
|
+
(eansToUpdate.eansToAdd.length === 0 &&
|
|
333
|
+
eansToUpdate.eansToRemove.length === 0)) {
|
|
293
334
|
// No updates needed
|
|
294
335
|
return false;
|
|
295
336
|
}
|
|
@@ -363,7 +404,7 @@ let QlsProductService = class QlsProductService {
|
|
|
363
404
|
* Update stock level for a variant based on the given available stock
|
|
364
405
|
*/
|
|
365
406
|
async updateStock(ctx, variantId, availableStock) {
|
|
366
|
-
if (this.options.
|
|
407
|
+
if (!this.options.synchronizeStockLevels) {
|
|
367
408
|
core_1.Logger.warn(`Stock sync disabled. Not updating stock for variant '${variantId}'`, constants_1.loggerCtx);
|
|
368
409
|
return;
|
|
369
410
|
}
|
|
@@ -390,5 +431,6 @@ exports.QlsProductService = QlsProductService = __decorate([
|
|
|
390
431
|
core_1.StockLocationService,
|
|
391
432
|
core_1.EventBus,
|
|
392
433
|
core_1.ListQueryBuilder,
|
|
393
|
-
core_1.ProductPriceApplicator
|
|
434
|
+
core_1.ProductPriceApplicator,
|
|
435
|
+
core_2.ModuleRef])
|
|
394
436
|
], QlsProductService);
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ID, Injector, Order, ProductVariant, RequestContext, SerializedRequestContext } from '@vendure/core';
|
|
2
|
-
import { CustomValue, FulfillmentProductInput } from './lib/client-types';
|
|
2
|
+
import { CustomValue, FulfillmentOrderInput, 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
|
|
@@ -21,10 +21,32 @@ export interface QlsPluginOptions {
|
|
|
21
21
|
*/
|
|
22
22
|
webhookSecret: string;
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
26
|
*/
|
|
27
|
-
|
|
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;
|
|
28
50
|
}
|
|
29
51
|
/**
|
|
30
52
|
* Additional fields for a product variant that are used to create or update a product in QLS
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pinelab/vendure-plugin-qls-fulfillment",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Vendure plugin to fulfill orders via QLS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fulfillment",
|
|
@@ -31,5 +31,6 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"catch-unknown": "^2.0.0"
|
|
34
|
-
}
|
|
34
|
+
},
|
|
35
|
+
"gitHead": "6bdee4f31075af25589b52e08cb0ac5658d90f92"
|
|
35
36
|
}
|