@pinelab/vendure-plugin-qls-fulfillment 3.2.0 → 3.4.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 +8 -0
- package/README.md +3 -1
- package/dist/api/qls-webhooks-controller.d.ts +3 -3
- package/dist/api/qls-webhooks-controller.js +7 -1
- package/dist/config/sync-products-scheduled-task.js +1 -1
- package/dist/custom-fields.d.ts +2 -0
- package/dist/custom-fields.js +32 -0
- package/dist/dashboard/components/AddAdditionalEanButton.tsx +1 -1
- package/dist/dashboard/components/PushOrderToQlsMenuItem.tsx +1 -1
- package/dist/dashboard/components/SyncProductsMenuItem.tsx +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/client-types.d.ts +20 -0
- package/dist/lib/qls-client.d.ts +1 -1
- package/dist/qls-plugin.d.ts +2 -4
- package/dist/qls-plugin.js +2 -11
- package/dist/services/qls-order.service.d.ts +18 -1
- package/dist/services/qls-order.service.js +83 -31
- package/dist/services/qls-product.service.d.ts +1 -1
- package/dist/services/qls-product.service.js +63 -61
- package/package.json +4 -4
- package/dist/ui/providers.ts +0 -246
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
# 3.4.0 (2026-10-30)
|
|
2
|
+
|
|
3
|
+
- Removed the legacy Angular Admin UI extension. Backend functionality and the React Dashboard extension are unchanged.
|
|
4
|
+
|
|
5
|
+
# 3.3.0 (2026-09-24)
|
|
6
|
+
|
|
7
|
+
- Handle the `shipment.barcode` webhook and store the QLS tracking code and tracking URL on the order's custom fields.
|
|
8
|
+
|
|
1
9
|
# 3.2.0 (2026-08-12)
|
|
2
10
|
|
|
3
11
|
- Restore the legacy Angular Admin UI extension alongside the React Dashboard.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Vendure plugin to fulfill orders via QLS. This uses QLS as fulfillment center and does not support shipments only via QLS.
|
|
4
4
|
|
|
5
|
-
[Official documentation here](https://
|
|
5
|
+
[Official documentation here](https://engineering.pinelab.studio/plugin/vendure-plugin-qls-fulfillment)
|
|
6
6
|
|
|
7
7
|
This plugin contains:
|
|
8
8
|
|
|
@@ -34,6 +34,7 @@ The mutation returns the list of all existing EANs for the product in QLS after
|
|
|
34
34
|
- Pushes orders to QLS automatically on order placement
|
|
35
35
|
- Supports manual order push to QLS via Admin UI
|
|
36
36
|
- Updates order status in Vendure based on QLS webhooks
|
|
37
|
+
- Stores tracking codes and tracking URLs from incoming `shipment.barcode` webhooks on the order's (private) `qlsTrackingCodes` and `qlsTrackingUrls` custom fields. An order can have multiple shipments/parcels, so codes and URLs are accumulated in these lists.
|
|
37
38
|
|
|
38
39
|
## Getting started
|
|
39
40
|
|
|
@@ -93,6 +94,7 @@ You should set up webhooks for the following events:
|
|
|
93
94
|
- `fulfillment_product.stock`
|
|
94
95
|
- `fulfillment_order.status`
|
|
95
96
|
- `fulfillment_order.completed`
|
|
97
|
+
- `shipment.barcode`
|
|
96
98
|
|
|
97
99
|
The URL for all these events should be `https://<YOUR_VENDURE_HOST>/qls/webhook/<CHANNEL_TOKEN>?secret=<PLUGIN_SECRET>`. E.g. `https://example.com/qls/webhook/your-channel-token?secret=121231`.
|
|
98
100
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ChannelService } from '@vendure/core';
|
|
2
2
|
import { Request } from 'express';
|
|
3
|
+
import { IncomingOrderWebhook, IncomingShipmentWebhook, IncomingStockWebhook } from '../lib/client-types';
|
|
4
|
+
import { QlsOrderService } from '../services/qls-order.service';
|
|
3
5
|
import { QlsProductService } from '../services/qls-product.service';
|
|
4
6
|
import { QlsPluginOptions } from '../types';
|
|
5
|
-
import { QlsOrderService } from '../services/qls-order.service';
|
|
6
|
-
import { IncomingOrderWebhook, IncomingStockWebhook } from '../lib/client-types';
|
|
7
7
|
export declare class QlsWebhooksController {
|
|
8
8
|
private channelService;
|
|
9
9
|
private qlsProductService;
|
|
@@ -13,6 +13,6 @@ export declare class QlsWebhooksController {
|
|
|
13
13
|
/**
|
|
14
14
|
* Endpoint for all incoming webhooks
|
|
15
15
|
*/
|
|
16
|
-
events(channelToken: string, webhookSecret: string, request: Request, body: IncomingStockWebhook | IncomingOrderWebhook): Promise<void>;
|
|
16
|
+
events(channelToken: string, webhookSecret: string, request: Request, body: IncomingStockWebhook | IncomingOrderWebhook | IncomingShipmentWebhook): Promise<void>;
|
|
17
17
|
private getCtxForChannel;
|
|
18
18
|
}
|
|
@@ -16,9 +16,9 @@ exports.QlsWebhooksController = void 0;
|
|
|
16
16
|
const common_1 = require("@nestjs/common");
|
|
17
17
|
const core_1 = require("@vendure/core");
|
|
18
18
|
const catch_unknown_1 = require("catch-unknown");
|
|
19
|
-
const qls_product_service_1 = require("../services/qls-product.service");
|
|
20
19
|
const constants_1 = require("../constants");
|
|
21
20
|
const qls_order_service_1 = require("../services/qls-order.service");
|
|
21
|
+
const qls_product_service_1 = require("../services/qls-product.service");
|
|
22
22
|
let QlsWebhooksController = class QlsWebhooksController {
|
|
23
23
|
constructor(channelService, qlsProductService, qlsOrderService, options) {
|
|
24
24
|
this.channelService = channelService;
|
|
@@ -42,6 +42,9 @@ let QlsWebhooksController = class QlsWebhooksController {
|
|
|
42
42
|
if (isStockWebhook(body)) {
|
|
43
43
|
await this.qlsProductService.handleStockWebhook(ctx, body);
|
|
44
44
|
}
|
|
45
|
+
else if (isShipmentWebhook(body)) {
|
|
46
|
+
await this.qlsOrderService.handleShipmentBarcodeWebhook(ctx, body);
|
|
47
|
+
}
|
|
45
48
|
else if (isOrderWebhook(body)) {
|
|
46
49
|
await this.qlsOrderService.handleOrderStatusUpdate(ctx, body);
|
|
47
50
|
}
|
|
@@ -92,3 +95,6 @@ function isStockWebhook(body) {
|
|
|
92
95
|
function isOrderWebhook(body) {
|
|
93
96
|
return 'customer_reference' in body && 'status' in body;
|
|
94
97
|
}
|
|
98
|
+
function isShipmentWebhook(body) {
|
|
99
|
+
return 'carrier_id' in body && 'reference' in body;
|
|
100
|
+
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.qlsSyncAllProductsTask = void 0;
|
|
4
4
|
const core_1 = require("@vendure/core");
|
|
5
|
-
const qls_product_service_1 = require("../services/qls-product.service");
|
|
6
5
|
const constants_1 = require("../constants");
|
|
6
|
+
const qls_product_service_1 = require("../services/qls-product.service");
|
|
7
7
|
/**
|
|
8
8
|
* Synchronize all products to QLS for all channels, every day at 3:00 AM.
|
|
9
9
|
*/
|
package/dist/custom-fields.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ declare module '@vendure/core' {
|
|
|
7
7
|
interface CustomOrderFields {
|
|
8
8
|
qlsServicePointId?: string;
|
|
9
9
|
qlsServicePointDetails?: string;
|
|
10
|
+
qlsTrackingCodes?: string[];
|
|
11
|
+
qlsTrackingUrls?: string[];
|
|
10
12
|
}
|
|
11
13
|
}
|
|
12
14
|
/** Returns variant custom fields with the given Admin UI tab name. */
|
package/dist/custom-fields.js
CHANGED
|
@@ -44,4 +44,36 @@ exports.orderCustomFields = [
|
|
|
44
44
|
readonly: false,
|
|
45
45
|
ui: { tab: 'QLS' },
|
|
46
46
|
},
|
|
47
|
+
{
|
|
48
|
+
name: 'qlsTrackingCodes',
|
|
49
|
+
type: 'string',
|
|
50
|
+
list: true,
|
|
51
|
+
label: [{ value: 'QLS Tracking Codes', languageCode: core_1.LanguageCode.en }],
|
|
52
|
+
description: [
|
|
53
|
+
{
|
|
54
|
+
value: 'Barcodes/tracking codes of all shipments/parcels created for this order in QLS.',
|
|
55
|
+
languageCode: core_1.LanguageCode.en,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
nullable: true,
|
|
59
|
+
public: false,
|
|
60
|
+
readonly: true,
|
|
61
|
+
ui: { tab: 'QLS' },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'qlsTrackingUrls',
|
|
65
|
+
type: 'string',
|
|
66
|
+
list: true,
|
|
67
|
+
label: [{ value: 'QLS Tracking URLs', languageCode: core_1.LanguageCode.en }],
|
|
68
|
+
description: [
|
|
69
|
+
{
|
|
70
|
+
value: 'Tracking URLs of all shipments/parcels created for this order in QLS.',
|
|
71
|
+
languageCode: core_1.LanguageCode.en,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
nullable: true,
|
|
75
|
+
public: false,
|
|
76
|
+
readonly: true,
|
|
77
|
+
ui: { tab: 'QLS' },
|
|
78
|
+
},
|
|
47
79
|
];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useMutation } from '@tanstack/react-query';
|
|
1
2
|
import {
|
|
2
3
|
api,
|
|
3
4
|
Button,
|
|
@@ -11,7 +12,6 @@ import {
|
|
|
11
12
|
Input,
|
|
12
13
|
Label,
|
|
13
14
|
} from '@vendure/dashboard';
|
|
14
|
-
import { useMutation } from '@tanstack/react-query';
|
|
15
15
|
import { QrCode } from 'lucide-react';
|
|
16
16
|
import { useState } from 'react';
|
|
17
17
|
import { toast } from 'sonner';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
1
2
|
import {
|
|
2
3
|
api,
|
|
3
4
|
Button,
|
|
@@ -9,7 +10,6 @@ import {
|
|
|
9
10
|
DialogTitle,
|
|
10
11
|
DropdownMenuItem,
|
|
11
12
|
} from '@vendure/dashboard';
|
|
12
|
-
import { useQuery, useMutation } from '@tanstack/react-query';
|
|
13
13
|
import { RefreshCw } from 'lucide-react';
|
|
14
14
|
import { useState } from 'react';
|
|
15
15
|
import { toast } from 'sonner';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from './api/generated/graphql';
|
|
2
|
+
export * from './config/permissions';
|
|
2
3
|
export * from './config/sync-products-scheduled-task';
|
|
3
4
|
export * from './custom-fields';
|
|
4
5
|
export * from './lib/client-types';
|
|
@@ -6,5 +7,4 @@ export * from './lib/qls-client';
|
|
|
6
7
|
export * from './qls-plugin';
|
|
7
8
|
export * from './services/qls-order.service';
|
|
8
9
|
export * from './services/qls-product.service';
|
|
9
|
-
export * from './config/permissions';
|
|
10
10
|
export * from './types';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./api/generated/graphql"), exports);
|
|
18
|
+
__exportStar(require("./config/permissions"), exports);
|
|
18
19
|
__exportStar(require("./config/sync-products-scheduled-task"), exports);
|
|
19
20
|
__exportStar(require("./custom-fields"), exports);
|
|
20
21
|
__exportStar(require("./lib/client-types"), exports);
|
|
@@ -22,5 +23,4 @@ __exportStar(require("./lib/qls-client"), exports);
|
|
|
22
23
|
__exportStar(require("./qls-plugin"), exports);
|
|
23
24
|
__exportStar(require("./services/qls-order.service"), exports);
|
|
24
25
|
__exportStar(require("./services/qls-product.service"), exports);
|
|
25
|
-
__exportStar(require("./config/permissions"), exports);
|
|
26
26
|
__exportStar(require("./types"), exports);
|
|
@@ -165,6 +165,26 @@ export interface FulfillmentOrder {
|
|
|
165
165
|
}
|
|
166
166
|
export type IncomingStockWebhook = FulfillmentProductDetail;
|
|
167
167
|
export type IncomingOrderWebhook = Pick<FulfillmentOrder, 'customer_reference' | 'status' | 'cancelled' | 'amount_delivered' | 'amount_total'>;
|
|
168
|
+
/**
|
|
169
|
+
* Incoming `shipment.barcode` webhook payload from QLS.
|
|
170
|
+
* `reference` matches the Vendure order code.
|
|
171
|
+
* Note: `barcode`/`tracking_url` on the root object are sometimes not yet populated,
|
|
172
|
+
* so we also fall back to the nested shipment with the same `id` under `deliveries[].shipments[]`.
|
|
173
|
+
*/
|
|
174
|
+
export interface IncomingShipmentWebhook {
|
|
175
|
+
id: string;
|
|
176
|
+
reference: string;
|
|
177
|
+
carrier_id: string;
|
|
178
|
+
barcode: string | null;
|
|
179
|
+
tracking_url: string | null;
|
|
180
|
+
deliveries?: Array<{
|
|
181
|
+
shipments?: Array<{
|
|
182
|
+
id: string;
|
|
183
|
+
barcode: string | null;
|
|
184
|
+
tracking_url: string | null;
|
|
185
|
+
}>;
|
|
186
|
+
}>;
|
|
187
|
+
}
|
|
168
188
|
/**
|
|
169
189
|
* This is the full data returend by the QLS API when fetching a fulfillment product by ID
|
|
170
190
|
* Getting via list, sku or filtering does NOT return this full object
|
package/dist/lib/qls-client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RequestContext } from '@vendure/core';
|
|
2
|
+
import { QlsServicePoint } from '../api/generated/graphql';
|
|
2
3
|
import { QlsClientConfig, QlsPluginOptions } from '../types';
|
|
3
4
|
import type { FulfillmentOrder, FulfillmentOrderInput, FulfillmentProduct, FulfillmentProductDetail, FulfillmentProductInput, QlsApiResponse } from './client-types';
|
|
4
|
-
import { QlsServicePoint } from '../api/generated/graphql';
|
|
5
5
|
export declare function getQlsClient(ctx: RequestContext, pluginOptions: QlsPluginOptions): Promise<QlsClient | undefined>;
|
|
6
6
|
/**
|
|
7
7
|
* Wrapper around the QLS Rest API.
|
package/dist/qls-plugin.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { Type } from '@vendure/core';
|
|
2
|
-
import {
|
|
3
|
-
import { QlsPluginOptions } from './types';
|
|
1
|
+
import { type Type } from '@vendure/core';
|
|
2
|
+
import type { QlsPluginOptions } from './types';
|
|
4
3
|
export declare class QlsPlugin {
|
|
5
4
|
static options: QlsPluginOptions;
|
|
6
5
|
static init(options: QlsPluginOptions): Type<QlsPlugin>;
|
|
7
|
-
static ui: AdminUiExtension;
|
|
8
6
|
}
|
package/dist/qls-plugin.js
CHANGED
|
@@ -5,14 +5,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
5
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
6
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
7
|
};
|
|
8
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
-
};
|
|
11
8
|
var QlsPlugin_1;
|
|
12
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
10
|
exports.QlsPlugin = void 0;
|
|
14
11
|
const core_1 = require("@vendure/core");
|
|
15
|
-
const path_1 = __importDefault(require("path"));
|
|
16
12
|
const api_extensions_1 = require("./api/api-extensions");
|
|
17
13
|
const qls_admin_resolver_1 = require("./api/qls-admin.resolver");
|
|
18
14
|
const qls_shop_resolver_1 = require("./api/qls-shop.resolver");
|
|
@@ -20,12 +16,12 @@ const qls_webhooks_controller_1 = require("./api/qls-webhooks-controller");
|
|
|
20
16
|
const permissions_1 = require("./config/permissions");
|
|
21
17
|
const constants_1 = require("./constants");
|
|
22
18
|
const custom_fields_1 = require("./custom-fields");
|
|
19
|
+
const qls_order_entity_entity_1 = require("./entities/qls-order-entity.entity");
|
|
23
20
|
const qls_order_service_1 = require("./services/qls-order.service");
|
|
24
21
|
const qls_product_service_1 = require("./services/qls-product.service");
|
|
25
|
-
const qls_order_entity_entity_1 = require("./entities/qls-order-entity.entity");
|
|
26
22
|
let QlsPlugin = QlsPlugin_1 = class QlsPlugin {
|
|
27
23
|
static init(options) {
|
|
28
|
-
|
|
24
|
+
QlsPlugin_1.options = {
|
|
29
25
|
...options,
|
|
30
26
|
orderSync: {
|
|
31
27
|
autoPushOrders: true,
|
|
@@ -41,11 +37,6 @@ let QlsPlugin = QlsPlugin_1 = class QlsPlugin {
|
|
|
41
37
|
}
|
|
42
38
|
};
|
|
43
39
|
exports.QlsPlugin = QlsPlugin;
|
|
44
|
-
QlsPlugin.ui = {
|
|
45
|
-
extensionPath: path_1.default.join(__dirname, 'ui'),
|
|
46
|
-
id: 'qls-fulfillment-ui',
|
|
47
|
-
providers: ['providers.ts'],
|
|
48
|
-
};
|
|
49
40
|
exports.QlsPlugin = QlsPlugin = QlsPlugin_1 = __decorate([
|
|
50
41
|
(0, core_1.VendurePlugin)({
|
|
51
42
|
imports: [core_1.PluginCommonModule],
|
|
@@ -2,7 +2,7 @@ import { OnApplicationBootstrap, OnModuleInit } from '@nestjs/common';
|
|
|
2
2
|
import { ModuleRef } from '@nestjs/core';
|
|
3
3
|
import { EventBus, ID, Job, JobQueueService, OrderService, RequestContext, TransactionalConnection } from '@vendure/core';
|
|
4
4
|
import { QlsServicePoint, QlsServicePointSearchInput } from '../api/generated/graphql';
|
|
5
|
-
import { IncomingOrderWebhook } from '../lib/client-types';
|
|
5
|
+
import { IncomingOrderWebhook, IncomingShipmentWebhook } from '../lib/client-types';
|
|
6
6
|
import { QlsOrderJobData, QlsPluginOptions } from '../types';
|
|
7
7
|
export declare class QlsOrderService implements OnModuleInit, OnApplicationBootstrap {
|
|
8
8
|
private options;
|
|
@@ -20,6 +20,12 @@ export declare class QlsOrderService implements OnModuleInit, OnApplicationBoots
|
|
|
20
20
|
* Returns the result of the job, which will be stored in the job record.
|
|
21
21
|
*/
|
|
22
22
|
handleOrderJob(job: Job<QlsOrderJobData>): Promise<unknown>;
|
|
23
|
+
/** Checks the address fields required by QLS fulfillment orders. */
|
|
24
|
+
private hasRequiredShippingFields;
|
|
25
|
+
/** Rejects repeated pushes unless explicitly forced. */
|
|
26
|
+
private assertOrderNotSynced;
|
|
27
|
+
/** Loads optional extra order items and reports callback failures consistently. */
|
|
28
|
+
private getAdditionalOrderItems;
|
|
23
29
|
/**
|
|
24
30
|
* Push an order to QLS by id.
|
|
25
31
|
* Returns a human-readable message describing the result of the operation (Used as job result).
|
|
@@ -31,6 +37,17 @@ export declare class QlsOrderService implements OnModuleInit, OnApplicationBoots
|
|
|
31
37
|
* Update the status of an order in QLS based on the given order code and status
|
|
32
38
|
*/
|
|
33
39
|
handleOrderStatusUpdate(ctx: RequestContext, body: IncomingOrderWebhook): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Store the tracking code and tracking URL from an incoming `shipment.barcode` webhook
|
|
42
|
+
* on the order's custom fields. Codes/URLs are accumulated, because an order can have
|
|
43
|
+
* multiple shipments/parcels.
|
|
44
|
+
*/
|
|
45
|
+
handleShipmentBarcodeWebhook(ctx: RequestContext, body: IncomingShipmentWebhook): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* The root `barcode`/`tracking_url` fields on the webhook payload are sometimes not yet
|
|
48
|
+
* populated, so fall back to the nested shipment with the same id under `deliveries[].shipments[]`.
|
|
49
|
+
*/
|
|
50
|
+
private getShipmentTrackingInfo;
|
|
34
51
|
triggerPushOrder(ctx: RequestContext, orderId: ID, orderCode?: string): Promise<Job<QlsOrderJobData> | undefined>;
|
|
35
52
|
/**
|
|
36
53
|
* Get QLS order id(s) for a Vendure order (for Order.qlsOrderIds field).
|
|
@@ -16,15 +16,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
16
16
|
};
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.QlsOrderService = void 0;
|
|
19
|
+
const node_util_1 = __importDefault(require("node:util"));
|
|
19
20
|
const common_1 = require("@nestjs/common");
|
|
20
21
|
const core_1 = require("@nestjs/core");
|
|
21
22
|
const core_2 = require("@vendure/core");
|
|
22
23
|
const catch_unknown_1 = require("catch-unknown");
|
|
23
|
-
const util_1 = __importDefault(require("util"));
|
|
24
24
|
const constants_1 = require("../constants");
|
|
25
|
+
const qls_order_entity_entity_1 = require("../entities/qls-order-entity.entity");
|
|
25
26
|
const qls_client_1 = require("../lib/qls-client");
|
|
26
27
|
const qls_order_failed_event_1 = require("./qls-order-failed-event");
|
|
27
|
-
const qls_order_entity_entity_1 = require("../entities/qls-order-entity.entity");
|
|
28
28
|
let QlsOrderService = class QlsOrderService {
|
|
29
29
|
constructor(options, jobQueueService, eventBus, orderService, moduleRef, connection) {
|
|
30
30
|
this.options = options;
|
|
@@ -74,10 +74,39 @@ let QlsOrderService = class QlsOrderService {
|
|
|
74
74
|
...job.data,
|
|
75
75
|
ctx: undefined,
|
|
76
76
|
};
|
|
77
|
-
core_2.Logger.error(`Error handling job ${job.data.action}: ${error}`, constants_1.loggerCtx,
|
|
77
|
+
core_2.Logger.error(`Error handling job ${job.data.action}: ${error}`, constants_1.loggerCtx, node_util_1.default.inspect(dataWithoutCtx, false, 5));
|
|
78
78
|
throw error;
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
/** Checks the address fields required by QLS fulfillment orders. */
|
|
82
|
+
hasRequiredShippingFields(address) {
|
|
83
|
+
return !!(address.streetLine1 &&
|
|
84
|
+
address.postalCode &&
|
|
85
|
+
address.city &&
|
|
86
|
+
address.streetLine2 &&
|
|
87
|
+
address.countryCode);
|
|
88
|
+
}
|
|
89
|
+
/** Rejects repeated pushes unless explicitly forced. */
|
|
90
|
+
async assertOrderNotSynced(ctx, order, force) {
|
|
91
|
+
if (force)
|
|
92
|
+
return;
|
|
93
|
+
const existing = await this.connection
|
|
94
|
+
.getRepository(ctx, qls_order_entity_entity_1.QlsOrderEntity)
|
|
95
|
+
.findOne({ where: { vendureOrderId: order.id } });
|
|
96
|
+
if (existing)
|
|
97
|
+
throw new core_2.UserInputError(`Order '${order.code}' has already been synced to QLS`);
|
|
98
|
+
}
|
|
99
|
+
/** Loads optional extra order items and reports callback failures consistently. */
|
|
100
|
+
async getAdditionalOrderItems(ctx, order) {
|
|
101
|
+
try {
|
|
102
|
+
return ((await this.options.orderSync.addAdditionalOrderItems?.(ctx, new core_2.Injector(this.moduleRef), order)) ?? []);
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
const error = (0, catch_unknown_1.asError)(e);
|
|
106
|
+
core_2.Logger.error(`Error getting additional order items for order '${order.code}': ${error.message}`, constants_1.loggerCtx, error.stack);
|
|
107
|
+
throw new Error(`Error adding additional order items for order '${order.code}': ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
81
110
|
/**
|
|
82
111
|
* Push an order to QLS by id.
|
|
83
112
|
* Returns a human-readable message describing the result of the operation (Used as job result).
|
|
@@ -94,18 +123,7 @@ let QlsOrderService = class QlsOrderService {
|
|
|
94
123
|
if (!order) {
|
|
95
124
|
throw new Error(`No order with id ${orderId} not found`);
|
|
96
125
|
}
|
|
97
|
-
|
|
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
|
-
}
|
|
108
|
-
}
|
|
126
|
+
await this.assertOrderNotSynced(ctx, order, force);
|
|
109
127
|
try {
|
|
110
128
|
// Map order variants to QLS products
|
|
111
129
|
const qlsProducts = [];
|
|
@@ -126,17 +144,7 @@ let QlsOrderService = class QlsOrderService {
|
|
|
126
144
|
});
|
|
127
145
|
}));
|
|
128
146
|
// Add additional order items, if any
|
|
129
|
-
|
|
130
|
-
const additionalOrderItems = await this.options.orderSync.addAdditionalOrderItems?.(ctx, new core_2.Injector(this.moduleRef), order);
|
|
131
|
-
if (additionalOrderItems) {
|
|
132
|
-
qlsProducts.push(...additionalOrderItems);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
catch (e) {
|
|
136
|
-
const error = (0, catch_unknown_1.asError)(e);
|
|
137
|
-
core_2.Logger.error(`Error getting additional order items for order '${order.code}': ${error.message}`, constants_1.loggerCtx, error.stack);
|
|
138
|
-
throw new Error(`Error adding additional order items for order '${order.code}': ${error.message}`);
|
|
139
|
-
}
|
|
147
|
+
qlsProducts.push(...(await this.getAdditionalOrderItems(ctx, order)));
|
|
140
148
|
if (qlsProducts.length === 0) {
|
|
141
149
|
const message = `No products to push to QLS for order '${order.code}'. Ignoring order.`;
|
|
142
150
|
core_2.Logger.info(message, constants_1.loggerCtx);
|
|
@@ -153,11 +161,7 @@ let QlsOrderService = class QlsOrderService {
|
|
|
153
161
|
if (!order.shippingAddress) {
|
|
154
162
|
throw new Error(`Order '${order.code}' has no shipping address! Can not push order to QLS.`);
|
|
155
163
|
}
|
|
156
|
-
if (!order.shippingAddress
|
|
157
|
-
!order.shippingAddress.postalCode ||
|
|
158
|
-
!order.shippingAddress.city ||
|
|
159
|
-
!order.shippingAddress.streetLine2 ||
|
|
160
|
-
!order.shippingAddress.countryCode) {
|
|
164
|
+
if (!this.hasRequiredShippingFields(order.shippingAddress)) {
|
|
161
165
|
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.`);
|
|
162
166
|
}
|
|
163
167
|
const processable = (await this.options.orderSync.processOrderFrom?.(ctx, order)) ??
|
|
@@ -260,6 +264,54 @@ let QlsOrderService = class QlsOrderService {
|
|
|
260
264
|
await this.orderService.transitionToState(ctx, order.id, vendureOrderState);
|
|
261
265
|
core_2.Logger.info(`Successfully updated order '${orderCode}' to '${vendureOrderState}'`, constants_1.loggerCtx);
|
|
262
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Store the tracking code and tracking URL from an incoming `shipment.barcode` webhook
|
|
269
|
+
* on the order's custom fields. Codes/URLs are accumulated, because an order can have
|
|
270
|
+
* multiple shipments/parcels.
|
|
271
|
+
*/
|
|
272
|
+
async handleShipmentBarcodeWebhook(ctx, body) {
|
|
273
|
+
const orderCode = body.reference;
|
|
274
|
+
const { barcode, trackingUrl } = this.getShipmentTrackingInfo(body);
|
|
275
|
+
if (!barcode) {
|
|
276
|
+
core_2.Logger.info(`Ignoring shipment.barcode webhook for order '${orderCode}', because no barcode was found in the payload`, constants_1.loggerCtx);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const order = await this.orderService.findOneByCode(ctx, orderCode, []);
|
|
280
|
+
if (!order) {
|
|
281
|
+
return core_2.Logger.warn(`Order with code '${orderCode}' not found, ignoring shipment.barcode webhook`, constants_1.loggerCtx);
|
|
282
|
+
}
|
|
283
|
+
const existingCodes = order.customFields.qlsTrackingCodes ?? [];
|
|
284
|
+
if (existingCodes.includes(barcode)) {
|
|
285
|
+
core_2.Logger.info(`Tracking code '${barcode}' already stored for order '${orderCode}', ignoring duplicate shipment.barcode webhook`, constants_1.loggerCtx);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const existingUrls = order.customFields.qlsTrackingUrls ?? [];
|
|
289
|
+
await this.connection.getRepository(ctx, core_2.Order).update({ id: order.id }, {
|
|
290
|
+
customFields: {
|
|
291
|
+
qlsTrackingCodes: [...existingCodes, barcode],
|
|
292
|
+
qlsTrackingUrls: trackingUrl
|
|
293
|
+
? [...existingUrls, trackingUrl]
|
|
294
|
+
: existingUrls,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
core_2.Logger.info(`Stored tracking code '${barcode}' for order '${orderCode}'`, constants_1.loggerCtx);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* The root `barcode`/`tracking_url` fields on the webhook payload are sometimes not yet
|
|
301
|
+
* populated, so fall back to the nested shipment with the same id under `deliveries[].shipments[]`.
|
|
302
|
+
*/
|
|
303
|
+
getShipmentTrackingInfo(body) {
|
|
304
|
+
if (body.barcode) {
|
|
305
|
+
return { barcode: body.barcode, trackingUrl: body.tracking_url };
|
|
306
|
+
}
|
|
307
|
+
const nestedShipment = body.deliveries
|
|
308
|
+
?.flatMap((delivery) => delivery.shipments ?? [])
|
|
309
|
+
.find((shipment) => shipment.id === body.id);
|
|
310
|
+
return {
|
|
311
|
+
barcode: nestedShipment?.barcode ?? null,
|
|
312
|
+
trackingUrl: nestedShipment?.tracking_url ?? null,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
263
315
|
async triggerPushOrder(ctx, orderId, orderCode) {
|
|
264
316
|
const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
|
|
265
317
|
if (!client) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { OnApplicationBootstrap, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { ModuleRef } from '@nestjs/core';
|
|
2
3
|
import { EventBus, ID, Job, JobQueueService, ListQueryBuilder, ProductPriceApplicator, ProductVariant, ProductVariantService, RequestContext, StockLevelService, StockLocationService, TransactionalConnection } from '@vendure/core';
|
|
3
4
|
import { FulfillmentProduct, IncomingStockWebhook } from '../lib/client-types';
|
|
4
5
|
import { QlsClient } from '../lib/qls-client';
|
|
5
6
|
import { QlsPluginOptions, QlsProductJobData } from '../types';
|
|
6
|
-
import { ModuleRef } from '@nestjs/core';
|
|
7
7
|
type SyncProductsResult = {
|
|
8
8
|
updatedInQls: Partial<ProductVariant>[];
|
|
9
9
|
createdInQls: Partial<ProductVariant>[];
|
|
@@ -16,15 +16,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
16
16
|
};
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.QlsProductService = void 0;
|
|
19
|
+
const node_util_1 = __importDefault(require("node:util"));
|
|
19
20
|
const common_1 = require("@nestjs/common");
|
|
20
|
-
const core_1 = require("@
|
|
21
|
+
const core_1 = require("@nestjs/core");
|
|
22
|
+
const core_2 = require("@vendure/core");
|
|
21
23
|
const catch_unknown_1 = require("catch-unknown");
|
|
22
24
|
const typeorm_1 = require("typeorm");
|
|
23
|
-
const util_1 = __importDefault(require("util"));
|
|
24
25
|
const constants_1 = require("../constants");
|
|
25
26
|
const qls_client_1 = require("../lib/qls-client");
|
|
26
27
|
const qls_variant_sync_failed_event_1 = require("./qls-variant-sync-failed-event");
|
|
27
|
-
const core_2 = require("@nestjs/core");
|
|
28
28
|
// Wait for 700ms to avoid rate limit of 500/5 minutes
|
|
29
29
|
const waitToPreventRateLimit = () => new Promise((resolve) => setTimeout(resolve, 700));
|
|
30
30
|
let QlsProductService = class QlsProductService {
|
|
@@ -42,13 +42,13 @@ let QlsProductService = class QlsProductService {
|
|
|
42
42
|
}
|
|
43
43
|
onApplicationBootstrap() {
|
|
44
44
|
// Listen for ProductVariantEvent and add a job to the queue
|
|
45
|
-
this.eventBus.ofType(
|
|
45
|
+
this.eventBus.ofType(core_2.ProductVariantEvent).subscribe((event) => {
|
|
46
46
|
if (event.type !== 'created' && event.type !== 'updated') {
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
this.triggerSyncVariants(event.ctx, event.entity.map((v) => v.id)).catch((e) => {
|
|
50
50
|
const error = (0, catch_unknown_1.asError)(e);
|
|
51
|
-
|
|
51
|
+
core_2.Logger.error(`Error adding job to queue: ${error.message}`, constants_1.loggerCtx, error.stack);
|
|
52
52
|
});
|
|
53
53
|
});
|
|
54
54
|
}
|
|
@@ -66,7 +66,7 @@ let QlsProductService = class QlsProductService {
|
|
|
66
66
|
*/
|
|
67
67
|
async handleProductJob(job) {
|
|
68
68
|
try {
|
|
69
|
-
const ctx =
|
|
69
|
+
const ctx = core_2.RequestContext.deserialize(job.data.ctx);
|
|
70
70
|
if (job.data.action === 'full-sync-products') {
|
|
71
71
|
const result = await this.runFullSync(ctx);
|
|
72
72
|
return {
|
|
@@ -87,7 +87,7 @@ let QlsProductService = class QlsProductService {
|
|
|
87
87
|
...job.data,
|
|
88
88
|
ctx: undefined,
|
|
89
89
|
};
|
|
90
|
-
|
|
90
|
+
core_2.Logger.error(`Error handling job ${job.data.action}: ${error}`, constants_1.loggerCtx, node_util_1.default.inspect(dataWithoutCtx, false, 5));
|
|
91
91
|
throw error;
|
|
92
92
|
}
|
|
93
93
|
}
|
|
@@ -104,33 +104,35 @@ let QlsProductService = class QlsProductService {
|
|
|
104
104
|
if (!client) {
|
|
105
105
|
throw new Error(`QLS not enabled for channel ${ctx.channel.token}`);
|
|
106
106
|
}
|
|
107
|
-
|
|
107
|
+
core_2.Logger.info(`Running full sync for channel ${ctx.channel.token}`, constants_1.loggerCtx);
|
|
108
108
|
const allQlsProducts = await client.getAllFulfillmentProducts();
|
|
109
109
|
const allVariants = await this.getAllVariants(ctx);
|
|
110
|
-
|
|
110
|
+
core_2.Logger.info(`Running full sync for ${allQlsProducts.length} QLS products and ${allVariants.length} Vendure variants`, constants_1.loggerCtx);
|
|
111
111
|
// Update stock in Vendure based on QLS products
|
|
112
112
|
const updatedStock = [];
|
|
113
113
|
for (const variant of allVariants) {
|
|
114
|
-
const qlsProduct = allQlsProducts.find((p) => p.sku
|
|
114
|
+
const qlsProduct = allQlsProducts.find((p) => p.sku === variant.sku);
|
|
115
115
|
if (qlsProduct) {
|
|
116
116
|
await this.updateStock(ctx, variant.id, qlsProduct.amount_available);
|
|
117
117
|
updatedStock.push(variant);
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
-
|
|
120
|
+
core_2.Logger.info(`Updated stock for ${updatedStock.length} variants based on QLS stock levels`, constants_1.loggerCtx);
|
|
121
121
|
// Create or update products in QLS
|
|
122
122
|
const createdQlsProducts = [];
|
|
123
123
|
const updatedQlsProducts = [];
|
|
124
124
|
const failed = [];
|
|
125
125
|
for (const variant of allVariants) {
|
|
126
126
|
try {
|
|
127
|
-
const existingQlsProduct = allQlsProducts.find((p) => p.sku
|
|
127
|
+
const existingQlsProduct = allQlsProducts.find((p) => p.sku === variant.sku);
|
|
128
128
|
const result = await this.createOrUpdateProductInQls(ctx, client, variant, existingQlsProduct ?? null);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
switch (result.status) {
|
|
130
|
+
case 'created':
|
|
131
|
+
createdQlsProducts.push(variant);
|
|
132
|
+
break;
|
|
133
|
+
case 'updated':
|
|
134
|
+
updatedQlsProducts.push(variant);
|
|
135
|
+
break;
|
|
134
136
|
}
|
|
135
137
|
if (result.status === 'created' || result.status === 'updated') {
|
|
136
138
|
// Wait only if we created or updated a product, otherwise no calls have been made yet.
|
|
@@ -139,15 +141,15 @@ let QlsProductService = class QlsProductService {
|
|
|
139
141
|
}
|
|
140
142
|
catch (e) {
|
|
141
143
|
const error = (0, catch_unknown_1.asError)(e);
|
|
142
|
-
|
|
144
|
+
core_2.Logger.error(`Error creating or updating variant '${variant.sku}' in QLS: ${error.message}`, constants_1.loggerCtx, error.stack);
|
|
143
145
|
failed.push(variant);
|
|
144
146
|
await this.eventBus.publish(new qls_variant_sync_failed_event_1.QlsVariantSyncFailedEvent(ctx, variant, new Date(), error.message));
|
|
145
147
|
await waitToPreventRateLimit();
|
|
146
148
|
}
|
|
147
149
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
core_2.Logger.info(`Created ${createdQlsProducts.length} products in QLS`, constants_1.loggerCtx);
|
|
151
|
+
core_2.Logger.info(`Updated ${updatedQlsProducts.length} products in QLS`, constants_1.loggerCtx);
|
|
152
|
+
core_2.Logger.info(`Finished full sync with ${failed.length} failures`, constants_1.loggerCtx);
|
|
151
153
|
return {
|
|
152
154
|
updatedInQls: updatedQlsProducts,
|
|
153
155
|
createdInQls: createdQlsProducts,
|
|
@@ -157,7 +159,7 @@ let QlsProductService = class QlsProductService {
|
|
|
157
159
|
}
|
|
158
160
|
catch (e) {
|
|
159
161
|
const error = (0, catch_unknown_1.asError)(e);
|
|
160
|
-
|
|
162
|
+
core_2.Logger.error(`Error running full sync: ${error.message}`, constants_1.loggerCtx, error.stack);
|
|
161
163
|
throw error;
|
|
162
164
|
}
|
|
163
165
|
}
|
|
@@ -170,14 +172,14 @@ let QlsProductService = class QlsProductService {
|
|
|
170
172
|
if (!client) {
|
|
171
173
|
throw new Error(`QLS not enabled for channel ${ctx.channel.token}`);
|
|
172
174
|
}
|
|
173
|
-
|
|
175
|
+
core_2.Logger.warn(`Removing all products from QLS for channel ${ctx.channel.token}...`, constants_1.loggerCtx);
|
|
174
176
|
const allProducts = await client.getAllFulfillmentProducts();
|
|
175
177
|
for (const product of allProducts) {
|
|
176
178
|
await client.deleteFulfillmentProduct(product.id);
|
|
177
|
-
|
|
179
|
+
core_2.Logger.info(`Removed product '${product.sku}' (${product.id}) from QLS`, constants_1.loggerCtx);
|
|
178
180
|
await waitToPreventRateLimit();
|
|
179
181
|
}
|
|
180
|
-
|
|
182
|
+
core_2.Logger.warn(`Removed ${allProducts.length} products from QLS for channel ${ctx.channel.token}`, constants_1.loggerCtx);
|
|
181
183
|
}
|
|
182
184
|
/**
|
|
183
185
|
* Creates or updates the fulfillment products in QLS for the given product variants.
|
|
@@ -185,7 +187,7 @@ let QlsProductService = class QlsProductService {
|
|
|
185
187
|
async syncVariants(ctx, productVariantIds) {
|
|
186
188
|
const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
|
|
187
189
|
if (!client) {
|
|
188
|
-
|
|
190
|
+
core_2.Logger.debug(`QLS not enabled for channel ${ctx.channel.token}. Not handling product update/create.`, constants_1.loggerCtx);
|
|
189
191
|
return {
|
|
190
192
|
updatedInQls: [],
|
|
191
193
|
createdInQls: [],
|
|
@@ -205,7 +207,7 @@ let QlsProductService = class QlsProductService {
|
|
|
205
207
|
]);
|
|
206
208
|
if (!variant) {
|
|
207
209
|
// Can happen when a variant is deleted from Vendure after the job was triggered
|
|
208
|
-
|
|
210
|
+
core_2.Logger.info(`Variant with id ${variantId} not found. Not creating or updating product in QLS.`, constants_1.loggerCtx);
|
|
209
211
|
continue;
|
|
210
212
|
}
|
|
211
213
|
try {
|
|
@@ -221,7 +223,7 @@ let QlsProductService = class QlsProductService {
|
|
|
221
223
|
catch (e) {
|
|
222
224
|
const error = (0, catch_unknown_1.asError)(e);
|
|
223
225
|
// Log as warning, because this is probably a functional mistake, i.e. duplicate barcodes or EANs
|
|
224
|
-
|
|
226
|
+
core_2.Logger.warn(`Error syncing variant ${variantId} (${variant.sku}) to QLS: ${error.message}`, constants_1.loggerCtx);
|
|
225
227
|
failed.push({ id: variantId });
|
|
226
228
|
await this.eventBus.publish(new qls_variant_sync_failed_event_1.QlsVariantSyncFailedEvent(ctx, variant, new Date(), error.message));
|
|
227
229
|
}
|
|
@@ -248,7 +250,7 @@ let QlsProductService = class QlsProductService {
|
|
|
248
250
|
async triggerSyncVariants(ctx, productVariantIds) {
|
|
249
251
|
const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
|
|
250
252
|
if (!client) {
|
|
251
|
-
|
|
253
|
+
core_2.Logger.debug(`QLS not enabled for channel ${ctx.channel.token}. Not triggering product sync.`, constants_1.loggerCtx);
|
|
252
254
|
return;
|
|
253
255
|
}
|
|
254
256
|
return this.productJobQueue.add({
|
|
@@ -294,7 +296,7 @@ let QlsProductService = class QlsProductService {
|
|
|
294
296
|
.filter((ean) => ean.length > 0);
|
|
295
297
|
await Promise.all(eansToAdd.map((ean) => client.addBarcode(qlsProductId, ean)));
|
|
296
298
|
if (eansToAdd.length > 0) {
|
|
297
|
-
|
|
299
|
+
core_2.Logger.info(`Added additional EANs: ${eansToAdd.join(',')} to variant '${variant.sku}' in QLS`, constants_1.loggerCtx);
|
|
298
300
|
}
|
|
299
301
|
await this.triggerSyncVariants(ctx, [variantId]);
|
|
300
302
|
return eansToAdd;
|
|
@@ -307,12 +309,12 @@ let QlsProductService = class QlsProductService {
|
|
|
307
309
|
filter: { sku: { eq: sku } },
|
|
308
310
|
});
|
|
309
311
|
if (!result.items.length) {
|
|
310
|
-
|
|
312
|
+
core_2.Logger.info(`Variant with sku '${sku}' not found, not updating stock`, constants_1.loggerCtx);
|
|
311
313
|
return false;
|
|
312
314
|
}
|
|
313
315
|
const variant = result.items[0];
|
|
314
316
|
if (result.items.length > 1) {
|
|
315
|
-
|
|
317
|
+
core_2.Logger.error(`Multiple variants found for sku '${sku}', using '${variant.id}'`, constants_1.loggerCtx);
|
|
316
318
|
}
|
|
317
319
|
return await this.updateStock(ctx, variant.id, availableStock);
|
|
318
320
|
}
|
|
@@ -324,22 +326,22 @@ let QlsProductService = class QlsProductService {
|
|
|
324
326
|
filter: { sku: { eq: qlsProduct.sku } },
|
|
325
327
|
});
|
|
326
328
|
if (!result.items.length) {
|
|
327
|
-
|
|
329
|
+
core_2.Logger.info(`Variant with sku '${qlsProduct.sku}' not found, not handling stock webhook`, constants_1.loggerCtx);
|
|
328
330
|
return false;
|
|
329
331
|
}
|
|
330
332
|
const variant = result.items[0];
|
|
331
333
|
if (result.items.length > 1) {
|
|
332
|
-
|
|
334
|
+
core_2.Logger.error(`Multiple variants found for sku '${qlsProduct.sku}', using '${variant.id}'`, constants_1.loggerCtx);
|
|
333
335
|
}
|
|
334
336
|
await this.updateStockBySku(ctx, qlsProduct.sku, qlsProduct.amount_available);
|
|
335
337
|
if (!this.options.productSync.saveAdditionalVariantData) {
|
|
336
338
|
return true;
|
|
337
339
|
}
|
|
338
340
|
try {
|
|
339
|
-
await this.options.productSync.saveAdditionalVariantData(ctx, new
|
|
341
|
+
await this.options.productSync.saveAdditionalVariantData(ctx, new core_2.Injector(this.moduleRef), qlsProduct, variant);
|
|
340
342
|
}
|
|
341
343
|
catch (e) {
|
|
342
|
-
|
|
344
|
+
core_2.Logger.error(`Error in saveAdditionalData for variant '${variant.sku}': ${(0, catch_unknown_1.asError)(e).message}`, constants_1.loggerCtx);
|
|
343
345
|
}
|
|
344
346
|
return true;
|
|
345
347
|
}
|
|
@@ -347,17 +349,17 @@ let QlsProductService = class QlsProductService {
|
|
|
347
349
|
* Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
|
|
348
350
|
*/
|
|
349
351
|
async createOrUpdateProductInQls(ctx, client, variant, existingProduct) {
|
|
350
|
-
if (await this.options.productSync.excludeVariantFromSync?.(ctx, new
|
|
351
|
-
|
|
352
|
+
if (await this.options.productSync.excludeVariantFromSync?.(ctx, new core_2.Injector(this.moduleRef), variant)) {
|
|
353
|
+
core_2.Logger.info(`Variant '${variant.sku}' excluded from sync to QLS.`, constants_1.loggerCtx);
|
|
352
354
|
return { status: 'not-changed' };
|
|
353
355
|
}
|
|
354
356
|
if (!variant.enabled || !variant.product?.enabled) {
|
|
355
357
|
const disabledEntity = !variant.enabled ? 'Variant' : 'Product';
|
|
356
358
|
if (existingProduct) {
|
|
357
|
-
|
|
359
|
+
core_2.Logger.warn(`${disabledEntity} '${variant.sku}' is disabled but exists in QLS (product ID ${existingProduct.id}). Skipping sync.`, constants_1.loggerCtx);
|
|
358
360
|
}
|
|
359
361
|
else {
|
|
360
|
-
|
|
362
|
+
core_2.Logger.info(`${disabledEntity} '${variant.sku}' is disabled, skipping sync to QLS.`, constants_1.loggerCtx);
|
|
361
363
|
}
|
|
362
364
|
return { status: 'not-changed', qlsProductId: existingProduct?.id };
|
|
363
365
|
}
|
|
@@ -371,7 +373,7 @@ let QlsProductService = class QlsProductService {
|
|
|
371
373
|
...additionalVariantFields,
|
|
372
374
|
});
|
|
373
375
|
qlsProduct = result;
|
|
374
|
-
|
|
376
|
+
core_2.Logger.info(`Created product '${variant.sku}' in QLS`, constants_1.loggerCtx);
|
|
375
377
|
createdOrUpdated = 'created';
|
|
376
378
|
}
|
|
377
379
|
else if (this.shouldUpdateProductInQls(variant, existingProduct, additionalVariantFields)) {
|
|
@@ -380,21 +382,21 @@ let QlsProductService = class QlsProductService {
|
|
|
380
382
|
name: variant.name,
|
|
381
383
|
...additionalVariantFields,
|
|
382
384
|
});
|
|
383
|
-
|
|
385
|
+
core_2.Logger.info(`Updated product '${variant.sku}' in QLS`, constants_1.loggerCtx);
|
|
384
386
|
createdOrUpdated = 'updated';
|
|
385
387
|
}
|
|
386
388
|
if (qlsProduct && qlsProduct?.id !== variant.customFields.qlsProductId) {
|
|
387
389
|
// Update variant with QLS product ID if it changed
|
|
388
390
|
// Do not use variantService.update because it will trigger a change event and cause an infinite loop
|
|
389
391
|
await this.connection
|
|
390
|
-
.getRepository(ctx,
|
|
392
|
+
.getRepository(ctx, core_2.ProductVariant)
|
|
391
393
|
.update({ id: variant.id }, { customFields: { qlsProductId: qlsProduct.id } });
|
|
392
|
-
|
|
394
|
+
core_2.Logger.info(`Set QLS product ID for variant '${variant.sku}' to ${qlsProduct.id}`, constants_1.loggerCtx);
|
|
393
395
|
}
|
|
394
396
|
// Note: additional EANs are no longer synced automatically during product sync.
|
|
395
397
|
// Use the addAdditionalEANsToQls mutation to add EANs manually.
|
|
396
398
|
if (createdOrUpdated === 'not-changed') {
|
|
397
|
-
|
|
399
|
+
core_2.Logger.info(`Variant '${variant.sku}' not updated in QLS, because no changes were found.`, constants_1.loggerCtx);
|
|
398
400
|
}
|
|
399
401
|
return { status: createdOrUpdated, qlsProductId: qlsProduct?.id };
|
|
400
402
|
}
|
|
@@ -426,7 +428,7 @@ let QlsProductService = class QlsProductService {
|
|
|
426
428
|
'product.featuredAsset',
|
|
427
429
|
];
|
|
428
430
|
const [items, totalItems] = await this.listQueryBuilder
|
|
429
|
-
.build(
|
|
431
|
+
.build(core_2.ProductVariant, {
|
|
430
432
|
skip,
|
|
431
433
|
take,
|
|
432
434
|
}, {
|
|
@@ -437,7 +439,7 @@ let QlsProductService = class QlsProductService {
|
|
|
437
439
|
})
|
|
438
440
|
.getManyAndCount();
|
|
439
441
|
let variants = await Promise.all(items.map(async (item) => this.productPriceApplicator.applyChannelPriceAndTax(item, ctx, undefined, false)));
|
|
440
|
-
variants = variants.map((v) => (0,
|
|
442
|
+
variants = variants.map((v) => (0, core_2.translateDeep)(v, ctx.languageCode));
|
|
441
443
|
allVariants.push(...variants);
|
|
442
444
|
if (allVariants.length >= totalItems) {
|
|
443
445
|
hasMore = false;
|
|
@@ -452,7 +454,7 @@ let QlsProductService = class QlsProductService {
|
|
|
452
454
|
*/
|
|
453
455
|
async updateStock(ctx, variantId, availableStock) {
|
|
454
456
|
if (!this.options.productSync.synchronizeStockLevels) {
|
|
455
|
-
|
|
457
|
+
core_2.Logger.warn(`Stock sync disabled. Not updating stock for variant '${variantId}'`, constants_1.loggerCtx);
|
|
456
458
|
return false;
|
|
457
459
|
}
|
|
458
460
|
// Find default Stock Location
|
|
@@ -461,28 +463,28 @@ let QlsProductService = class QlsProductService {
|
|
|
461
463
|
const stockLevel = await this.stockLevelService.getStockLevel(ctx, variantId, defaultStockLocation.id);
|
|
462
464
|
const delta = availableStock - stockLevel.stockOnHand;
|
|
463
465
|
if (delta !== 0) {
|
|
464
|
-
const adjustment = new
|
|
466
|
+
const adjustment = new core_2.StockAdjustment({
|
|
465
467
|
quantity: delta,
|
|
466
468
|
stockLocation: { id: defaultStockLocation.id },
|
|
467
469
|
productVariant: { id: variantId },
|
|
468
470
|
});
|
|
469
471
|
// Update stock level
|
|
470
|
-
await this.connection.getRepository(ctx,
|
|
472
|
+
await this.connection.getRepository(ctx, core_2.StockLevel).save({
|
|
471
473
|
id: stockLevel.id,
|
|
472
474
|
stockOnHand: availableStock,
|
|
473
475
|
stockAllocated: 0, // Reset allocations, because allocation is handled by QLS
|
|
474
476
|
});
|
|
475
|
-
await this.eventBus.publish(new
|
|
477
|
+
await this.eventBus.publish(new core_2.StockMovementEvent(ctx, [adjustment]));
|
|
476
478
|
}
|
|
477
479
|
else {
|
|
478
480
|
// No stock change, but still reset allocations to ensure consistency
|
|
479
|
-
await this.connection.getRepository(ctx,
|
|
481
|
+
await this.connection.getRepository(ctx, core_2.StockLevel).save({
|
|
480
482
|
id: stockLevel.id,
|
|
481
483
|
stockOnHand: availableStock,
|
|
482
484
|
stockAllocated: 0, // Reset allocations, because allocation is handled by QLS
|
|
483
485
|
});
|
|
484
486
|
}
|
|
485
|
-
|
|
487
|
+
core_2.Logger.info(`Updated stock for variant ${variantId} to ${availableStock}`, constants_1.loggerCtx);
|
|
486
488
|
return delta !== 0;
|
|
487
489
|
}
|
|
488
490
|
};
|
|
@@ -490,12 +492,12 @@ exports.QlsProductService = QlsProductService;
|
|
|
490
492
|
exports.QlsProductService = QlsProductService = __decorate([
|
|
491
493
|
(0, common_1.Injectable)(),
|
|
492
494
|
__param(1, (0, common_1.Inject)(constants_1.PLUGIN_INIT_OPTIONS)),
|
|
493
|
-
__metadata("design:paramtypes", [
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
495
|
+
__metadata("design:paramtypes", [core_2.TransactionalConnection, Object, core_2.JobQueueService,
|
|
496
|
+
core_2.StockLevelService,
|
|
497
|
+
core_2.ProductVariantService,
|
|
498
|
+
core_2.StockLocationService,
|
|
499
|
+
core_2.EventBus,
|
|
500
|
+
core_2.ListQueryBuilder,
|
|
501
|
+
core_2.ProductPriceApplicator,
|
|
502
|
+
core_1.ModuleRef])
|
|
501
503
|
], QlsProductService);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pinelab/vendure-plugin-qls-fulfillment",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "Vendure plugin to fulfill orders via QLS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fulfillment",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"qls"
|
|
9
9
|
],
|
|
10
10
|
"author": "Martijn van de Brug <martijn@pinelab.studio>",
|
|
11
|
-
"homepage": "https://
|
|
11
|
+
"homepage": "https://engineering.pinelab.studio/",
|
|
12
12
|
"repository": "https://github.com/Pinelab-studio/pinelab-vendure-plugins",
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"private": false,
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
"CHANGELOG.md"
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
|
-
"build": "rimraf dist && yarn generate && tsc && copyfiles -u 1 'src/
|
|
26
|
+
"build": "rimraf dist && yarn generate && tsc && copyfiles -u 1 'src/dashboard/**/*' dist/",
|
|
27
27
|
"build:dashboard": "vite build --config ./vite.config.mts",
|
|
28
28
|
"dev:dashboard": "vite --clearScreen false",
|
|
29
29
|
"start": "nodemon --watch src --exec ts-node test/dev-server.ts",
|
|
30
30
|
"generate": "graphql-codegen",
|
|
31
31
|
"test": "vitest run --bail 1",
|
|
32
|
-
"lint": "
|
|
32
|
+
"lint": "biome check ."
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"catch-unknown": "^2.0.0"
|
package/dist/ui/providers.ts
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
addActionBarDropdownMenuItem,
|
|
3
|
-
addActionBarItem,
|
|
4
|
-
ModalService,
|
|
5
|
-
} from '@vendure/admin-ui/core';
|
|
6
|
-
import { firstValueFrom, of } from 'rxjs';
|
|
7
|
-
import { map, catchError, take } from 'rxjs/operators';
|
|
8
|
-
import gql from 'graphql-tag';
|
|
9
|
-
|
|
10
|
-
export default [
|
|
11
|
-
// Product sync button in product list
|
|
12
|
-
addActionBarDropdownMenuItem({
|
|
13
|
-
id: 'qls-fulfillment-sync',
|
|
14
|
-
label: 'Synchronize with QLS',
|
|
15
|
-
locationId: 'product-list',
|
|
16
|
-
icon: 'resistor',
|
|
17
|
-
requiresPermission: ['QLSFullSync'],
|
|
18
|
-
onClick: (_, { dataService, notificationService }) => {
|
|
19
|
-
dataService
|
|
20
|
-
.mutate(
|
|
21
|
-
gql`
|
|
22
|
-
mutation TriggerQlsProductSync {
|
|
23
|
-
triggerQlsProductSync
|
|
24
|
-
}
|
|
25
|
-
`
|
|
26
|
-
)
|
|
27
|
-
.subscribe({
|
|
28
|
-
next: () => {
|
|
29
|
-
notificationService.notify({
|
|
30
|
-
message: `Triggered QLS full product sync...`,
|
|
31
|
-
type: 'success',
|
|
32
|
-
});
|
|
33
|
-
},
|
|
34
|
-
error: (err) => {
|
|
35
|
-
notificationService.notify({
|
|
36
|
-
message: err.message,
|
|
37
|
-
type: 'error',
|
|
38
|
-
});
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
},
|
|
42
|
-
}),
|
|
43
|
-
// QLS order link button on order detail page
|
|
44
|
-
addActionBarItem({
|
|
45
|
-
id: 'qls-visit-order',
|
|
46
|
-
label: 'QLS',
|
|
47
|
-
locationId: 'order-detail',
|
|
48
|
-
buttonColor: 'primary',
|
|
49
|
-
buttonStyle: 'outline',
|
|
50
|
-
icon: 'link',
|
|
51
|
-
buttonState: (context) => {
|
|
52
|
-
return context.entity$.pipe(
|
|
53
|
-
map((entity) => ({
|
|
54
|
-
visible: !!entity?.orderPlacedAt,
|
|
55
|
-
disabled: false,
|
|
56
|
-
}))
|
|
57
|
-
);
|
|
58
|
-
},
|
|
59
|
-
onClick: async (event, { route, dataService, notificationService }) => {
|
|
60
|
-
const orderId = route.snapshot.params.id;
|
|
61
|
-
const res = await firstValueFrom(
|
|
62
|
-
dataService.query(
|
|
63
|
-
gql`
|
|
64
|
-
query Order($orderId: ID!) {
|
|
65
|
-
order(id: $orderId) {
|
|
66
|
-
id
|
|
67
|
-
qlsOrderUrl
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
`,
|
|
71
|
-
{ orderId }
|
|
72
|
-
).single$
|
|
73
|
-
);
|
|
74
|
-
const url = (res as any).order.qlsOrderUrl;
|
|
75
|
-
if (url) {
|
|
76
|
-
window.open(url, '_blank');
|
|
77
|
-
} else {
|
|
78
|
-
notificationService.notify({
|
|
79
|
-
message: 'No QLS order found for this order.',
|
|
80
|
-
type: 'error',
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
}),
|
|
85
|
-
// QLS product link button on product variant detail page
|
|
86
|
-
addActionBarItem({
|
|
87
|
-
id: 'qls-visit-product',
|
|
88
|
-
label: 'QLS',
|
|
89
|
-
locationId: 'product-variant-detail',
|
|
90
|
-
buttonColor: 'primary',
|
|
91
|
-
buttonStyle: 'outline',
|
|
92
|
-
icon: 'link',
|
|
93
|
-
onClick: async (event, { route, dataService, notificationService }) => {
|
|
94
|
-
const variantId = route.snapshot.params.id;
|
|
95
|
-
const res = await firstValueFrom(
|
|
96
|
-
dataService.query(
|
|
97
|
-
gql`
|
|
98
|
-
query ProductVariant($id: ID!) {
|
|
99
|
-
productVariant(id: $id) {
|
|
100
|
-
id
|
|
101
|
-
qlsProductUrl
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
`,
|
|
105
|
-
{ id: variantId }
|
|
106
|
-
).single$
|
|
107
|
-
);
|
|
108
|
-
const url = (res as any).productVariant.qlsProductUrl;
|
|
109
|
-
if (url) {
|
|
110
|
-
window.open(url, '_blank');
|
|
111
|
-
} else {
|
|
112
|
-
notificationService.notify({
|
|
113
|
-
message: 'No QLS product found for this variant.',
|
|
114
|
-
type: 'error',
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
}),
|
|
119
|
-
// Push order to QLS button on order detail page
|
|
120
|
-
addActionBarDropdownMenuItem({
|
|
121
|
-
id: 'qls-fulfillment-push-order',
|
|
122
|
-
label: 'Push order to QLS',
|
|
123
|
-
locationId: 'order-detail',
|
|
124
|
-
icon: 'resistor',
|
|
125
|
-
requiresPermission: ['QLSFullSync'],
|
|
126
|
-
hasDivider: true,
|
|
127
|
-
onClick: async (
|
|
128
|
-
_,
|
|
129
|
-
{ route, dataService, notificationService, injector }
|
|
130
|
-
) => {
|
|
131
|
-
const orderId = route.snapshot.params.id;
|
|
132
|
-
const res = await firstValueFrom(
|
|
133
|
-
dataService.query(
|
|
134
|
-
gql`
|
|
135
|
-
query Order($orderId: ID!) {
|
|
136
|
-
order(id: $orderId) {
|
|
137
|
-
id
|
|
138
|
-
qlsOrderIds
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
`,
|
|
142
|
-
{ orderId }
|
|
143
|
-
).single$
|
|
144
|
-
);
|
|
145
|
-
if ((res as any).order.qlsOrderIds.length > 0) {
|
|
146
|
-
const modalService = injector.get(ModalService);
|
|
147
|
-
const confirmed = await firstValueFrom(
|
|
148
|
-
modalService.dialog({
|
|
149
|
-
title: 'Push order to QLS',
|
|
150
|
-
body: 'This order already exists in QLS. Are you sure you want to push it again?',
|
|
151
|
-
buttons: [
|
|
152
|
-
{ type: 'secondary', label: 'Cancel', returnValue: false },
|
|
153
|
-
{ type: 'primary', label: 'Push to QLS', returnValue: true },
|
|
154
|
-
],
|
|
155
|
-
})
|
|
156
|
-
);
|
|
157
|
-
if (!confirmed) return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
dataService
|
|
161
|
-
.mutate(
|
|
162
|
-
gql`
|
|
163
|
-
mutation PushOrderToQls($orderId: ID!) {
|
|
164
|
-
pushOrderToQls(orderId: $orderId)
|
|
165
|
-
}
|
|
166
|
-
`,
|
|
167
|
-
{ orderId }
|
|
168
|
-
)
|
|
169
|
-
.subscribe({
|
|
170
|
-
next: (result) => {
|
|
171
|
-
notificationService.notify({
|
|
172
|
-
message: (result as any).pushOrderToQls,
|
|
173
|
-
type: 'success',
|
|
174
|
-
});
|
|
175
|
-
},
|
|
176
|
-
error: (err) => {
|
|
177
|
-
notificationService.notify({
|
|
178
|
-
message: err.message,
|
|
179
|
-
type: 'error',
|
|
180
|
-
});
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
},
|
|
184
|
-
}),
|
|
185
|
-
addActionBarDropdownMenuItem({
|
|
186
|
-
id: 'qls-fulfillment-add-ean',
|
|
187
|
-
label: 'Add additional EAN to QLS',
|
|
188
|
-
locationId: 'product-variant-detail',
|
|
189
|
-
icon: 'resistor',
|
|
190
|
-
requiresPermission: ['QLSFullSync'],
|
|
191
|
-
onClick: async (
|
|
192
|
-
_,
|
|
193
|
-
{ route, dataService, notificationService, entity$ }
|
|
194
|
-
) => {
|
|
195
|
-
const ean = window.prompt('Enter additional EAN to add to QLS:');
|
|
196
|
-
if (!ean) {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
const variantId = route.snapshot.params.id;
|
|
200
|
-
try {
|
|
201
|
-
const variant = await firstValueFrom(entity$.pipe(take(1)));
|
|
202
|
-
const confirmed = window.confirm(
|
|
203
|
-
`Wil je EAN "${ean}" toevoegen aan "${variant?.name}"?`
|
|
204
|
-
);
|
|
205
|
-
if (!confirmed) {
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
} catch (err) {
|
|
209
|
-
notificationService.notify({
|
|
210
|
-
message: (err as any).message,
|
|
211
|
-
type: 'error',
|
|
212
|
-
});
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
dataService
|
|
216
|
-
.mutate(
|
|
217
|
-
gql`
|
|
218
|
-
mutation AddAdditionalEANSToQLS(
|
|
219
|
-
$variantId: ID!
|
|
220
|
-
$additionalEANS: [String!]!
|
|
221
|
-
) {
|
|
222
|
-
addAdditionalEANSToQLS(
|
|
223
|
-
variantId: $variantId
|
|
224
|
-
additionalEANS: $additionalEANS
|
|
225
|
-
)
|
|
226
|
-
}
|
|
227
|
-
`,
|
|
228
|
-
{ variantId, additionalEANS: [ean] }
|
|
229
|
-
)
|
|
230
|
-
.subscribe({
|
|
231
|
-
next: () => {
|
|
232
|
-
notificationService.notify({
|
|
233
|
-
message: `Added EAN ${ean} to QLS`,
|
|
234
|
-
type: 'success',
|
|
235
|
-
});
|
|
236
|
-
},
|
|
237
|
-
error: (err) => {
|
|
238
|
-
notificationService.notify({
|
|
239
|
-
message: err.message,
|
|
240
|
-
type: 'error',
|
|
241
|
-
});
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
},
|
|
245
|
-
}),
|
|
246
|
-
];
|