@pinelab/vendure-plugin-qls-fulfillment 1.0.0-beta.2 → 1.0.0-beta.21
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 +33 -1
- 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/custom-fields.d.ts +6 -0
- package/dist/custom-fields.js +51 -1
- package/dist/lib/client-types.d.ts +14 -1
- package/dist/lib/qls-client.d.ts +13 -2
- package/dist/lib/qls-client.js +42 -5
- package/dist/qls-plugin.js +11 -1
- package/dist/services/qls-order.service.d.ts +4 -3
- package/dist/services/qls-order.service.js +37 -10
- package/dist/services/qls-product.service.d.ts +21 -9
- package/dist/services/qls-product.service.js +181 -57
- package/dist/services/util.d.ts +15 -0
- package/dist/services/util.js +31 -0
- package/dist/services/util.spec.d.ts +1 -0
- package/dist/services/util.spec.js +60 -0
- package/dist/types.d.ts +36 -2
- package/package.json +1 -1
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:
|
|
@@ -58,7 +85,12 @@ Make sure to monitor failed jobs: A job that failed after its retries were exhau
|
|
|
58
85
|
1. An order was not pushed to QLS
|
|
59
86
|
2. A product was not synced to QLS
|
|
60
87
|
|
|
61
|
-
Monitor your logs for the
|
|
88
|
+
Monitor your logs for the following text:
|
|
89
|
+
|
|
90
|
+
- `QLS webhook error` - This means an incoming stock update webhook was not processed correctly.
|
|
91
|
+
- `Error creating or updating variant` - This means a product was not synced to QLS.
|
|
92
|
+
|
|
93
|
+
Make sure to filter by logger context `QlsPlugin`, to prevent false positive alerts.
|
|
62
94
|
|
|
63
95
|
## Cancelling orders and manually pushing orders to QLS
|
|
64
96
|
|
|
@@ -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);
|
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
|
+
];
|
|
@@ -30,6 +30,9 @@ export type FulfillmentProduct = {
|
|
|
30
30
|
collection_id: string | null;
|
|
31
31
|
dimensions: unknown;
|
|
32
32
|
article_number: string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Main EAN of the product
|
|
35
|
+
*/
|
|
33
36
|
ean: string;
|
|
34
37
|
name: string;
|
|
35
38
|
sku: string;
|
|
@@ -53,8 +56,18 @@ export type FulfillmentProduct = {
|
|
|
53
56
|
amount_blocked: number;
|
|
54
57
|
status: string | null;
|
|
55
58
|
suppliers: unknown[];
|
|
56
|
-
barcodes:
|
|
59
|
+
barcodes: Array<{
|
|
60
|
+
id: number;
|
|
61
|
+
fulfillment_product_id: string;
|
|
62
|
+
company_id: string;
|
|
63
|
+
barcode: string;
|
|
64
|
+
created: string;
|
|
65
|
+
modified: string;
|
|
66
|
+
}>;
|
|
57
67
|
image_url_handheld: string | null;
|
|
68
|
+
/**
|
|
69
|
+
* All EANs of the product, including the main EAN
|
|
70
|
+
*/
|
|
58
71
|
barcodes_and_ean: string[];
|
|
59
72
|
};
|
|
60
73
|
export interface FulfillmentOrderInput {
|
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,17 @@ 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
|
+
/**
|
|
28
|
+
* Add an extra barcode to a fulfillment product in QLS
|
|
29
|
+
*/
|
|
30
|
+
addBarcode(productId: string, barcode: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Add an extra barcode to a fulfillment product in QLS
|
|
33
|
+
*/
|
|
34
|
+
removeBarcode(productId: string, barcodeId: number): Promise<void>;
|
|
35
|
+
getServicePoints(countryCode: string, postalCode: string): Promise<QlsServicePoint[]>;
|
|
36
|
+
rawRequest<T>(method: 'POST' | 'GET' | 'PUT' | 'DELETE', action: string, data?: unknown): Promise<QlsApiResponse<T | undefined>>;
|
|
26
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) {
|
|
@@ -50,25 +50,53 @@ class QlsClient {
|
|
|
50
50
|
allProducts.push(...result.data);
|
|
51
51
|
hasNextPage = result.pagination?.nextPage ?? false;
|
|
52
52
|
page++;
|
|
53
|
-
|
|
53
|
+
core_1.Logger.info(`Fetched products ${allProducts.length}/${result.pagination?.count}`, constants_1.loggerCtx);
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, 700)); // Limit to ~86/minute, because the rate limit is 500/5 minutes
|
|
54
55
|
}
|
|
55
56
|
return allProducts;
|
|
56
57
|
}
|
|
57
58
|
async createFulfillmentProduct(data) {
|
|
58
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
|
+
}
|
|
59
63
|
return response.data;
|
|
60
64
|
}
|
|
61
65
|
async updateFulfillmentProduct(fulfillmentProductId, data) {
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
await this.rawRequest('PUT', `fulfillment/products/${fulfillmentProductId}`, data);
|
|
67
|
+
}
|
|
68
|
+
async deleteFulfillmentProduct(fulfillmentProductId) {
|
|
69
|
+
await this.rawRequest('DELETE', `fulfillment/products/${fulfillmentProductId}`);
|
|
64
70
|
}
|
|
65
71
|
async createFulfillmentOrder(data) {
|
|
66
72
|
const response = await this.rawRequest('POST', 'fulfillment/orders', {
|
|
67
73
|
...data,
|
|
68
74
|
brand_id: this.config.brandId,
|
|
69
75
|
});
|
|
76
|
+
if (!response.data) {
|
|
77
|
+
throw new Error('Failed to create fulfillment order. Got empty response.');
|
|
78
|
+
}
|
|
70
79
|
return response.data;
|
|
71
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Add an extra barcode to a fulfillment product in QLS
|
|
83
|
+
*/
|
|
84
|
+
async addBarcode(productId, barcode) {
|
|
85
|
+
await this.rawRequest('POST', `fulfillment/products/${productId}/barcodes`, {
|
|
86
|
+
barcode,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Add an extra barcode to a fulfillment product in QLS
|
|
91
|
+
*/
|
|
92
|
+
async removeBarcode(productId, barcodeId) {
|
|
93
|
+
await this.rawRequest('DELETE', `fulfillment/products/${productId}/barcodes/${barcodeId}`);
|
|
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
|
+
}
|
|
72
100
|
async rawRequest(method, action, data) {
|
|
73
101
|
// Set headers
|
|
74
102
|
const headers = {
|
|
@@ -83,10 +111,19 @@ class QlsClient {
|
|
|
83
111
|
headers,
|
|
84
112
|
body,
|
|
85
113
|
});
|
|
114
|
+
if (response.status === 204) {
|
|
115
|
+
// 204 No Content
|
|
116
|
+
return {
|
|
117
|
+
data: undefined,
|
|
118
|
+
meta: {
|
|
119
|
+
code: response.status,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
86
123
|
if (!response.ok) {
|
|
87
124
|
const errorText = await response.text();
|
|
88
125
|
// Log error including the request body
|
|
89
|
-
core_1.Logger.error(`QLS request 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);
|
|
90
127
|
throw new Error(errorText);
|
|
91
128
|
}
|
|
92
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
|
/**
|
|
@@ -25,5 +25,6 @@ export declare class QlsOrderService implements OnModuleInit, OnApplicationBoots
|
|
|
25
25
|
*/
|
|
26
26
|
handleOrderStatusUpdate(ctx: RequestContext, body: IncomingOrderWebhook): Promise<void>;
|
|
27
27
|
triggerPushOrder(ctx: RequestContext, orderId: ID, orderCode?: string): Promise<Job<QlsOrderJobData> | undefined>;
|
|
28
|
+
getServicePoints(ctx: RequestContext, input: QlsServicePointSearchInput): Promise<QlsServicePoint[]>;
|
|
28
29
|
private getVendureOrderState;
|
|
29
30
|
}
|
|
@@ -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);
|
|
@@ -82,9 +85,20 @@ let QlsOrderService = class QlsOrderService {
|
|
|
82
85
|
if (!order) {
|
|
83
86
|
throw new Error(`No order with id ${orderId} not found`);
|
|
84
87
|
}
|
|
88
|
+
if (order.customFields.syncedToQls) {
|
|
89
|
+
throw new core_2.UserInputError(`Order '${order.code}' has already been synced to QLS`);
|
|
90
|
+
}
|
|
85
91
|
try {
|
|
86
92
|
// Check if all products are available in QLS
|
|
87
|
-
const qlsProducts = order.lines
|
|
93
|
+
const qlsProducts = order.lines
|
|
94
|
+
.filter((line) => {
|
|
95
|
+
if (this.options.excludeVariantFromSync?.(ctx, line.productVariant)) {
|
|
96
|
+
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);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
})
|
|
101
|
+
.map((line) => {
|
|
88
102
|
if (!line.productVariant.customFields.qlsProductId) {
|
|
89
103
|
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
104
|
}
|
|
@@ -112,15 +126,17 @@ let QlsOrderService = class QlsOrderService {
|
|
|
112
126
|
!order.shippingAddress.countryCode) {
|
|
113
127
|
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
128
|
}
|
|
129
|
+
const processable = (await this.options.processOrderFrom?.(ctx, order)) ?? new Date();
|
|
130
|
+
const receiverContact = this.options.getReceiverContact?.(ctx, order);
|
|
115
131
|
const qlsOrder = {
|
|
116
132
|
customer_reference: order.code,
|
|
117
|
-
processable:
|
|
118
|
-
servicepoint_code:
|
|
133
|
+
processable: processable.toISOString(),
|
|
134
|
+
servicepoint_code: order.customFields?.qlsServicePointId,
|
|
119
135
|
delivery_options: additionalOrderFields?.delivery_options ?? [],
|
|
120
136
|
total_price: order.totalWithTax,
|
|
121
|
-
receiver_contact: {
|
|
137
|
+
receiver_contact: receiverContact ?? {
|
|
122
138
|
name: order.shippingAddress.fullName || customerName,
|
|
123
|
-
companyname: order.shippingAddress.company,
|
|
139
|
+
companyname: order.shippingAddress.company ?? '',
|
|
124
140
|
street: order.shippingAddress.streetLine1,
|
|
125
141
|
housenumber: order.shippingAddress.streetLine2,
|
|
126
142
|
postalcode: order.shippingAddress.postalCode,
|
|
@@ -133,6 +149,10 @@ let QlsOrderService = class QlsOrderService {
|
|
|
133
149
|
...(additionalOrderFields ?? {}),
|
|
134
150
|
};
|
|
135
151
|
const result = await client.createFulfillmentOrder(qlsOrder);
|
|
152
|
+
await this.orderService.updateCustomFields(ctx, orderId, {
|
|
153
|
+
syncedToQls: true,
|
|
154
|
+
});
|
|
155
|
+
core_2.Logger.info(`Successfully created order '${order.code}' in QLS with id '${result.id}'`, constants_1.loggerCtx);
|
|
136
156
|
await this.orderService.addNoteToOrder(ctx, {
|
|
137
157
|
id: orderId,
|
|
138
158
|
isPublic: false,
|
|
@@ -182,7 +202,14 @@ let QlsOrderService = class QlsOrderService {
|
|
|
182
202
|
action: 'push-order',
|
|
183
203
|
ctx: ctx.serialize(),
|
|
184
204
|
orderId,
|
|
185
|
-
}, { retries:
|
|
205
|
+
}, { retries: 3 });
|
|
206
|
+
}
|
|
207
|
+
async getServicePoints(ctx, input) {
|
|
208
|
+
const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
|
|
209
|
+
if (!client) {
|
|
210
|
+
throw new core_2.UserInputError(`QLS not enabled for channel ${ctx.channel.token}`);
|
|
211
|
+
}
|
|
212
|
+
return await client.getServicePoints(input.countryCode, input.postalCode);
|
|
186
213
|
}
|
|
187
214
|
getVendureOrderState(body) {
|
|
188
215
|
if (body.cancelled) {
|
|
@@ -202,8 +229,8 @@ let QlsOrderService = class QlsOrderService {
|
|
|
202
229
|
exports.QlsOrderService = QlsOrderService;
|
|
203
230
|
exports.QlsOrderService = QlsOrderService = __decorate([
|
|
204
231
|
(0, common_1.Injectable)(),
|
|
205
|
-
__param(
|
|
206
|
-
__metadata("design:paramtypes", [
|
|
232
|
+
__param(0, (0, common_1.Inject)(constants_1.PLUGIN_INIT_OPTIONS)),
|
|
233
|
+
__metadata("design:paramtypes", [Object, core_2.JobQueueService,
|
|
207
234
|
core_2.EventBus,
|
|
208
235
|
core_2.OrderService,
|
|
209
236
|
core_1.ModuleRef])
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { OnApplicationBootstrap, OnModuleInit } from '@nestjs/common';
|
|
2
|
-
import { EventBus, ID, Job, JobQueueService, ProductVariant, ProductVariantService, RequestContext, StockLevelService, StockLocationService, TransactionalConnection } from '@vendure/core';
|
|
2
|
+
import { EventBus, ID, Job, JobQueueService, ListQueryBuilder, ProductPriceApplicator, ProductVariant, ProductVariantService, RequestContext, StockLevelService, StockLocationService, TransactionalConnection } from '@vendure/core';
|
|
3
|
+
import { FulfillmentProduct } from '../lib/client-types';
|
|
3
4
|
import { QlsClient } from '../lib/qls-client';
|
|
4
5
|
import { QlsPluginOptions, QlsProductJobData } from '../types';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
type SyncProductsResult = {
|
|
7
|
+
updatedInQls: Partial<ProductVariant>[];
|
|
8
|
+
createdInQls: Partial<ProductVariant>[];
|
|
9
|
+
updatedStock: Partial<ProductVariant>[];
|
|
10
|
+
failed: Partial<ProductVariant>[];
|
|
10
11
|
};
|
|
11
12
|
export declare class QlsProductService implements OnModuleInit, OnApplicationBootstrap {
|
|
12
13
|
private connection;
|
|
@@ -16,8 +17,10 @@ export declare class QlsProductService implements OnModuleInit, OnApplicationBoo
|
|
|
16
17
|
private readonly variantService;
|
|
17
18
|
private readonly stockLocationService;
|
|
18
19
|
private readonly eventBus;
|
|
20
|
+
private readonly listQueryBuilder;
|
|
21
|
+
private readonly productPriceApplicator;
|
|
19
22
|
private productJobQueue;
|
|
20
|
-
constructor(connection: TransactionalConnection, options: QlsPluginOptions, jobQueueService: JobQueueService, stockLevelService: StockLevelService, variantService: ProductVariantService, stockLocationService: StockLocationService, eventBus: EventBus);
|
|
23
|
+
constructor(connection: TransactionalConnection, options: QlsPluginOptions, jobQueueService: JobQueueService, stockLevelService: StockLevelService, variantService: ProductVariantService, stockLocationService: StockLocationService, eventBus: EventBus, listQueryBuilder: ListQueryBuilder, productPriceApplicator: ProductPriceApplicator);
|
|
21
24
|
onApplicationBootstrap(): void;
|
|
22
25
|
onModuleInit(): Promise<void>;
|
|
23
26
|
/**
|
|
@@ -32,11 +35,16 @@ export declare class QlsProductService implements OnModuleInit, OnApplicationBoo
|
|
|
32
35
|
* 3. Creates products in QLS if needed
|
|
33
36
|
* 4. Updates products in QLS if needed
|
|
34
37
|
*/
|
|
35
|
-
runFullSync(ctx: RequestContext): Promise<
|
|
38
|
+
runFullSync(ctx: RequestContext): Promise<SyncProductsResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Utility function to remove all products from QLS.
|
|
41
|
+
* You might need to cancel active orders in QLS first, because products cannot be deleted if they are in an active order.
|
|
42
|
+
*/
|
|
43
|
+
removeAllProductsFromQls(ctx: RequestContext): Promise<void>;
|
|
36
44
|
/**
|
|
37
45
|
* Creates or updates the fulfillment products in QLS for the given product variants.
|
|
38
46
|
*/
|
|
39
|
-
syncVariants(ctx: RequestContext, productVariantIds: ID[]): Promise<
|
|
47
|
+
syncVariants(ctx: RequestContext, productVariantIds: ID[]): Promise<SyncProductsResult>;
|
|
40
48
|
/**
|
|
41
49
|
* Trigger a full product sync job
|
|
42
50
|
*/
|
|
@@ -53,6 +61,10 @@ export declare class QlsProductService implements OnModuleInit, OnApplicationBoo
|
|
|
53
61
|
* Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
|
|
54
62
|
*/
|
|
55
63
|
createOrUpdateProductInQls(ctx: RequestContext, client: QlsClient, variant: ProductVariant, existingProduct: FulfillmentProduct | null): Promise<'created' | 'updated' | 'not-changed'>;
|
|
64
|
+
/**
|
|
65
|
+
* Update the additional EANs/barcodes for a product in QLS if needed
|
|
66
|
+
*/
|
|
67
|
+
private updateAdditionalEANs;
|
|
56
68
|
/**
|
|
57
69
|
* Determine if a product needs to be updated in QLS based on the given variant and QLS product.
|
|
58
70
|
*/
|
|
@@ -19,11 +19,15 @@ exports.QlsProductService = void 0;
|
|
|
19
19
|
const common_1 = require("@nestjs/common");
|
|
20
20
|
const core_1 = require("@vendure/core");
|
|
21
21
|
const catch_unknown_1 = require("catch-unknown");
|
|
22
|
+
const typeorm_1 = require("typeorm");
|
|
22
23
|
const util_1 = __importDefault(require("util"));
|
|
23
24
|
const constants_1 = require("../constants");
|
|
24
25
|
const qls_client_1 = require("../lib/qls-client");
|
|
26
|
+
const util_2 = require("./util");
|
|
27
|
+
// Wait for 700ms to avoid rate limit of 500/5 minutes
|
|
28
|
+
const waitToPreventRateLimit = () => new Promise((resolve) => setTimeout(resolve, 700));
|
|
25
29
|
let QlsProductService = class QlsProductService {
|
|
26
|
-
constructor(connection, options, jobQueueService, stockLevelService, variantService, stockLocationService, eventBus) {
|
|
30
|
+
constructor(connection, options, jobQueueService, stockLevelService, variantService, stockLocationService, eventBus, listQueryBuilder, productPriceApplicator) {
|
|
27
31
|
this.connection = connection;
|
|
28
32
|
this.options = options;
|
|
29
33
|
this.jobQueueService = jobQueueService;
|
|
@@ -31,6 +35,8 @@ let QlsProductService = class QlsProductService {
|
|
|
31
35
|
this.variantService = variantService;
|
|
32
36
|
this.stockLocationService = stockLocationService;
|
|
33
37
|
this.eventBus = eventBus;
|
|
38
|
+
this.listQueryBuilder = listQueryBuilder;
|
|
39
|
+
this.productPriceApplicator = productPriceApplicator;
|
|
34
40
|
}
|
|
35
41
|
onApplicationBootstrap() {
|
|
36
42
|
// Listen for ProductVariantEvent and add a job to the queue
|
|
@@ -60,7 +66,13 @@ let QlsProductService = class QlsProductService {
|
|
|
60
66
|
try {
|
|
61
67
|
const ctx = core_1.RequestContext.deserialize(job.data.ctx);
|
|
62
68
|
if (job.data.action === 'full-sync-products') {
|
|
63
|
-
|
|
69
|
+
const result = await this.runFullSync(ctx);
|
|
70
|
+
return {
|
|
71
|
+
updatedInQls: result.updatedInQls.length,
|
|
72
|
+
createdInQls: result.createdInQls.length,
|
|
73
|
+
updatedStock: result.updatedStock.length,
|
|
74
|
+
failed: result.failed.length,
|
|
75
|
+
};
|
|
64
76
|
}
|
|
65
77
|
else if (job.data.action === 'sync-products') {
|
|
66
78
|
return await this.syncVariants(ctx, job.data.productVariantIds);
|
|
@@ -95,34 +107,49 @@ let QlsProductService = class QlsProductService {
|
|
|
95
107
|
const allVariants = await this.getAllVariants(ctx);
|
|
96
108
|
core_1.Logger.info(`Running full sync for ${allQlsProducts.length} QLS products and ${allVariants.length} Vendure variants`, constants_1.loggerCtx);
|
|
97
109
|
// Update stock in Vendure based on QLS products
|
|
98
|
-
|
|
110
|
+
const updatedStock = [];
|
|
99
111
|
for (const variant of allVariants) {
|
|
100
112
|
const qlsProduct = allQlsProducts.find((p) => p.sku == variant.sku);
|
|
101
113
|
if (qlsProduct) {
|
|
102
114
|
await this.updateStock(ctx, variant.id, qlsProduct.amount_available);
|
|
103
|
-
|
|
115
|
+
updatedStock.push(variant);
|
|
104
116
|
}
|
|
105
117
|
}
|
|
106
|
-
core_1.Logger.info(`Updated stock for ${
|
|
118
|
+
core_1.Logger.info(`Updated stock for ${updatedStock.length} variants based on QLS stock levels`, constants_1.loggerCtx);
|
|
107
119
|
// Create or update products in QLS
|
|
108
|
-
|
|
109
|
-
|
|
120
|
+
const createdQlsProducts = [];
|
|
121
|
+
const updatedQlsProducts = [];
|
|
122
|
+
const failed = [];
|
|
110
123
|
for (const variant of allVariants) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
try {
|
|
125
|
+
const existingQlsProduct = allQlsProducts.find((p) => p.sku == variant.sku);
|
|
126
|
+
const result = await this.createOrUpdateProductInQls(ctx, client, variant, existingQlsProduct ?? null);
|
|
127
|
+
if (result === 'created') {
|
|
128
|
+
createdQlsProducts.push(variant);
|
|
129
|
+
}
|
|
130
|
+
else if (result === 'updated') {
|
|
131
|
+
updatedQlsProducts.push(variant);
|
|
132
|
+
}
|
|
133
|
+
if (result === 'created' || result === 'updated') {
|
|
134
|
+
// Wait only if we created or updated a product, otherwise no calls have been made yet.
|
|
135
|
+
await waitToPreventRateLimit();
|
|
136
|
+
}
|
|
115
137
|
}
|
|
116
|
-
|
|
117
|
-
|
|
138
|
+
catch (e) {
|
|
139
|
+
const error = (0, catch_unknown_1.asError)(e);
|
|
140
|
+
core_1.Logger.error(`Error creating or updating variant '${variant.sku}' in QLS: ${error.message}`, constants_1.loggerCtx, error.stack);
|
|
141
|
+
failed.push(variant);
|
|
142
|
+
await waitToPreventRateLimit();
|
|
118
143
|
}
|
|
119
144
|
}
|
|
120
|
-
core_1.Logger.info(`Created ${
|
|
121
|
-
core_1.Logger.info(`Updated ${
|
|
145
|
+
core_1.Logger.info(`Created ${createdQlsProducts.length} products in QLS`, constants_1.loggerCtx);
|
|
146
|
+
core_1.Logger.info(`Updated ${updatedQlsProducts.length} products in QLS`, constants_1.loggerCtx);
|
|
147
|
+
core_1.Logger.info(`Finished full sync with ${failed.length} failures`, constants_1.loggerCtx);
|
|
122
148
|
return {
|
|
123
|
-
updatedInQls:
|
|
124
|
-
createdInQls:
|
|
125
|
-
updatedStock
|
|
149
|
+
updatedInQls: updatedQlsProducts,
|
|
150
|
+
createdInQls: createdQlsProducts,
|
|
151
|
+
updatedStock,
|
|
152
|
+
failed,
|
|
126
153
|
};
|
|
127
154
|
}
|
|
128
155
|
catch (e) {
|
|
@@ -131,6 +158,24 @@ let QlsProductService = class QlsProductService {
|
|
|
131
158
|
throw error;
|
|
132
159
|
}
|
|
133
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Utility function to remove all products from QLS.
|
|
163
|
+
* You might need to cancel active orders in QLS first, because products cannot be deleted if they are in an active order.
|
|
164
|
+
*/
|
|
165
|
+
async removeAllProductsFromQls(ctx) {
|
|
166
|
+
const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
|
|
167
|
+
if (!client) {
|
|
168
|
+
throw new Error(`QLS not enabled for channel ${ctx.channel.token}`);
|
|
169
|
+
}
|
|
170
|
+
core_1.Logger.warn(`Removing all products from QLS for channel ${ctx.channel.token}...`, constants_1.loggerCtx);
|
|
171
|
+
const allProducts = await client.getAllFulfillmentProducts();
|
|
172
|
+
for (const product of allProducts) {
|
|
173
|
+
await client.deleteFulfillmentProduct(product.id);
|
|
174
|
+
core_1.Logger.info(`Removed product '${product.sku}' (${product.id}) from QLS`, constants_1.loggerCtx);
|
|
175
|
+
await waitToPreventRateLimit();
|
|
176
|
+
}
|
|
177
|
+
core_1.Logger.warn(`Removed ${allProducts.length} products from QLS for channel ${ctx.channel.token}`, constants_1.loggerCtx);
|
|
178
|
+
}
|
|
134
179
|
/**
|
|
135
180
|
* Creates or updates the fulfillment products in QLS for the given product variants.
|
|
136
181
|
*/
|
|
@@ -139,32 +184,47 @@ let QlsProductService = class QlsProductService {
|
|
|
139
184
|
if (!client) {
|
|
140
185
|
core_1.Logger.debug(`QLS not enabled for channel ${ctx.channel.token}. Not handling product update/create.`, constants_1.loggerCtx);
|
|
141
186
|
return {
|
|
142
|
-
updatedInQls:
|
|
143
|
-
createdInQls:
|
|
144
|
-
updatedStock:
|
|
187
|
+
updatedInQls: [],
|
|
188
|
+
createdInQls: [],
|
|
189
|
+
updatedStock: [],
|
|
190
|
+
failed: [],
|
|
145
191
|
};
|
|
146
192
|
}
|
|
147
|
-
|
|
148
|
-
|
|
193
|
+
const updatedInQls = [];
|
|
194
|
+
const createdInQls = [];
|
|
195
|
+
const failed = [];
|
|
149
196
|
for (const variantId of productVariantIds) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
197
|
+
try {
|
|
198
|
+
const variant = await this.variantService.findOne(ctx, variantId, [
|
|
199
|
+
'featuredAsset',
|
|
200
|
+
'taxCategory',
|
|
201
|
+
'channels',
|
|
202
|
+
'product.featuredAsset',
|
|
203
|
+
]);
|
|
204
|
+
if (!variant) {
|
|
205
|
+
core_1.Logger.error(`Variant with id ${variantId} not found. Not creating or updating product in QLS.`, constants_1.loggerCtx);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const existingQlsProduct = await client.getFulfillmentProductBySku(variant.sku);
|
|
209
|
+
const result = await this.createOrUpdateProductInQls(ctx, client, variant, existingQlsProduct ?? null);
|
|
210
|
+
if (result === 'created') {
|
|
211
|
+
createdInQls.push(variant);
|
|
212
|
+
}
|
|
213
|
+
else if (result === 'updated') {
|
|
214
|
+
updatedInQls.push(variant);
|
|
215
|
+
}
|
|
159
216
|
}
|
|
160
|
-
|
|
161
|
-
|
|
217
|
+
catch (e) {
|
|
218
|
+
const error = (0, catch_unknown_1.asError)(e);
|
|
219
|
+
core_1.Logger.error(`Error syncing variant ${variantId} to QLS: ${error.message}`, constants_1.loggerCtx, error.stack);
|
|
220
|
+
failed.push({ id: variantId });
|
|
162
221
|
}
|
|
163
222
|
}
|
|
164
223
|
return {
|
|
165
224
|
updatedInQls,
|
|
166
225
|
createdInQls,
|
|
167
|
-
updatedStock:
|
|
226
|
+
updatedStock: [],
|
|
227
|
+
failed,
|
|
168
228
|
};
|
|
169
229
|
}
|
|
170
230
|
/**
|
|
@@ -189,7 +249,7 @@ let QlsProductService = class QlsProductService {
|
|
|
189
249
|
action: 'sync-products',
|
|
190
250
|
ctx: ctx.serialize(),
|
|
191
251
|
productVariantIds,
|
|
192
|
-
}, { retries:
|
|
252
|
+
}, { retries: 1 });
|
|
193
253
|
}
|
|
194
254
|
/**
|
|
195
255
|
* Update the stock level for a variant based on the given available stock
|
|
@@ -211,45 +271,92 @@ let QlsProductService = class QlsProductService {
|
|
|
211
271
|
* Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
|
|
212
272
|
*/
|
|
213
273
|
async createOrUpdateProductInQls(ctx, client, variant, existingProduct) {
|
|
214
|
-
|
|
274
|
+
if (this.options.excludeVariantFromSync?.(ctx, variant)) {
|
|
275
|
+
core_1.Logger.info(`Variant '${variant.sku}' excluded from sync to QLS.`, constants_1.loggerCtx);
|
|
276
|
+
return 'not-changed';
|
|
277
|
+
}
|
|
278
|
+
let qlsProduct = existingProduct;
|
|
215
279
|
let createdOrUpdated = 'not-changed';
|
|
280
|
+
const { additionalEANs, ...additionalVariantFields } = this.options.getAdditionalVariantFields(ctx, variant);
|
|
216
281
|
if (!existingProduct) {
|
|
217
282
|
const result = await client.createFulfillmentProduct({
|
|
218
283
|
name: variant.name,
|
|
219
284
|
sku: variant.sku,
|
|
220
|
-
...
|
|
285
|
+
...additionalVariantFields,
|
|
221
286
|
});
|
|
222
|
-
|
|
287
|
+
qlsProduct = result;
|
|
223
288
|
core_1.Logger.info(`Created product '${variant.sku}' in QLS`, constants_1.loggerCtx);
|
|
224
289
|
createdOrUpdated = 'created';
|
|
225
290
|
}
|
|
226
|
-
else if (this.shouldUpdateProductInQls(
|
|
291
|
+
else if (this.shouldUpdateProductInQls(variant, existingProduct, additionalVariantFields)) {
|
|
227
292
|
await client.updateFulfillmentProduct(existingProduct.id, {
|
|
228
293
|
sku: variant.sku,
|
|
229
294
|
name: variant.name,
|
|
230
|
-
...
|
|
295
|
+
...additionalVariantFields,
|
|
231
296
|
});
|
|
232
297
|
core_1.Logger.info(`Updated product '${variant.sku}' in QLS`, constants_1.loggerCtx);
|
|
233
298
|
createdOrUpdated = 'updated';
|
|
234
299
|
}
|
|
235
|
-
if (
|
|
300
|
+
if (qlsProduct && qlsProduct?.id !== variant.customFields.qlsProductId) {
|
|
236
301
|
// Update variant with QLS product ID if it changed
|
|
237
302
|
// Do not use variantService.update because it will trigger a change event and cause an infinite loop
|
|
238
303
|
await this.connection
|
|
239
304
|
.getRepository(ctx, core_1.ProductVariant)
|
|
240
|
-
.update({ id: variant.id }, { customFields: { qlsProductId } });
|
|
241
|
-
core_1.Logger.info(`Set QLS product ID for variant '${variant.sku}' to ${
|
|
305
|
+
.update({ id: variant.id }, { customFields: { qlsProductId: qlsProduct.id } });
|
|
306
|
+
core_1.Logger.info(`Set QLS product ID for variant '${variant.sku}' to ${qlsProduct.id}`, constants_1.loggerCtx);
|
|
307
|
+
}
|
|
308
|
+
if (qlsProduct) {
|
|
309
|
+
const updatedEANs = await this.updateAdditionalEANs(ctx, client, qlsProduct, additionalEANs);
|
|
310
|
+
if (createdOrUpdated === 'not-changed' && updatedEANs) {
|
|
311
|
+
// If nothing changed so far, but EANs changed, then we did update the product in QLS
|
|
312
|
+
createdOrUpdated = 'updated';
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (createdOrUpdated === 'not-changed') {
|
|
316
|
+
core_1.Logger.info(`Variant '${variant.sku}' not updated in QLS, because no changes were found.`, constants_1.loggerCtx);
|
|
242
317
|
}
|
|
243
318
|
return createdOrUpdated;
|
|
244
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Update the additional EANs/barcodes for a product in QLS if needed
|
|
322
|
+
*/
|
|
323
|
+
async updateAdditionalEANs(ctx, client, qlsProduct, additionalEANs) {
|
|
324
|
+
const existingAdditionalEANs = qlsProduct?.barcodes_and_ean.filter((ean) => ean !== qlsProduct.ean); // Remove the main EAN
|
|
325
|
+
const eansToUpdate = (0, util_2.getEansToUpdate)({
|
|
326
|
+
existingEans: existingAdditionalEANs,
|
|
327
|
+
desiredEans: additionalEANs,
|
|
328
|
+
});
|
|
329
|
+
if (!eansToUpdate ||
|
|
330
|
+
(eansToUpdate.eansToAdd.length === 0 &&
|
|
331
|
+
eansToUpdate.eansToRemove.length === 0)) {
|
|
332
|
+
// No updates needed
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
// Add additional EANs
|
|
336
|
+
eansToUpdate.eansToAdd = (0, util_2.normalizeEans)(eansToUpdate.eansToAdd);
|
|
337
|
+
await Promise.all(eansToUpdate.eansToAdd.map((ean) => client.addBarcode(qlsProduct.id, ean)));
|
|
338
|
+
if (eansToUpdate.eansToAdd.length > 0) {
|
|
339
|
+
core_1.Logger.info(`Added additional EANs: ${eansToUpdate.eansToAdd.join(',')} to product '${qlsProduct.sku}' in QLS`, constants_1.loggerCtx);
|
|
340
|
+
}
|
|
341
|
+
// Remove EANs, normalize first
|
|
342
|
+
eansToUpdate.eansToRemove = (0, util_2.normalizeEans)(eansToUpdate.eansToRemove);
|
|
343
|
+
// get barcode ID's to remove, because deletion goes via barcode ID, not barcode value
|
|
344
|
+
const barcodeIdsToRemove = qlsProduct.barcodes
|
|
345
|
+
.filter((barcode) => eansToUpdate.eansToRemove.includes(barcode.barcode))
|
|
346
|
+
.map((barcode) => barcode.id);
|
|
347
|
+
await Promise.all(barcodeIdsToRemove.map((barcodeId) => client.removeBarcode(qlsProduct.id, barcodeId)));
|
|
348
|
+
if (eansToUpdate.eansToRemove.length > 0) {
|
|
349
|
+
core_1.Logger.info(`Removed EANs '${eansToUpdate.eansToRemove.join(',')} from product '${qlsProduct.sku}' in QLS`, constants_1.loggerCtx);
|
|
350
|
+
}
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
245
353
|
/**
|
|
246
354
|
* Determine if a product needs to be updated in QLS based on the given variant and QLS product.
|
|
247
355
|
*/
|
|
248
|
-
shouldUpdateProductInQls(
|
|
249
|
-
const additionalFields = this.options.getAdditionalVariantFields?.(ctx, variant);
|
|
356
|
+
shouldUpdateProductInQls(variant, qlsProduct, additionalVariantFields) {
|
|
250
357
|
if (qlsProduct.name !== variant.name ||
|
|
251
|
-
qlsProduct.ean !==
|
|
252
|
-
qlsProduct.image_url !==
|
|
358
|
+
qlsProduct.ean !== additionalVariantFields?.ean ||
|
|
359
|
+
qlsProduct.image_url !== additionalVariantFields?.image_url) {
|
|
253
360
|
// If name or ean has changed, product should be updated in QLS
|
|
254
361
|
return true;
|
|
255
362
|
}
|
|
@@ -264,16 +371,27 @@ let QlsProductService = class QlsProductService {
|
|
|
264
371
|
const take = 100;
|
|
265
372
|
let hasMore = true;
|
|
266
373
|
while (hasMore) {
|
|
267
|
-
const
|
|
268
|
-
|
|
374
|
+
const relations = [
|
|
375
|
+
'featuredAsset',
|
|
376
|
+
'taxCategory',
|
|
377
|
+
'channels',
|
|
378
|
+
'product.featuredAsset',
|
|
379
|
+
];
|
|
380
|
+
const [items, totalItems] = await this.listQueryBuilder
|
|
381
|
+
.build(core_1.ProductVariant, {
|
|
269
382
|
skip,
|
|
270
383
|
take,
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
384
|
+
}, {
|
|
385
|
+
relations,
|
|
386
|
+
channelId: ctx.channelId,
|
|
387
|
+
where: { deletedAt: (0, typeorm_1.IsNull)() },
|
|
388
|
+
ctx,
|
|
389
|
+
})
|
|
390
|
+
.getManyAndCount();
|
|
391
|
+
let variants = await Promise.all(items.map(async (item) => this.productPriceApplicator.applyChannelPriceAndTax(item, ctx, undefined, false)));
|
|
392
|
+
variants = variants.map((v) => (0, core_1.translateDeep)(v, ctx.languageCode));
|
|
393
|
+
allVariants.push(...variants);
|
|
394
|
+
if (allVariants.length >= totalItems) {
|
|
277
395
|
hasMore = false;
|
|
278
396
|
}
|
|
279
397
|
skip += take;
|
|
@@ -284,6 +402,10 @@ let QlsProductService = class QlsProductService {
|
|
|
284
402
|
* Update stock level for a variant based on the given available stock
|
|
285
403
|
*/
|
|
286
404
|
async updateStock(ctx, variantId, availableStock) {
|
|
405
|
+
if (!this.options.synchronizeStockLevels) {
|
|
406
|
+
core_1.Logger.warn(`Stock sync disabled. Not updating stock for variant '${variantId}'`, constants_1.loggerCtx);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
287
409
|
// Find default Stock Location
|
|
288
410
|
const defaultStockLocation = await this.stockLocationService.defaultStockLocation(ctx);
|
|
289
411
|
// Get current stock level id
|
|
@@ -305,5 +427,7 @@ exports.QlsProductService = QlsProductService = __decorate([
|
|
|
305
427
|
core_1.StockLevelService,
|
|
306
428
|
core_1.ProductVariantService,
|
|
307
429
|
core_1.StockLocationService,
|
|
308
|
-
core_1.EventBus
|
|
430
|
+
core_1.EventBus,
|
|
431
|
+
core_1.ListQueryBuilder,
|
|
432
|
+
core_1.ProductPriceApplicator])
|
|
309
433
|
], QlsProductService);
|
|
@@ -0,0 +1,15 @@
|
|
|
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[];
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
});
|
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
|
|
@@ -9,7 +9,7 @@ export interface QlsPluginOptions {
|
|
|
9
9
|
* Required to get the EAN and image URL for a product variant.
|
|
10
10
|
* Also allows you to override other product attributes like name, price, etc.
|
|
11
11
|
*/
|
|
12
|
-
getAdditionalVariantFields
|
|
12
|
+
getAdditionalVariantFields: (ctx: RequestContext, variant: ProductVariant) => AdditionalVariantFields;
|
|
13
13
|
/**
|
|
14
14
|
* Function to get the set service point code for an order.
|
|
15
15
|
* Return undefined to not use a service point at all.
|
|
@@ -20,7 +20,41 @@ export interface QlsPluginOptions {
|
|
|
20
20
|
* Set this to a random string
|
|
21
21
|
*/
|
|
22
22
|
webhookSecret: string;
|
|
23
|
+
/**
|
|
24
|
+
* Allows you to disable the pulling in of stock levels from QLS. When disabled, stock in Vendure will not be modified based on QLS stock levels.
|
|
25
|
+
* Defaults to true.
|
|
26
|
+
*/
|
|
27
|
+
synchronizeStockLevels?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Allows you to disable the automatic pushing of orders to QLS. You can still push orders manually via the Admin UI.
|
|
30
|
+
* Defaults to true.
|
|
31
|
+
*/
|
|
32
|
+
autoPushOrders?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Allows you to define a date from when the order should be processed by QLS.
|
|
35
|
+
* You can for example make orders processable 2 hours from now, so that you can still edit the order in QLS
|
|
36
|
+
* Defaults to now.
|
|
37
|
+
*/
|
|
38
|
+
processOrderFrom?: (ctx: RequestContext, order: Order) => Date | Promise<Date>;
|
|
39
|
+
/**
|
|
40
|
+
* Optional function to determine if a product variant should be excluded from syncing to QLS.
|
|
41
|
+
* Return true to exclude the variant from sync, false or undefined to include it.
|
|
42
|
+
*/
|
|
43
|
+
excludeVariantFromSync?: (ctx: RequestContext, 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;
|
|
23
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Additional fields for a product variant that are used to create or update a product in QLS
|
|
53
|
+
*/
|
|
54
|
+
export type AdditionalVariantFields = Partial<FulfillmentProductInput & {
|
|
55
|
+
ean: string;
|
|
56
|
+
additionalEANs?: string[];
|
|
57
|
+
}>;
|
|
24
58
|
export interface AdditionalOrderFields {
|
|
25
59
|
servicepoint_code?: string;
|
|
26
60
|
delivery_options?: string[];
|