@pinelab/vendure-plugin-qls-fulfillment 1.0.0-beta.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/CHANGELOG.md +3 -0
- package/README.md +65 -0
- package/dist/api/api-extensions.d.ts +1 -0
- package/dist/api/api-extensions.js +20 -0
- package/dist/api/generated/graphql.d.ts +31 -0
- package/dist/api/generated/graphql.js +2 -0
- package/dist/api/qls-admin.resolver.d.ts +11 -0
- package/dist/api/qls-admin.resolver.js +59 -0
- package/dist/api/qls-webhooks-controller.d.ts +18 -0
- package/dist/api/qls-webhooks-controller.js +93 -0
- package/dist/config/full-product-sync-task.d.ts +2 -0
- package/dist/config/full-product-sync-task.js +42 -0
- package/dist/config/permissions.d.ts +3 -0
- package/dist/config/permissions.js +12 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +5 -0
- package/dist/custom-fields.d.ts +7 -0
- package/dist/custom-fields.js +15 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +26 -0
- package/dist/lib/client-types.d.ts +151 -0
- package/dist/lib/client-types.js +5 -0
- package/dist/lib/qls-client.d.ts +26 -0
- package/dist/lib/qls-client.js +99 -0
- package/dist/qls-plugin.d.ts +8 -0
- package/dist/qls-plugin.js +60 -0
- package/dist/services/qls-order.service.d.ts +29 -0
- package/dist/services/qls-order.service.js +210 -0
- package/dist/services/qls-product.service.d.ts +69 -0
- package/dist/services/qls-product.service.js +309 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +2 -0
- package/package.json +35 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QlsClient = void 0;
|
|
4
|
+
exports.getQlsClient = getQlsClient;
|
|
5
|
+
const core_1 = require("@vendure/core");
|
|
6
|
+
const constants_1 = require("../constants");
|
|
7
|
+
async function getQlsClient(ctx, pluginOptions) {
|
|
8
|
+
const config = await pluginOptions.getConfig(ctx);
|
|
9
|
+
if (!config) {
|
|
10
|
+
core_1.Logger.info(`QLS not enabled for channel ${ctx.channel.token}`, constants_1.loggerCtx);
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
return new QlsClient(config);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Wrapper around the QLS Rest API.
|
|
17
|
+
*/
|
|
18
|
+
class QlsClient {
|
|
19
|
+
constructor(config) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.baseUrl = config.url || 'https://api.pakketdienstqls.nl';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Find a product by SKU.
|
|
25
|
+
* Returns the first product found, or undefined if no product is found.
|
|
26
|
+
*/
|
|
27
|
+
async getFulfillmentProductBySku(sku) {
|
|
28
|
+
const result = await this.rawRequest('GET', `fulfillment/products?filter%5Bsku%5D=${encodeURIComponent(sku)}`);
|
|
29
|
+
if (result.data.length === 0) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
if (result.data.length > 1) {
|
|
33
|
+
core_1.Logger.error(`Multiple products found for SKU: ${sku}`, constants_1.loggerCtx, JSON.stringify(result.data));
|
|
34
|
+
}
|
|
35
|
+
return result.data[0];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get stock for all fulfillment products.
|
|
39
|
+
* Might require multiple requests if the result is paginated.
|
|
40
|
+
*/
|
|
41
|
+
async getAllFulfillmentProducts() {
|
|
42
|
+
let page = 1;
|
|
43
|
+
const allProducts = [];
|
|
44
|
+
let hasNextPage = true;
|
|
45
|
+
while (hasNextPage) {
|
|
46
|
+
const result = await this.rawRequest('GET', `fulfillment/products?page=${page}`);
|
|
47
|
+
if (!result.data || result.data.length === 0) {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
allProducts.push(...result.data);
|
|
51
|
+
hasNextPage = result.pagination?.nextPage ?? false;
|
|
52
|
+
page++;
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, 1100)); // Limit to 55/minute, because the rate limit is 60/minute
|
|
54
|
+
}
|
|
55
|
+
return allProducts;
|
|
56
|
+
}
|
|
57
|
+
async createFulfillmentProduct(data) {
|
|
58
|
+
const response = await this.rawRequest('POST', 'fulfillment/products', data);
|
|
59
|
+
return response.data;
|
|
60
|
+
}
|
|
61
|
+
async updateFulfillmentProduct(fulfillmentProductId, data) {
|
|
62
|
+
const response = await this.rawRequest('PUT', `fulfillment/products/${fulfillmentProductId}`, data);
|
|
63
|
+
return response.data;
|
|
64
|
+
}
|
|
65
|
+
async createFulfillmentOrder(data) {
|
|
66
|
+
const response = await this.rawRequest('POST', 'fulfillment/orders', {
|
|
67
|
+
...data,
|
|
68
|
+
brand_id: this.config.brandId,
|
|
69
|
+
});
|
|
70
|
+
return response.data;
|
|
71
|
+
}
|
|
72
|
+
async rawRequest(method, action, data) {
|
|
73
|
+
// Set headers
|
|
74
|
+
const headers = {
|
|
75
|
+
Authorization: `Basic ${Buffer.from(`${this.config.username}:${this.config.password}`).toString('base64')}`,
|
|
76
|
+
Accept: 'application/json',
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
};
|
|
79
|
+
const body = data ? JSON.stringify(data) : undefined;
|
|
80
|
+
const url = `${this.baseUrl}/companies/${this.config.companyId}/${action}`;
|
|
81
|
+
const response = await fetch(url, {
|
|
82
|
+
method,
|
|
83
|
+
headers,
|
|
84
|
+
body,
|
|
85
|
+
});
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
const errorText = await response.text();
|
|
88
|
+
// 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);
|
|
90
|
+
throw new Error(errorText);
|
|
91
|
+
}
|
|
92
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
93
|
+
if (contentType.includes('application/json')) {
|
|
94
|
+
return response.json();
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`Unexpected content type: ${contentType}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.QlsClient = QlsClient;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Type } from '@vendure/core';
|
|
2
|
+
import { AdminUiExtension } from '@vendure/ui-devkit/compiler';
|
|
3
|
+
import { QlsPluginOptions } from './types';
|
|
4
|
+
export declare class QlsPlugin {
|
|
5
|
+
static options: QlsPluginOptions;
|
|
6
|
+
static init(options: QlsPluginOptions): Type<QlsPlugin>;
|
|
7
|
+
static ui: AdminUiExtension;
|
|
8
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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 __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
var QlsPlugin_1;
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.QlsPlugin = void 0;
|
|
14
|
+
const core_1 = require("@vendure/core");
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const api_extensions_1 = require("./api/api-extensions");
|
|
17
|
+
const qls_admin_resolver_1 = require("./api/qls-admin.resolver");
|
|
18
|
+
const qls_webhooks_controller_1 = require("./api/qls-webhooks-controller");
|
|
19
|
+
const permissions_1 = require("./config/permissions");
|
|
20
|
+
const constants_1 = require("./constants");
|
|
21
|
+
const custom_fields_1 = require("./custom-fields");
|
|
22
|
+
const qls_order_service_1 = require("./services/qls-order.service");
|
|
23
|
+
const qls_product_service_1 = require("./services/qls-product.service");
|
|
24
|
+
let QlsPlugin = QlsPlugin_1 = class QlsPlugin {
|
|
25
|
+
static init(options) {
|
|
26
|
+
this.options = options;
|
|
27
|
+
return QlsPlugin_1;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
exports.QlsPlugin = QlsPlugin;
|
|
31
|
+
QlsPlugin.ui = {
|
|
32
|
+
extensionPath: path_1.default.join(__dirname, 'ui'),
|
|
33
|
+
id: 'qls-fulfillment-ui',
|
|
34
|
+
providers: ['providers.ts'],
|
|
35
|
+
};
|
|
36
|
+
exports.QlsPlugin = QlsPlugin = QlsPlugin_1 = __decorate([
|
|
37
|
+
(0, core_1.VendurePlugin)({
|
|
38
|
+
imports: [core_1.PluginCommonModule],
|
|
39
|
+
providers: [
|
|
40
|
+
{
|
|
41
|
+
provide: constants_1.PLUGIN_INIT_OPTIONS,
|
|
42
|
+
useFactory: () => QlsPlugin.options,
|
|
43
|
+
},
|
|
44
|
+
qls_product_service_1.QlsProductService,
|
|
45
|
+
qls_order_service_1.QlsOrderService,
|
|
46
|
+
],
|
|
47
|
+
controllers: [qls_webhooks_controller_1.QlsWebhooksController],
|
|
48
|
+
configuration: (config) => {
|
|
49
|
+
config.authOptions.customPermissions.push(permissions_1.qlsFullSyncPermission);
|
|
50
|
+
config.authOptions.customPermissions.push(permissions_1.qlsPushOrderPermission);
|
|
51
|
+
config.customFields.ProductVariant.push(...custom_fields_1.variantCustomFields);
|
|
52
|
+
return config;
|
|
53
|
+
},
|
|
54
|
+
compatibility: '>=3.2.0',
|
|
55
|
+
adminApiExtensions: {
|
|
56
|
+
schema: api_extensions_1.adminApiExtensions,
|
|
57
|
+
resolvers: [qls_admin_resolver_1.QlsAdminResolver],
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
], QlsPlugin);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { OnApplicationBootstrap, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { ModuleRef } from '@nestjs/core';
|
|
3
|
+
import { EventBus, ID, Job, JobQueueService, OrderService, RequestContext, TransactionalConnection } from '@vendure/core';
|
|
4
|
+
import { IncomingOrderWebhook } from '../lib/client-types';
|
|
5
|
+
import { QlsOrderJobData, QlsPluginOptions } from '../types';
|
|
6
|
+
export declare class QlsOrderService implements OnModuleInit, OnApplicationBootstrap {
|
|
7
|
+
private connection;
|
|
8
|
+
private options;
|
|
9
|
+
private jobQueueService;
|
|
10
|
+
private eventBus;
|
|
11
|
+
private orderService;
|
|
12
|
+
private moduleRef;
|
|
13
|
+
private orderJobQueue;
|
|
14
|
+
constructor(connection: TransactionalConnection, options: QlsPluginOptions, jobQueueService: JobQueueService, eventBus: EventBus, orderService: OrderService, moduleRef: ModuleRef);
|
|
15
|
+
onApplicationBootstrap(): void;
|
|
16
|
+
onModuleInit(): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Decide what kind of job it is and handle accordingly.
|
|
19
|
+
* Returns the result of the job, which will be stored in the job record.
|
|
20
|
+
*/
|
|
21
|
+
handleOrderJob(job: Job<QlsOrderJobData>): Promise<unknown>;
|
|
22
|
+
pushOrderToQls(ctx: RequestContext, orderId: ID): Promise<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Update the status of an order in QLS based on the given order code and status
|
|
25
|
+
*/
|
|
26
|
+
handleOrderStatusUpdate(ctx: RequestContext, body: IncomingOrderWebhook): Promise<void>;
|
|
27
|
+
triggerPushOrder(ctx: RequestContext, orderId: ID, orderCode?: string): Promise<Job<QlsOrderJobData> | undefined>;
|
|
28
|
+
private getVendureOrderState;
|
|
29
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.QlsOrderService = void 0;
|
|
19
|
+
const common_1 = require("@nestjs/common");
|
|
20
|
+
const core_1 = require("@nestjs/core");
|
|
21
|
+
const core_2 = require("@vendure/core");
|
|
22
|
+
const catch_unknown_1 = require("catch-unknown");
|
|
23
|
+
const util_1 = __importDefault(require("util"));
|
|
24
|
+
const constants_1 = require("../constants");
|
|
25
|
+
const qls_client_1 = require("../lib/qls-client");
|
|
26
|
+
let QlsOrderService = class QlsOrderService {
|
|
27
|
+
constructor(connection, options, jobQueueService, eventBus, orderService, moduleRef) {
|
|
28
|
+
this.connection = connection;
|
|
29
|
+
this.options = options;
|
|
30
|
+
this.jobQueueService = jobQueueService;
|
|
31
|
+
this.eventBus = eventBus;
|
|
32
|
+
this.orderService = orderService;
|
|
33
|
+
this.moduleRef = moduleRef;
|
|
34
|
+
}
|
|
35
|
+
onApplicationBootstrap() {
|
|
36
|
+
// Listen for OrderPlacedEvent and add a job to the queue
|
|
37
|
+
this.eventBus.ofType(core_2.OrderPlacedEvent).subscribe((event) => {
|
|
38
|
+
this.triggerPushOrder(event.ctx, event.order.id, event.order.code).catch((e) => {
|
|
39
|
+
const error = (0, catch_unknown_1.asError)(e);
|
|
40
|
+
core_2.Logger.error(`Failed to trigger push order job for order ${event.order.code}: ${error.message}`, constants_1.loggerCtx, error.stack);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async onModuleInit() {
|
|
45
|
+
this.orderJobQueue = await this.jobQueueService.createQueue({
|
|
46
|
+
name: 'qls-order-jobs',
|
|
47
|
+
process: (job) => {
|
|
48
|
+
return this.handleOrderJob(job);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Decide what kind of job it is and handle accordingly.
|
|
54
|
+
* Returns the result of the job, which will be stored in the job record.
|
|
55
|
+
*/
|
|
56
|
+
async handleOrderJob(job) {
|
|
57
|
+
try {
|
|
58
|
+
const ctx = core_2.RequestContext.deserialize(job.data.ctx);
|
|
59
|
+
if (job.data.action === 'push-order') {
|
|
60
|
+
return await this.pushOrderToQls(ctx, job.data.orderId);
|
|
61
|
+
}
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- According to TS this cant happen, in reality an old job with different action could be in the queue
|
|
63
|
+
throw new Error(`Unknown job action: ${job.data.action}`);
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
const error = (0, catch_unknown_1.asError)(e);
|
|
67
|
+
const dataWithoutCtx = {
|
|
68
|
+
...job.data,
|
|
69
|
+
ctx: undefined,
|
|
70
|
+
};
|
|
71
|
+
core_2.Logger.error(`Error handling job ${job.data.action}: ${error}`, constants_1.loggerCtx, util_1.default.inspect(dataWithoutCtx, false, 5));
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async pushOrderToQls(ctx, orderId) {
|
|
76
|
+
const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
|
|
77
|
+
if (!client) {
|
|
78
|
+
// Jobs are only added when QLS is enabled for the channel, so if we cant get a client here, something is wrong
|
|
79
|
+
throw new Error(`QLS not enabled for channel ${ctx.channel.token}`);
|
|
80
|
+
}
|
|
81
|
+
const order = await this.orderService.findOne(ctx, orderId);
|
|
82
|
+
if (!order) {
|
|
83
|
+
throw new Error(`No order with id ${orderId} not found`);
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
// Check if all products are available in QLS
|
|
87
|
+
const qlsProducts = order.lines.map((line) => {
|
|
88
|
+
if (!line.productVariant.customFields.qlsProductId) {
|
|
89
|
+
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
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
amount_ordered: line.quantity,
|
|
93
|
+
product_id: line.productVariant.customFields.qlsProductId,
|
|
94
|
+
name: line.productVariant.name,
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
const additionalOrderFields = await this.options.getAdditionalOrderFields?.(ctx, new core_2.Injector(this.moduleRef), order);
|
|
98
|
+
const customerName = [order.customer?.firstName, order.customer?.lastName]
|
|
99
|
+
.filter(Boolean)
|
|
100
|
+
.join(' ');
|
|
101
|
+
// Validate customer and shipping address
|
|
102
|
+
if (!order.customer) {
|
|
103
|
+
throw new Error(`Order '${order.code}' has no customer! Can not push order to QLS.`);
|
|
104
|
+
}
|
|
105
|
+
if (!order.shippingAddress) {
|
|
106
|
+
throw new Error(`Order '${order.code}' has no shipping address! Can not push order to QLS.`);
|
|
107
|
+
}
|
|
108
|
+
if (!order.shippingAddress.streetLine1 ||
|
|
109
|
+
!order.shippingAddress.postalCode ||
|
|
110
|
+
!order.shippingAddress.city ||
|
|
111
|
+
!order.shippingAddress.streetLine2 ||
|
|
112
|
+
!order.shippingAddress.countryCode) {
|
|
113
|
+
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
|
+
}
|
|
115
|
+
const qlsOrder = {
|
|
116
|
+
customer_reference: order.code,
|
|
117
|
+
processable: new Date().toISOString(), // Processable starting now
|
|
118
|
+
servicepoint_code: additionalOrderFields?.servicepoint_code,
|
|
119
|
+
delivery_options: additionalOrderFields?.delivery_options ?? [],
|
|
120
|
+
total_price: order.totalWithTax,
|
|
121
|
+
receiver_contact: {
|
|
122
|
+
name: order.shippingAddress.fullName || customerName,
|
|
123
|
+
companyname: order.shippingAddress.company,
|
|
124
|
+
street: order.shippingAddress.streetLine1,
|
|
125
|
+
housenumber: order.shippingAddress.streetLine2,
|
|
126
|
+
postalcode: order.shippingAddress.postalCode,
|
|
127
|
+
locality: order.shippingAddress.city,
|
|
128
|
+
country: order.shippingAddress.countryCode.toUpperCase(),
|
|
129
|
+
email: order.customer.emailAddress,
|
|
130
|
+
phone: order.customer.phoneNumber,
|
|
131
|
+
},
|
|
132
|
+
products: qlsProducts,
|
|
133
|
+
...(additionalOrderFields ?? {}),
|
|
134
|
+
};
|
|
135
|
+
const result = await client.createFulfillmentOrder(qlsOrder);
|
|
136
|
+
await this.orderService.addNoteToOrder(ctx, {
|
|
137
|
+
id: orderId,
|
|
138
|
+
isPublic: false,
|
|
139
|
+
note: `Created order '${result.id}' in QLS`,
|
|
140
|
+
});
|
|
141
|
+
return `Order '${order.code}' created in QLS with id '${result.id}'`;
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
const error = (0, catch_unknown_1.asError)(e);
|
|
145
|
+
await this.orderService.addNoteToOrder(ctx, {
|
|
146
|
+
id: orderId,
|
|
147
|
+
isPublic: false,
|
|
148
|
+
note: `Failed to create order '${order.code}' in QLS: ${error.message}`,
|
|
149
|
+
});
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Update the status of an order in QLS based on the given order code and status
|
|
155
|
+
*/
|
|
156
|
+
async handleOrderStatusUpdate(ctx, body) {
|
|
157
|
+
const orderCode = body.customer_reference;
|
|
158
|
+
const order = await this.orderService.findOneByCode(ctx, orderCode, []);
|
|
159
|
+
if (!order) {
|
|
160
|
+
throw new Error(`Order with code '${orderCode}' not found`);
|
|
161
|
+
}
|
|
162
|
+
const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
|
|
163
|
+
if (!client) {
|
|
164
|
+
throw new Error(`QLS not enabled for channel ${ctx.channel.token}`);
|
|
165
|
+
}
|
|
166
|
+
const vendureOrderState = this.getVendureOrderState(body);
|
|
167
|
+
if (!vendureOrderState) {
|
|
168
|
+
core_2.Logger.info(`Not handling QLS order status '${body.status}' for order '${orderCode}', because no Vendure order state found for this status`, constants_1.loggerCtx);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
await this.orderService.transitionToState(ctx, order.id, vendureOrderState);
|
|
172
|
+
core_2.Logger.info(`Successfully updated order '${orderCode}' to '${vendureOrderState}'`, constants_1.loggerCtx);
|
|
173
|
+
}
|
|
174
|
+
async triggerPushOrder(ctx, orderId, orderCode) {
|
|
175
|
+
const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
|
|
176
|
+
if (!client) {
|
|
177
|
+
// QLS not enabled for channel, so don't add a job to the queue
|
|
178
|
+
core_2.Logger.info(`QLS not enabled for channel ${ctx.channel.token}, not adding order '${orderCode ?? orderId}' job to queue`, constants_1.loggerCtx);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
return await this.orderJobQueue.add({
|
|
182
|
+
action: 'push-order',
|
|
183
|
+
ctx: ctx.serialize(),
|
|
184
|
+
orderId,
|
|
185
|
+
}, { retries: 5 });
|
|
186
|
+
}
|
|
187
|
+
getVendureOrderState(body) {
|
|
188
|
+
if (body.cancelled) {
|
|
189
|
+
return 'Cancelled';
|
|
190
|
+
}
|
|
191
|
+
if (body.amount_delivered === body.amount_total) {
|
|
192
|
+
return 'Delivered';
|
|
193
|
+
}
|
|
194
|
+
switch (body.status) {
|
|
195
|
+
case 'sent':
|
|
196
|
+
return 'Shipped';
|
|
197
|
+
case 'partically_sent':
|
|
198
|
+
return 'PartiallyShipped';
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
exports.QlsOrderService = QlsOrderService;
|
|
203
|
+
exports.QlsOrderService = QlsOrderService = __decorate([
|
|
204
|
+
(0, common_1.Injectable)(),
|
|
205
|
+
__param(1, (0, common_1.Inject)(constants_1.PLUGIN_INIT_OPTIONS)),
|
|
206
|
+
__metadata("design:paramtypes", [core_2.TransactionalConnection, Object, core_2.JobQueueService,
|
|
207
|
+
core_2.EventBus,
|
|
208
|
+
core_2.OrderService,
|
|
209
|
+
core_1.ModuleRef])
|
|
210
|
+
], QlsOrderService);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { OnApplicationBootstrap, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { EventBus, ID, Job, JobQueueService, ProductVariant, ProductVariantService, RequestContext, StockLevelService, StockLocationService, TransactionalConnection } from '@vendure/core';
|
|
3
|
+
import { QlsClient } from '../lib/qls-client';
|
|
4
|
+
import { QlsPluginOptions, QlsProductJobData } from '../types';
|
|
5
|
+
import { FulfillmentProduct } from '../lib/client-types';
|
|
6
|
+
type SyncProductsJobResult = {
|
|
7
|
+
updatedInQls: number;
|
|
8
|
+
createdInQls: number;
|
|
9
|
+
updatedStock: number;
|
|
10
|
+
};
|
|
11
|
+
export declare class QlsProductService implements OnModuleInit, OnApplicationBootstrap {
|
|
12
|
+
private connection;
|
|
13
|
+
private options;
|
|
14
|
+
private jobQueueService;
|
|
15
|
+
private stockLevelService;
|
|
16
|
+
private readonly variantService;
|
|
17
|
+
private readonly stockLocationService;
|
|
18
|
+
private readonly eventBus;
|
|
19
|
+
private productJobQueue;
|
|
20
|
+
constructor(connection: TransactionalConnection, options: QlsPluginOptions, jobQueueService: JobQueueService, stockLevelService: StockLevelService, variantService: ProductVariantService, stockLocationService: StockLocationService, eventBus: EventBus);
|
|
21
|
+
onApplicationBootstrap(): void;
|
|
22
|
+
onModuleInit(): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Decide what kind of job it is and handle accordingly.
|
|
25
|
+
* Returns the result of the job, which will be stored in the job record.
|
|
26
|
+
*/
|
|
27
|
+
handleProductJob(job: Job<QlsProductJobData>): Promise<unknown>;
|
|
28
|
+
/**
|
|
29
|
+
* Create fulfillment products in QLS for all product variants (full push)
|
|
30
|
+
* 1. Fetches all products from QLS
|
|
31
|
+
* 2. Updates stock levels in Vendure based on the QLS products
|
|
32
|
+
* 3. Creates products in QLS if needed
|
|
33
|
+
* 4. Updates products in QLS if needed
|
|
34
|
+
*/
|
|
35
|
+
runFullSync(ctx: RequestContext): Promise<SyncProductsJobResult>;
|
|
36
|
+
/**
|
|
37
|
+
* Creates or updates the fulfillment products in QLS for the given product variants.
|
|
38
|
+
*/
|
|
39
|
+
syncVariants(ctx: RequestContext, productVariantIds: ID[]): Promise<SyncProductsJobResult>;
|
|
40
|
+
/**
|
|
41
|
+
* Trigger a full product sync job
|
|
42
|
+
*/
|
|
43
|
+
triggerFullSync(ctx: RequestContext): Promise<import("@vendure/core/dist/job-queue/subscribable-job").SubscribableJob<QlsProductJobData>>;
|
|
44
|
+
/**
|
|
45
|
+
* Trigger a product sync job for particular product variants
|
|
46
|
+
*/
|
|
47
|
+
triggerSyncVariants(ctx: RequestContext, productVariantIds: ID[]): Promise<import("@vendure/core/dist/job-queue/subscribable-job").SubscribableJob<QlsProductJobData> | undefined>;
|
|
48
|
+
/**
|
|
49
|
+
* Update the stock level for a variant based on the given available stock
|
|
50
|
+
*/
|
|
51
|
+
updateStockBySku(ctx: RequestContext, sku: string, availableStock: number): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
|
|
54
|
+
*/
|
|
55
|
+
createOrUpdateProductInQls(ctx: RequestContext, client: QlsClient, variant: ProductVariant, existingProduct: FulfillmentProduct | null): Promise<'created' | 'updated' | 'not-changed'>;
|
|
56
|
+
/**
|
|
57
|
+
* Determine if a product needs to be updated in QLS based on the given variant and QLS product.
|
|
58
|
+
*/
|
|
59
|
+
private shouldUpdateProductInQls;
|
|
60
|
+
/**
|
|
61
|
+
* Get all variants for the current channel in batches
|
|
62
|
+
*/
|
|
63
|
+
private getAllVariants;
|
|
64
|
+
/**
|
|
65
|
+
* Update stock level for a variant based on the given available stock
|
|
66
|
+
*/
|
|
67
|
+
private updateStock;
|
|
68
|
+
}
|
|
69
|
+
export {};
|