@pinelab/vendure-plugin-qls-fulfillment 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +3 -1
- package/dist/api/api-extensions.js +7 -0
- package/dist/api/generated/graphql.d.ts +5 -0
- package/dist/api/qls-admin.resolver.d.ts +3 -2
- package/dist/api/qls-admin.resolver.js +18 -3
- package/dist/config/sync-products-scheduled-task.js +12 -6
- package/dist/custom-fields.d.ts +2 -2
- package/dist/custom-fields.js +16 -34
- package/dist/entities/qls-order-entity.entity.d.ts +9 -0
- package/dist/entities/qls-order-entity.entity.js +35 -0
- package/dist/qls-plugin.js +4 -1
- package/dist/services/qls-order.service.d.ts +10 -3
- package/dist/services/qls-order.service.js +57 -15
- package/dist/services/qls-product.service.js +18 -13
- package/dist/services/qls-variant-sync-failed-event.d.ts +17 -0
- package/dist/services/qls-variant-sync-failed-event.js +21 -0
- package/dist/types.d.ts +5 -0
- package/dist/ui/providers.ts +39 -6
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
# 1.2.0 (2026-01-28)
|
|
2
|
+
|
|
3
|
+
- Prevent accidently pushing orders multiple times by checking if the order is already synced to QLS.
|
|
4
|
+
- Store combination of QLS order id and Vendure order id to prevent duplicate orders in QLS.
|
|
5
|
+
- Emit event for failed product pushes, and log it as a warning instead of an error.
|
|
6
|
+
- Store failed products as scheduled task data so they can be viewed in the Admin UI.
|
|
7
|
+
- Allow specifying UI tab name where the QLS product ID custom field is shown on ProductVariant page.
|
|
8
|
+
|
|
9
|
+
# 1.1.3 (2026-01-28)
|
|
10
|
+
|
|
11
|
+
- Gracefully handle missing variants from incoming webhooks by logging instead of throwing an error.
|
|
12
|
+
|
|
1
13
|
# 1.1.2 (2026-01-14)
|
|
2
14
|
|
|
3
15
|
- Run scheduled full sync without job queue, because scheduled tasks already run in the worker only.
|
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ mutation {
|
|
|
78
78
|
}
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
## Monitoring failed orders
|
|
81
|
+
## Monitoring failed orders and product syncs
|
|
82
82
|
|
|
83
83
|
Whenever an order fails to be pushed to QLS, an event is emitted. You can listen to this event to monitor failed orders.
|
|
84
84
|
|
|
@@ -100,6 +100,8 @@ this.eventBus.ofType(QlsOrderFailedEvent).subscribe((event) => {
|
|
|
100
100
|
});
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
+
For product syncs, the same approach can be used, but with the `QlsProductSyncFailedEvent` instead.
|
|
104
|
+
|
|
103
105
|
## Manually pushing orders to QLS
|
|
104
106
|
|
|
105
107
|
This plugin adds a button `push to QLS` to the order detail page in the Admin UI. This will push the order to QLS again. If the order has been pushed before, you need to uncheck the checkbox `synced to QLS` in the order custom fields first.
|
|
@@ -6,6 +6,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
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
|
+
extend type Order {
|
|
10
|
+
"""
|
|
11
|
+
QLS order id(s) for this Vendure order.
|
|
12
|
+
"""
|
|
13
|
+
qlsOrderIds: [String!]!
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
extend type Mutation {
|
|
10
17
|
"""
|
|
11
18
|
Trigger a sync to create or update all products in Vendure to QLS, and pull in stock levels from QLS.
|
|
@@ -29,6 +29,11 @@ export type Mutation = {
|
|
|
29
29
|
export type MutationPushOrderToQlsArgs = {
|
|
30
30
|
orderId: Scalars['ID'];
|
|
31
31
|
};
|
|
32
|
+
export type Order = {
|
|
33
|
+
__typename?: 'Order';
|
|
34
|
+
/** QLS order id(s) for this Vendure order. */
|
|
35
|
+
qlsOrderIds: Array<Scalars['String']>;
|
|
36
|
+
};
|
|
32
37
|
export type QlsServicePoint = {
|
|
33
38
|
__typename?: 'QlsServicePoint';
|
|
34
39
|
address: QlsServicePointAddress;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { RequestContext } from '@vendure/core';
|
|
1
|
+
import { Order, RequestContext } from '@vendure/core';
|
|
2
|
+
import { QlsOrderService } from '../services/qls-order.service';
|
|
2
3
|
import { QlsProductService } from '../services/qls-product.service';
|
|
3
4
|
import { MutationPushOrderToQlsArgs } from './generated/graphql';
|
|
4
|
-
import { QlsOrderService } from '../services/qls-order.service';
|
|
5
5
|
export declare class QlsAdminResolver {
|
|
6
6
|
private qlsProductService;
|
|
7
7
|
private qlsOrderService;
|
|
8
8
|
constructor(qlsProductService: QlsProductService, qlsOrderService: QlsOrderService);
|
|
9
|
+
qlsOrderIds(ctx: RequestContext, order: Order): Promise<string[]>;
|
|
9
10
|
triggerQlsProductSync(ctx: RequestContext): Promise<boolean>;
|
|
10
11
|
pushOrderToQls(ctx: RequestContext, input: MutationPushOrderToQlsArgs): Promise<string>;
|
|
11
12
|
}
|
|
@@ -15,23 +15,38 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
exports.QlsAdminResolver = void 0;
|
|
16
16
|
const graphql_1 = require("@nestjs/graphql");
|
|
17
17
|
const core_1 = require("@vendure/core");
|
|
18
|
-
const qls_product_service_1 = require("../services/qls-product.service");
|
|
19
|
-
const qls_order_service_1 = require("../services/qls-order.service");
|
|
20
18
|
const permissions_1 = require("../config/permissions");
|
|
19
|
+
const qls_order_service_1 = require("../services/qls-order.service");
|
|
20
|
+
const qls_product_service_1 = require("../services/qls-product.service");
|
|
21
21
|
let QlsAdminResolver = class QlsAdminResolver {
|
|
22
22
|
constructor(qlsProductService, qlsOrderService) {
|
|
23
23
|
this.qlsProductService = qlsProductService;
|
|
24
24
|
this.qlsOrderService = qlsOrderService;
|
|
25
25
|
}
|
|
26
|
+
async qlsOrderIds(ctx, order) {
|
|
27
|
+
return this.qlsOrderService.getQlsOrderIdsForOrder(ctx, order.id);
|
|
28
|
+
}
|
|
26
29
|
async triggerQlsProductSync(ctx) {
|
|
27
30
|
await this.qlsProductService.triggerFullSync(ctx);
|
|
28
31
|
return true;
|
|
29
32
|
}
|
|
30
33
|
async pushOrderToQls(ctx, input) {
|
|
31
|
-
return await this.qlsOrderService.pushOrderToQls(ctx, input.orderId);
|
|
34
|
+
return await this.qlsOrderService.pushOrderToQls(ctx, input.orderId, true);
|
|
32
35
|
}
|
|
33
36
|
};
|
|
34
37
|
exports.QlsAdminResolver = QlsAdminResolver;
|
|
38
|
+
__decorate([
|
|
39
|
+
(0, graphql_1.ResolveField)(),
|
|
40
|
+
(0, graphql_1.Resolver)('Order'),
|
|
41
|
+
(0, core_1.Allow)(permissions_1.qlsPushOrderPermission.Permission),
|
|
42
|
+
(0, core_1.Allow)(core_1.Permission.UpdateAdministrator),
|
|
43
|
+
__param(0, (0, core_1.Ctx)()),
|
|
44
|
+
__param(1, (0, graphql_1.Parent)()),
|
|
45
|
+
__metadata("design:type", Function),
|
|
46
|
+
__metadata("design:paramtypes", [core_1.RequestContext,
|
|
47
|
+
core_1.Order]),
|
|
48
|
+
__metadata("design:returntype", Promise)
|
|
49
|
+
], QlsAdminResolver.prototype, "qlsOrderIds", null);
|
|
35
50
|
__decorate([
|
|
36
51
|
(0, graphql_1.Mutation)(),
|
|
37
52
|
(0, core_1.Transaction)(),
|
|
@@ -19,7 +19,12 @@ exports.qlsSyncAllProductsTask = new core_1.ScheduledTask({
|
|
|
19
19
|
apiType: 'admin',
|
|
20
20
|
});
|
|
21
21
|
const channels = await injector.get(core_1.ChannelService).findAll(ctx);
|
|
22
|
-
|
|
22
|
+
const aggregatedResult = {
|
|
23
|
+
updatedInQls: 0,
|
|
24
|
+
createdInQls: 0,
|
|
25
|
+
updatedStock: 0,
|
|
26
|
+
failed: 0,
|
|
27
|
+
};
|
|
23
28
|
for (const channel of channels.items) {
|
|
24
29
|
// Create ctx for channel
|
|
25
30
|
const channelCtx = new core_1.RequestContext({
|
|
@@ -35,11 +40,12 @@ exports.qlsSyncAllProductsTask = new core_1.ScheduledTask({
|
|
|
35
40
|
core_1.Logger.info(`QLS not enabled for channel ${channel.token}`, constants_1.loggerCtx);
|
|
36
41
|
continue;
|
|
37
42
|
}
|
|
38
|
-
await qlsProductService.runFullSync(channelCtx);
|
|
39
|
-
|
|
43
|
+
const result = await qlsProductService.runFullSync(channelCtx);
|
|
44
|
+
aggregatedResult.updatedInQls += result.updatedInQls.length;
|
|
45
|
+
aggregatedResult.createdInQls += result.createdInQls.length;
|
|
46
|
+
aggregatedResult.updatedStock += result.updatedStock.length;
|
|
47
|
+
aggregatedResult.failed += result.failed.length;
|
|
40
48
|
}
|
|
41
|
-
return
|
|
42
|
-
message: `Finnished full sync for ${fullSyncCompleted} channels`,
|
|
43
|
-
};
|
|
49
|
+
return aggregatedResult;
|
|
44
50
|
},
|
|
45
51
|
});
|
package/dist/custom-fields.d.ts
CHANGED
|
@@ -6,8 +6,8 @@ declare module '@vendure/core' {
|
|
|
6
6
|
interface CustomOrderFields {
|
|
7
7
|
qlsServicePointId?: string;
|
|
8
8
|
qlsServicePointDetails?: string;
|
|
9
|
-
syncedToQls?: boolean;
|
|
10
9
|
}
|
|
11
10
|
}
|
|
12
|
-
|
|
11
|
+
/** Returns variant custom fields with the given Admin UI tab name. */
|
|
12
|
+
export declare function getVariantCustomFields(uiTab: string): CustomFieldConfig[];
|
|
13
13
|
export declare const orderCustomFields: CustomFieldConfig[];
|
package/dist/custom-fields.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.orderCustomFields =
|
|
3
|
+
exports.orderCustomFields = void 0;
|
|
4
|
+
exports.getVariantCustomFields = getVariantCustomFields;
|
|
4
5
|
const core_1 = require("@vendure/core");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
/** Returns variant custom fields with the given Admin UI tab name. */
|
|
7
|
+
function getVariantCustomFields(uiTab) {
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
name: 'qlsProductId',
|
|
11
|
+
type: 'string',
|
|
12
|
+
label: [{ value: 'QLS Product ID', languageCode: core_1.LanguageCode.en }],
|
|
13
|
+
nullable: true,
|
|
14
|
+
public: false,
|
|
15
|
+
readonly: true,
|
|
16
|
+
ui: { tab: uiTab },
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
}
|
|
16
20
|
exports.orderCustomFields = [
|
|
17
21
|
{
|
|
18
22
|
name: 'qlsServicePointId',
|
|
@@ -23,28 +27,6 @@ exports.orderCustomFields = [
|
|
|
23
27
|
readonly: false,
|
|
24
28
|
ui: { tab: 'QLS' },
|
|
25
29
|
},
|
|
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
30
|
{
|
|
49
31
|
name: 'qlsServicePointDetails',
|
|
50
32
|
type: 'string',
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { DeepPartial, ID, VendureEntity } from '@vendure/core';
|
|
2
|
+
/**
|
|
3
|
+
* Simple entity to keep track of what QLS orders were created for what Vendure orders.
|
|
4
|
+
*/
|
|
5
|
+
export declare class QlsOrderEntity extends VendureEntity {
|
|
6
|
+
constructor(input?: DeepPartial<QlsOrderEntity>);
|
|
7
|
+
qlsOrderId: ID;
|
|
8
|
+
vendureOrderId: ID;
|
|
9
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.QlsOrderEntity = void 0;
|
|
13
|
+
const core_1 = require("@vendure/core");
|
|
14
|
+
const typeorm_1 = require("typeorm");
|
|
15
|
+
/**
|
|
16
|
+
* Simple entity to keep track of what QLS orders were created for what Vendure orders.
|
|
17
|
+
*/
|
|
18
|
+
let QlsOrderEntity = class QlsOrderEntity extends core_1.VendureEntity {
|
|
19
|
+
constructor(input) {
|
|
20
|
+
super(input);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
exports.QlsOrderEntity = QlsOrderEntity;
|
|
24
|
+
__decorate([
|
|
25
|
+
(0, typeorm_1.Column)({ type: 'text', nullable: false, unique: true }),
|
|
26
|
+
__metadata("design:type", Object)
|
|
27
|
+
], QlsOrderEntity.prototype, "qlsOrderId", void 0);
|
|
28
|
+
__decorate([
|
|
29
|
+
(0, typeorm_1.Column)({ type: 'text', nullable: false }),
|
|
30
|
+
__metadata("design:type", Object)
|
|
31
|
+
], QlsOrderEntity.prototype, "vendureOrderId", void 0);
|
|
32
|
+
exports.QlsOrderEntity = QlsOrderEntity = __decorate([
|
|
33
|
+
(0, typeorm_1.Entity)(),
|
|
34
|
+
__metadata("design:paramtypes", [Object])
|
|
35
|
+
], QlsOrderEntity);
|
package/dist/qls-plugin.js
CHANGED
|
@@ -22,11 +22,13 @@ const constants_1 = require("./constants");
|
|
|
22
22
|
const custom_fields_1 = require("./custom-fields");
|
|
23
23
|
const qls_order_service_1 = require("./services/qls-order.service");
|
|
24
24
|
const qls_product_service_1 = require("./services/qls-product.service");
|
|
25
|
+
const qls_order_entity_entity_1 = require("./entities/qls-order-entity.entity");
|
|
25
26
|
let QlsPlugin = QlsPlugin_1 = class QlsPlugin {
|
|
26
27
|
static init(options) {
|
|
27
28
|
this.options = {
|
|
28
29
|
synchronizeStockLevels: true,
|
|
29
30
|
autoPushOrders: true,
|
|
31
|
+
qlsProductIdUiTab: 'QLS',
|
|
30
32
|
...options,
|
|
31
33
|
};
|
|
32
34
|
return QlsPlugin_1;
|
|
@@ -53,7 +55,7 @@ exports.QlsPlugin = QlsPlugin = QlsPlugin_1 = __decorate([
|
|
|
53
55
|
configuration: (config) => {
|
|
54
56
|
config.authOptions.customPermissions.push(permissions_1.qlsFullSyncPermission);
|
|
55
57
|
config.authOptions.customPermissions.push(permissions_1.qlsPushOrderPermission);
|
|
56
|
-
config.customFields.ProductVariant.push(...custom_fields_1.
|
|
58
|
+
config.customFields.ProductVariant.push(...(0, custom_fields_1.getVariantCustomFields)(QlsPlugin.options?.qlsProductIdUiTab ?? 'QLS'));
|
|
57
59
|
config.customFields.Order.push(...custom_fields_1.orderCustomFields);
|
|
58
60
|
return config;
|
|
59
61
|
},
|
|
@@ -66,5 +68,6 @@ exports.QlsPlugin = QlsPlugin = QlsPlugin_1 = __decorate([
|
|
|
66
68
|
schema: api_extensions_1.shopApiExtensions,
|
|
67
69
|
resolvers: [qls_shop_resolver_1.QlsShopResolver],
|
|
68
70
|
},
|
|
71
|
+
entities: [qls_order_entity_entity_1.QlsOrderEntity],
|
|
69
72
|
})
|
|
70
73
|
], QlsPlugin);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { OnApplicationBootstrap, OnModuleInit } from '@nestjs/common';
|
|
2
2
|
import { ModuleRef } from '@nestjs/core';
|
|
3
|
-
import { EventBus, ID, Job, JobQueueService, OrderService, RequestContext } from '@vendure/core';
|
|
3
|
+
import { EventBus, ID, Job, JobQueueService, OrderService, RequestContext, TransactionalConnection } from '@vendure/core';
|
|
4
4
|
import { QlsServicePoint, QlsServicePointSearchInput } from '../api/generated/graphql';
|
|
5
5
|
import { IncomingOrderWebhook } from '../lib/client-types';
|
|
6
6
|
import { QlsOrderJobData, QlsPluginOptions } from '../types';
|
|
@@ -10,8 +10,9 @@ export declare class QlsOrderService implements OnModuleInit, OnApplicationBoots
|
|
|
10
10
|
private eventBus;
|
|
11
11
|
private orderService;
|
|
12
12
|
private moduleRef;
|
|
13
|
+
private readonly connection;
|
|
13
14
|
private orderJobQueue;
|
|
14
|
-
constructor(options: QlsPluginOptions, jobQueueService: JobQueueService, eventBus: EventBus, orderService: OrderService, moduleRef: ModuleRef);
|
|
15
|
+
constructor(options: QlsPluginOptions, jobQueueService: JobQueueService, eventBus: EventBus, orderService: OrderService, moduleRef: ModuleRef, connection: TransactionalConnection);
|
|
15
16
|
onApplicationBootstrap(): void;
|
|
16
17
|
onModuleInit(): Promise<void>;
|
|
17
18
|
/**
|
|
@@ -22,13 +23,19 @@ export declare class QlsOrderService implements OnModuleInit, OnApplicationBoots
|
|
|
22
23
|
/**
|
|
23
24
|
* Push an order to QLS by id.
|
|
24
25
|
* Returns a human-readable message describing the result of the operation (Used as job result).
|
|
26
|
+
*
|
|
27
|
+
* `force` can be used to force the push of an order even if one already exists in QLS.
|
|
25
28
|
*/
|
|
26
|
-
pushOrderToQls(ctx: RequestContext, orderId: ID): Promise<string>;
|
|
29
|
+
pushOrderToQls(ctx: RequestContext, orderId: ID, force?: boolean): Promise<string>;
|
|
27
30
|
/**
|
|
28
31
|
* Update the status of an order in QLS based on the given order code and status
|
|
29
32
|
*/
|
|
30
33
|
handleOrderStatusUpdate(ctx: RequestContext, body: IncomingOrderWebhook): Promise<void>;
|
|
31
34
|
triggerPushOrder(ctx: RequestContext, orderId: ID, orderCode?: string): Promise<Job<QlsOrderJobData> | undefined>;
|
|
35
|
+
/**
|
|
36
|
+
* Get QLS order id(s) for a Vendure order (for Order.qlsOrderIds field).
|
|
37
|
+
*/
|
|
38
|
+
getQlsOrderIdsForOrder(ctx: RequestContext, orderId: ID): Promise<string[]>;
|
|
32
39
|
getServicePoints(ctx: RequestContext, input: QlsServicePointSearchInput): Promise<QlsServicePoint[]>;
|
|
33
40
|
private getVendureOrderState;
|
|
34
41
|
}
|
|
@@ -24,13 +24,15 @@ 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 qls_order_failed_event_1 = require("./qls-order-failed-event");
|
|
27
|
+
const qls_order_entity_entity_1 = require("../entities/qls-order-entity.entity");
|
|
27
28
|
let QlsOrderService = class QlsOrderService {
|
|
28
|
-
constructor(options, jobQueueService, eventBus, orderService, moduleRef) {
|
|
29
|
+
constructor(options, jobQueueService, eventBus, orderService, moduleRef, connection) {
|
|
29
30
|
this.options = options;
|
|
30
31
|
this.jobQueueService = jobQueueService;
|
|
31
32
|
this.eventBus = eventBus;
|
|
32
33
|
this.orderService = orderService;
|
|
33
34
|
this.moduleRef = moduleRef;
|
|
35
|
+
this.connection = connection;
|
|
34
36
|
}
|
|
35
37
|
onApplicationBootstrap() {
|
|
36
38
|
// Listen for OrderPlacedEvent and add a job to the queue
|
|
@@ -79,8 +81,10 @@ let QlsOrderService = class QlsOrderService {
|
|
|
79
81
|
/**
|
|
80
82
|
* Push an order to QLS by id.
|
|
81
83
|
* Returns a human-readable message describing the result of the operation (Used as job result).
|
|
84
|
+
*
|
|
85
|
+
* `force` can be used to force the push of an order even if one already exists in QLS.
|
|
82
86
|
*/
|
|
83
|
-
async pushOrderToQls(ctx, orderId) {
|
|
87
|
+
async pushOrderToQls(ctx, orderId, force = false) {
|
|
84
88
|
const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
|
|
85
89
|
if (!client) {
|
|
86
90
|
// Jobs are only added when QLS is enabled for the channel, so if we cant get a client here, something is wrong
|
|
@@ -90,8 +94,17 @@ let QlsOrderService = class QlsOrderService {
|
|
|
90
94
|
if (!order) {
|
|
91
95
|
throw new Error(`No order with id ${orderId} not found`);
|
|
92
96
|
}
|
|
93
|
-
if (
|
|
94
|
-
|
|
97
|
+
if (!force) {
|
|
98
|
+
const existingQlsOrder = await this.connection
|
|
99
|
+
.getRepository(ctx, qls_order_entity_entity_1.QlsOrderEntity)
|
|
100
|
+
.findOne({
|
|
101
|
+
where: {
|
|
102
|
+
vendureOrderId: orderId,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
if (existingQlsOrder) {
|
|
106
|
+
throw new core_2.UserInputError(`Order '${order.code}' has already been synced to QLS`);
|
|
107
|
+
}
|
|
95
108
|
}
|
|
96
109
|
try {
|
|
97
110
|
// Map variants to QLS products
|
|
@@ -159,32 +172,49 @@ let QlsOrderService = class QlsOrderService {
|
|
|
159
172
|
};
|
|
160
173
|
const result = await client.createFulfillmentOrder(qlsOrder);
|
|
161
174
|
core_2.Logger.info(`Successfully created order '${order.code}' in QLS with id '${result.id}'`, constants_1.loggerCtx);
|
|
162
|
-
|
|
175
|
+
// Add note but catch any errors, because we don't want the job to fail and retry when adding a note fails
|
|
176
|
+
await this.orderService
|
|
177
|
+
.addNoteToOrder(ctx, {
|
|
163
178
|
id: orderId,
|
|
164
179
|
isPublic: false,
|
|
165
180
|
note: `Created order '${result.id}' in QLS`,
|
|
181
|
+
})
|
|
182
|
+
.catch((e) => {
|
|
183
|
+
const error = (0, catch_unknown_1.asError)(e);
|
|
184
|
+
core_2.Logger.error(`Error adding note to order '${order.code}': ${error.message}`, constants_1.loggerCtx, error.stack);
|
|
166
185
|
});
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
.
|
|
171
|
-
|
|
186
|
+
await this.connection
|
|
187
|
+
.getRepository(ctx, qls_order_entity_entity_1.QlsOrderEntity)
|
|
188
|
+
.save({
|
|
189
|
+
qlsOrderId: result.id,
|
|
190
|
+
vendureOrderId: orderId,
|
|
172
191
|
})
|
|
173
192
|
.catch((e) => {
|
|
174
|
-
//
|
|
193
|
+
// Catch any errors, because we don't want the job to fail and retry when custom field update fails
|
|
175
194
|
const error = (0, catch_unknown_1.asError)(e);
|
|
176
|
-
core_2.Logger.error(`Error
|
|
195
|
+
core_2.Logger.error(`Error saving QLS order entity for order '${order.code}': ${error.message}`, constants_1.loggerCtx, error.stack);
|
|
177
196
|
});
|
|
178
197
|
return `Order '${order.code}' created in QLS with id '${result.id}'`;
|
|
179
198
|
}
|
|
180
199
|
catch (e) {
|
|
181
200
|
const error = (0, catch_unknown_1.asError)(e);
|
|
182
|
-
await this.orderService
|
|
201
|
+
await this.orderService
|
|
202
|
+
.addNoteToOrder(ctx, {
|
|
183
203
|
id: orderId,
|
|
184
204
|
isPublic: false,
|
|
185
205
|
note: `Failed to create order '${order.code}' in QLS: ${error.message}`,
|
|
206
|
+
})
|
|
207
|
+
.catch((e) => {
|
|
208
|
+
const error = (0, catch_unknown_1.asError)(e);
|
|
209
|
+
core_2.Logger.error(`Error adding note to order '${order.code}': ${error.message}`, constants_1.loggerCtx, error.stack);
|
|
210
|
+
});
|
|
211
|
+
await this.eventBus
|
|
212
|
+
.publish(new qls_order_failed_event_1.QlsOrderFailedEvent(ctx, order, new Date(), error.message))
|
|
213
|
+
.catch((e) => {
|
|
214
|
+
// Don't swallow original error, so catch and log this one
|
|
215
|
+
const error = (0, catch_unknown_1.asError)(e);
|
|
216
|
+
core_2.Logger.error(`Error publishing QlsOrderFailedEvent for order '${order.code}': ${error.message}`, constants_1.loggerCtx, error.stack);
|
|
186
217
|
});
|
|
187
|
-
await this.eventBus.publish(new qls_order_failed_event_1.QlsOrderFailedEvent(ctx, order, new Date(), error.message));
|
|
188
218
|
throw error;
|
|
189
219
|
}
|
|
190
220
|
}
|
|
@@ -223,6 +253,17 @@ let QlsOrderService = class QlsOrderService {
|
|
|
223
253
|
orderId,
|
|
224
254
|
}, { retries: 3 });
|
|
225
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Get QLS order id(s) for a Vendure order (for Order.qlsOrderIds field).
|
|
258
|
+
*/
|
|
259
|
+
async getQlsOrderIdsForOrder(ctx, orderId) {
|
|
260
|
+
const entities = await this.connection
|
|
261
|
+
.getRepository(ctx, qls_order_entity_entity_1.QlsOrderEntity)
|
|
262
|
+
.find({
|
|
263
|
+
where: { vendureOrderId: orderId },
|
|
264
|
+
});
|
|
265
|
+
return entities.map((e) => String(e.qlsOrderId));
|
|
266
|
+
}
|
|
226
267
|
async getServicePoints(ctx, input) {
|
|
227
268
|
const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
|
|
228
269
|
if (!client) {
|
|
@@ -252,5 +293,6 @@ exports.QlsOrderService = QlsOrderService = __decorate([
|
|
|
252
293
|
__metadata("design:paramtypes", [Object, core_2.JobQueueService,
|
|
253
294
|
core_2.EventBus,
|
|
254
295
|
core_2.OrderService,
|
|
255
|
-
core_1.ModuleRef
|
|
296
|
+
core_1.ModuleRef,
|
|
297
|
+
core_2.TransactionalConnection])
|
|
256
298
|
], QlsOrderService);
|
|
@@ -24,6 +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
|
const util_2 = require("./util");
|
|
27
|
+
const qls_variant_sync_failed_event_1 = require("./qls-variant-sync-failed-event");
|
|
27
28
|
const core_2 = require("@nestjs/core");
|
|
28
29
|
// Wait for 700ms to avoid rate limit of 500/5 minutes
|
|
29
30
|
const waitToPreventRateLimit = () => new Promise((resolve) => setTimeout(resolve, 700));
|
|
@@ -141,6 +142,7 @@ let QlsProductService = class QlsProductService {
|
|
|
141
142
|
const error = (0, catch_unknown_1.asError)(e);
|
|
142
143
|
core_1.Logger.error(`Error creating or updating variant '${variant.sku}' in QLS: ${error.message}`, constants_1.loggerCtx, error.stack);
|
|
143
144
|
failed.push(variant);
|
|
145
|
+
await this.eventBus.publish(new qls_variant_sync_failed_event_1.QlsVariantSyncFailedEvent(ctx, variant, new Date(), e));
|
|
144
146
|
await waitToPreventRateLimit();
|
|
145
147
|
}
|
|
146
148
|
}
|
|
@@ -196,17 +198,18 @@ let QlsProductService = class QlsProductService {
|
|
|
196
198
|
const createdInQls = [];
|
|
197
199
|
const failed = [];
|
|
198
200
|
for (const variantId of productVariantIds) {
|
|
201
|
+
const variant = await this.variantService.findOne(ctx, variantId, [
|
|
202
|
+
'featuredAsset',
|
|
203
|
+
'taxCategory',
|
|
204
|
+
'channels',
|
|
205
|
+
'product.featuredAsset',
|
|
206
|
+
]);
|
|
207
|
+
if (!variant) {
|
|
208
|
+
// Can happen when a variant is deleted from Vendure after the job was triggered
|
|
209
|
+
core_1.Logger.info(`Variant with id ${variantId} not found. Not creating or updating product in QLS.`, constants_1.loggerCtx);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
199
212
|
try {
|
|
200
|
-
const variant = await this.variantService.findOne(ctx, variantId, [
|
|
201
|
-
'featuredAsset',
|
|
202
|
-
'taxCategory',
|
|
203
|
-
'channels',
|
|
204
|
-
'product.featuredAsset',
|
|
205
|
-
]);
|
|
206
|
-
if (!variant) {
|
|
207
|
-
core_1.Logger.error(`Variant with id ${variantId} not found. Not creating or updating product in QLS.`, constants_1.loggerCtx);
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
213
|
const existingQlsProduct = await client.getFulfillmentProductBySku(variant.sku);
|
|
211
214
|
const result = await this.createOrUpdateProductInQls(ctx, client, variant, existingQlsProduct ?? null);
|
|
212
215
|
if (result === 'created') {
|
|
@@ -218,8 +221,10 @@ let QlsProductService = class QlsProductService {
|
|
|
218
221
|
}
|
|
219
222
|
catch (e) {
|
|
220
223
|
const error = (0, catch_unknown_1.asError)(e);
|
|
221
|
-
|
|
224
|
+
// Log as warning, because this is probably a functional mistake, i.e. duplicate barcodes or EANs
|
|
225
|
+
core_1.Logger.warn(`Error syncing variant ${variantId} (${variant.sku}) to QLS: ${error.message}`, constants_1.loggerCtx);
|
|
222
226
|
failed.push({ id: variantId });
|
|
227
|
+
await this.eventBus.publish(new qls_variant_sync_failed_event_1.QlsVariantSyncFailedEvent(ctx, variant, new Date(), e));
|
|
223
228
|
}
|
|
224
229
|
}
|
|
225
230
|
return {
|
|
@@ -261,13 +266,13 @@ let QlsProductService = class QlsProductService {
|
|
|
261
266
|
filter: { sku: { eq: sku } },
|
|
262
267
|
});
|
|
263
268
|
if (!result.items.length) {
|
|
264
|
-
|
|
269
|
+
return core_1.Logger.info(`Variant with sku '${sku}' not found, not updating stock`, constants_1.loggerCtx);
|
|
265
270
|
}
|
|
266
271
|
const variant = result.items[0];
|
|
267
272
|
if (result.items.length > 1) {
|
|
268
273
|
core_1.Logger.error(`Multiple variants found for sku '${sku}', using '${variant.id}'`, constants_1.loggerCtx);
|
|
269
274
|
}
|
|
270
|
-
|
|
275
|
+
await this.updateStock(ctx, variant.id, availableStock);
|
|
271
276
|
}
|
|
272
277
|
/**
|
|
273
278
|
* Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ProductVariant, RequestContext, VendureEvent } from '@vendure/core';
|
|
2
|
+
/**
|
|
3
|
+
* Emitted when a variant failed to sync to QLS.
|
|
4
|
+
*/
|
|
5
|
+
export declare class QlsVariantSyncFailedEvent extends VendureEvent {
|
|
6
|
+
ctx: RequestContext;
|
|
7
|
+
/** The variant that failed to sync (at least id, may be full variant). */
|
|
8
|
+
variant: Partial<ProductVariant>;
|
|
9
|
+
failedAt: Date;
|
|
10
|
+
/** The error that caused the sync to fail. */
|
|
11
|
+
fullError: unknown;
|
|
12
|
+
constructor(ctx: RequestContext,
|
|
13
|
+
/** The variant that failed to sync (at least id, may be full variant). */
|
|
14
|
+
variant: Partial<ProductVariant>, failedAt: Date,
|
|
15
|
+
/** The error that caused the sync to fail. */
|
|
16
|
+
fullError: unknown);
|
|
17
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QlsVariantSyncFailedEvent = void 0;
|
|
4
|
+
const core_1 = require("@vendure/core");
|
|
5
|
+
/**
|
|
6
|
+
* Emitted when a variant failed to sync to QLS.
|
|
7
|
+
*/
|
|
8
|
+
class QlsVariantSyncFailedEvent extends core_1.VendureEvent {
|
|
9
|
+
constructor(ctx,
|
|
10
|
+
/** The variant that failed to sync (at least id, may be full variant). */
|
|
11
|
+
variant, failedAt,
|
|
12
|
+
/** The error that caused the sync to fail. */
|
|
13
|
+
fullError) {
|
|
14
|
+
super();
|
|
15
|
+
this.ctx = ctx;
|
|
16
|
+
this.variant = variant;
|
|
17
|
+
this.failedAt = failedAt;
|
|
18
|
+
this.fullError = fullError;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.QlsVariantSyncFailedEvent = QlsVariantSyncFailedEvent;
|
package/dist/types.d.ts
CHANGED
|
@@ -47,6 +47,11 @@ export interface QlsPluginOptions {
|
|
|
47
47
|
* If not provided, default mapping will be used.
|
|
48
48
|
*/
|
|
49
49
|
getReceiverContact?: (ctx: RequestContext, order: Order) => FulfillmentOrderInput['receiver_contact'] | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Admin UI tab name where the QLS Product ID custom field is shown on ProductVariant.
|
|
52
|
+
* Defaults to 'QLS'.
|
|
53
|
+
*/
|
|
54
|
+
qlsProductIdUiTab?: string;
|
|
50
55
|
}
|
|
51
56
|
/**
|
|
52
57
|
* Additional fields for a product variant that are used to create or update a product in QLS
|
package/dist/ui/providers.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
addActionBarDropdownMenuItem,
|
|
3
|
+
ModalService,
|
|
4
|
+
} from '@vendure/admin-ui/core';
|
|
5
|
+
import { firstValueFrom } from 'rxjs';
|
|
2
6
|
import gql from 'graphql-tag';
|
|
3
7
|
|
|
4
8
|
export default [
|
|
@@ -42,7 +46,39 @@ export default [
|
|
|
42
46
|
icon: 'resistor',
|
|
43
47
|
requiresPermission: ['QLSFullSync'],
|
|
44
48
|
hasDivider: true,
|
|
45
|
-
onClick: (
|
|
49
|
+
onClick: async (
|
|
50
|
+
_,
|
|
51
|
+
{ route, dataService, notificationService, injector }
|
|
52
|
+
) => {
|
|
53
|
+
const orderId = route.snapshot.params.id;
|
|
54
|
+
const res = await firstValueFrom(
|
|
55
|
+
dataService.query(
|
|
56
|
+
gql`
|
|
57
|
+
query Order($orderId: ID!) {
|
|
58
|
+
order(id: $orderId) {
|
|
59
|
+
id
|
|
60
|
+
qlsOrderIds
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
`,
|
|
64
|
+
{ orderId }
|
|
65
|
+
).single$
|
|
66
|
+
);
|
|
67
|
+
if ((res as any).order.qlsOrderIds.length > 0) {
|
|
68
|
+
const modalService = injector.get(ModalService);
|
|
69
|
+
const confirmed = await firstValueFrom(
|
|
70
|
+
modalService.dialog({
|
|
71
|
+
title: 'Push order to QLS',
|
|
72
|
+
body: 'This order already exists in QLS. Are you sure you want to push it again?',
|
|
73
|
+
buttons: [
|
|
74
|
+
{ type: 'secondary', label: 'Cancel', returnValue: false },
|
|
75
|
+
{ type: 'primary', label: 'Push to QLS', returnValue: true },
|
|
76
|
+
],
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
if (!confirmed) return;
|
|
80
|
+
}
|
|
81
|
+
|
|
46
82
|
dataService
|
|
47
83
|
.mutate(
|
|
48
84
|
gql`
|
|
@@ -50,13 +86,10 @@ export default [
|
|
|
50
86
|
pushOrderToQls(orderId: $orderId)
|
|
51
87
|
}
|
|
52
88
|
`,
|
|
53
|
-
{
|
|
54
|
-
orderId: route.snapshot.params.id,
|
|
55
|
-
}
|
|
89
|
+
{ orderId }
|
|
56
90
|
)
|
|
57
91
|
.subscribe({
|
|
58
92
|
next: (result) => {
|
|
59
|
-
console.log(result);
|
|
60
93
|
notificationService.notify({
|
|
61
94
|
message: (result as any).pushOrderToQls,
|
|
62
95
|
type: 'success',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pinelab/vendure-plugin-qls-fulfillment",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Vendure plugin to fulfill orders via QLS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fulfillment",
|
|
@@ -32,5 +32,5 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"catch-unknown": "^2.0.0"
|
|
34
34
|
},
|
|
35
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "60255d8c40e4068a9f87601fbcedea27719e8385"
|
|
36
36
|
}
|