@pinelab/vendure-plugin-qls-fulfillment 3.2.0 → 3.3.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 +4 -0
- package/README.md +2 -0
- package/dist/api/qls-webhooks-controller.d.ts +2 -2
- package/dist/api/qls-webhooks-controller.js +6 -0
- package/dist/custom-fields.d.ts +2 -0
- package/dist/custom-fields.js +32 -0
- package/dist/lib/client-types.d.ts +20 -0
- package/dist/services/qls-order.service.d.ts +12 -1
- package/dist/services/qls-order.service.js +48 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -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
|
|
|
@@ -3,7 +3,7 @@ import { Request } from 'express';
|
|
|
3
3
|
import { QlsProductService } from '../services/qls-product.service';
|
|
4
4
|
import { QlsPluginOptions } from '../types';
|
|
5
5
|
import { QlsOrderService } from '../services/qls-order.service';
|
|
6
|
-
import { IncomingOrderWebhook, IncomingStockWebhook } from '../lib/client-types';
|
|
6
|
+
import { IncomingOrderWebhook, IncomingShipmentWebhook, 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
|
}
|
|
@@ -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
|
+
}
|
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
|
];
|
|
@@ -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
|
|
@@ -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;
|
|
@@ -31,6 +31,17 @@ export declare class QlsOrderService implements OnModuleInit, OnApplicationBoots
|
|
|
31
31
|
* Update the status of an order in QLS based on the given order code and status
|
|
32
32
|
*/
|
|
33
33
|
handleOrderStatusUpdate(ctx: RequestContext, body: IncomingOrderWebhook): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Store the tracking code and tracking URL from an incoming `shipment.barcode` webhook
|
|
36
|
+
* on the order's custom fields. Codes/URLs are accumulated, because an order can have
|
|
37
|
+
* multiple shipments/parcels.
|
|
38
|
+
*/
|
|
39
|
+
handleShipmentBarcodeWebhook(ctx: RequestContext, body: IncomingShipmentWebhook): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* The root `barcode`/`tracking_url` fields on the webhook payload are sometimes not yet
|
|
42
|
+
* populated, so fall back to the nested shipment with the same id under `deliveries[].shipments[]`.
|
|
43
|
+
*/
|
|
44
|
+
private getShipmentTrackingInfo;
|
|
34
45
|
triggerPushOrder(ctx: RequestContext, orderId: ID, orderCode?: string): Promise<Job<QlsOrderJobData> | undefined>;
|
|
35
46
|
/**
|
|
36
47
|
* Get QLS order id(s) for a Vendure order (for Order.qlsOrderIds field).
|
|
@@ -260,6 +260,54 @@ let QlsOrderService = class QlsOrderService {
|
|
|
260
260
|
await this.orderService.transitionToState(ctx, order.id, vendureOrderState);
|
|
261
261
|
core_2.Logger.info(`Successfully updated order '${orderCode}' to '${vendureOrderState}'`, constants_1.loggerCtx);
|
|
262
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Store the tracking code and tracking URL from an incoming `shipment.barcode` webhook
|
|
265
|
+
* on the order's custom fields. Codes/URLs are accumulated, because an order can have
|
|
266
|
+
* multiple shipments/parcels.
|
|
267
|
+
*/
|
|
268
|
+
async handleShipmentBarcodeWebhook(ctx, body) {
|
|
269
|
+
const orderCode = body.reference;
|
|
270
|
+
const { barcode, trackingUrl } = this.getShipmentTrackingInfo(body);
|
|
271
|
+
if (!barcode) {
|
|
272
|
+
core_2.Logger.info(`Ignoring shipment.barcode webhook for order '${orderCode}', because no barcode was found in the payload`, constants_1.loggerCtx);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const order = await this.orderService.findOneByCode(ctx, orderCode, []);
|
|
276
|
+
if (!order) {
|
|
277
|
+
return core_2.Logger.warn(`Order with code '${orderCode}' not found, ignoring shipment.barcode webhook`, constants_1.loggerCtx);
|
|
278
|
+
}
|
|
279
|
+
const existingCodes = order.customFields.qlsTrackingCodes ?? [];
|
|
280
|
+
if (existingCodes.includes(barcode)) {
|
|
281
|
+
core_2.Logger.info(`Tracking code '${barcode}' already stored for order '${orderCode}', ignoring duplicate shipment.barcode webhook`, constants_1.loggerCtx);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const existingUrls = order.customFields.qlsTrackingUrls ?? [];
|
|
285
|
+
await this.connection.getRepository(ctx, core_2.Order).update({ id: order.id }, {
|
|
286
|
+
customFields: {
|
|
287
|
+
qlsTrackingCodes: [...existingCodes, barcode],
|
|
288
|
+
qlsTrackingUrls: trackingUrl
|
|
289
|
+
? [...existingUrls, trackingUrl]
|
|
290
|
+
: existingUrls,
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
core_2.Logger.info(`Stored tracking code '${barcode}' for order '${orderCode}'`, constants_1.loggerCtx);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* The root `barcode`/`tracking_url` fields on the webhook payload are sometimes not yet
|
|
297
|
+
* populated, so fall back to the nested shipment with the same id under `deliveries[].shipments[]`.
|
|
298
|
+
*/
|
|
299
|
+
getShipmentTrackingInfo(body) {
|
|
300
|
+
if (body.barcode) {
|
|
301
|
+
return { barcode: body.barcode, trackingUrl: body.tracking_url };
|
|
302
|
+
}
|
|
303
|
+
const nestedShipment = body.deliveries
|
|
304
|
+
?.flatMap((delivery) => delivery.shipments ?? [])
|
|
305
|
+
.find((shipment) => shipment.id === body.id);
|
|
306
|
+
return {
|
|
307
|
+
barcode: nestedShipment?.barcode ?? null,
|
|
308
|
+
trackingUrl: nestedShipment?.tracking_url ?? null,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
263
311
|
async triggerPushOrder(ctx, orderId, orderCode) {
|
|
264
312
|
const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
|
|
265
313
|
if (!client) {
|