@pinelab/vendure-plugin-qls-fulfillment 2.1.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/api/api-extensions.js +12 -0
- package/dist/api/generated/graphql.d.ts +7 -0
- package/dist/api/qls-admin.resolver.d.ts +3 -1
- package/dist/api/qls-admin.resolver.js +28 -0
- package/dist/services/qls-order.service.d.ts +5 -0
- package/dist/services/qls-order.service.js +21 -0
- package/dist/services/qls-product.service.d.ts +10 -3
- package/dist/services/qls-product.service.js +49 -13
- package/dist/ui/providers.ts +79 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
# 2.2.1 (2026-07-15)
|
|
2
|
+
|
|
3
|
+
- Emit `StockMovementEvent` with corresponding `StockAdjustment` whenever a QLS webhook updates stock of a variant.
|
|
4
|
+
|
|
5
|
+
# 2.2.0 (2026-07-15)
|
|
6
|
+
|
|
7
|
+
- Added buttons on variant and order detail to view the product and order in QLS dashboard.
|
|
8
|
+
|
|
1
9
|
# 2.1.0 (2026-07-08)
|
|
2
10
|
|
|
3
11
|
- Added `addAdditionalEANSToQLS` admin mutation to manually add additional EANs/barcodes to existing QLS products.
|
|
@@ -11,6 +11,18 @@ exports.adminApiExtensions = (0, graphql_tag_1.default) `
|
|
|
11
11
|
QLS order id(s) for this Vendure order.
|
|
12
12
|
"""
|
|
13
13
|
qlsOrderIds: [String!]!
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Direct URL to the most recent QLS order for this Vendure order, or null if none exists.
|
|
17
|
+
"""
|
|
18
|
+
qlsOrderUrl: String
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
extend type ProductVariant {
|
|
22
|
+
"""
|
|
23
|
+
Direct URL to the QLS product for this Vendure variant, or null if none exists.
|
|
24
|
+
"""
|
|
25
|
+
qlsProductUrl: String
|
|
14
26
|
}
|
|
15
27
|
|
|
16
28
|
extend type Mutation {
|
|
@@ -42,6 +42,13 @@ export type Order = {
|
|
|
42
42
|
__typename?: 'Order';
|
|
43
43
|
/** QLS order id(s) for this Vendure order. */
|
|
44
44
|
qlsOrderIds: Array<Scalars['String']>;
|
|
45
|
+
/** Direct URL to the most recent QLS order for this Vendure order, or null if none exists. */
|
|
46
|
+
qlsOrderUrl?: Maybe<Scalars['String']>;
|
|
47
|
+
};
|
|
48
|
+
export type ProductVariant = {
|
|
49
|
+
__typename?: 'ProductVariant';
|
|
50
|
+
/** Direct URL to the QLS product for this Vendure variant, or null if none exists. */
|
|
51
|
+
qlsProductUrl?: Maybe<Scalars['String']>;
|
|
45
52
|
};
|
|
46
53
|
export type QlsServicePoint = {
|
|
47
54
|
__typename?: 'QlsServicePoint';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Order, RequestContext } from '@vendure/core';
|
|
1
|
+
import { Order, ProductVariant, RequestContext } from '@vendure/core';
|
|
2
2
|
import { QlsOrderService } from '../services/qls-order.service';
|
|
3
3
|
import { QlsProductService } from '../services/qls-product.service';
|
|
4
4
|
import { MutationAddAdditionalEansToQlsArgs, MutationPushOrderToQlsArgs } from './generated/graphql';
|
|
@@ -7,6 +7,8 @@ export declare class QlsAdminResolver {
|
|
|
7
7
|
private qlsOrderService;
|
|
8
8
|
constructor(qlsProductService: QlsProductService, qlsOrderService: QlsOrderService);
|
|
9
9
|
qlsOrderIds(ctx: RequestContext, order: Order): Promise<string[]>;
|
|
10
|
+
qlsOrderUrl(ctx: RequestContext, order: Order): Promise<string | null>;
|
|
11
|
+
qlsProductUrl(ctx: RequestContext, productVariant: ProductVariant): Promise<string | null>;
|
|
10
12
|
triggerQlsProductSync(ctx: RequestContext): Promise<boolean>;
|
|
11
13
|
addAdditionalEANSToQLS(ctx: RequestContext, input: MutationAddAdditionalEansToQlsArgs): Promise<string[]>;
|
|
12
14
|
pushOrderToQls(ctx: RequestContext, input: MutationPushOrderToQlsArgs): Promise<string>;
|
|
@@ -26,6 +26,12 @@ let QlsAdminResolver = class QlsAdminResolver {
|
|
|
26
26
|
async qlsOrderIds(ctx, order) {
|
|
27
27
|
return this.qlsOrderService.getQlsOrderIdsForOrder(ctx, order.id);
|
|
28
28
|
}
|
|
29
|
+
async qlsOrderUrl(ctx, order) {
|
|
30
|
+
return this.qlsOrderService.getQlsOrderUrl(ctx, order.id);
|
|
31
|
+
}
|
|
32
|
+
async qlsProductUrl(ctx, productVariant) {
|
|
33
|
+
return this.qlsProductService.getQlsProductUrl(ctx, productVariant);
|
|
34
|
+
}
|
|
29
35
|
async triggerQlsProductSync(ctx) {
|
|
30
36
|
await this.qlsProductService.triggerFullSync(ctx);
|
|
31
37
|
return true;
|
|
@@ -50,6 +56,28 @@ __decorate([
|
|
|
50
56
|
core_1.Order]),
|
|
51
57
|
__metadata("design:returntype", Promise)
|
|
52
58
|
], QlsAdminResolver.prototype, "qlsOrderIds", null);
|
|
59
|
+
__decorate([
|
|
60
|
+
(0, graphql_1.ResolveField)(),
|
|
61
|
+
(0, graphql_1.Resolver)('Order'),
|
|
62
|
+
(0, core_1.Allow)(permissions_1.qlsPushOrderPermission.Permission),
|
|
63
|
+
(0, core_1.Allow)(core_1.Permission.UpdateAdministrator),
|
|
64
|
+
__param(0, (0, core_1.Ctx)()),
|
|
65
|
+
__param(1, (0, graphql_1.Parent)()),
|
|
66
|
+
__metadata("design:type", Function),
|
|
67
|
+
__metadata("design:paramtypes", [core_1.RequestContext,
|
|
68
|
+
core_1.Order]),
|
|
69
|
+
__metadata("design:returntype", Promise)
|
|
70
|
+
], QlsAdminResolver.prototype, "qlsOrderUrl", null);
|
|
71
|
+
__decorate([
|
|
72
|
+
(0, graphql_1.ResolveField)(),
|
|
73
|
+
(0, graphql_1.Resolver)('ProductVariant'),
|
|
74
|
+
__param(0, (0, core_1.Ctx)()),
|
|
75
|
+
__param(1, (0, graphql_1.Parent)()),
|
|
76
|
+
__metadata("design:type", Function),
|
|
77
|
+
__metadata("design:paramtypes", [core_1.RequestContext,
|
|
78
|
+
core_1.ProductVariant]),
|
|
79
|
+
__metadata("design:returntype", Promise)
|
|
80
|
+
], QlsAdminResolver.prototype, "qlsProductUrl", null);
|
|
53
81
|
__decorate([
|
|
54
82
|
(0, graphql_1.Mutation)(),
|
|
55
83
|
(0, core_1.Transaction)(),
|
|
@@ -36,6 +36,11 @@ export declare class QlsOrderService implements OnModuleInit, OnApplicationBoots
|
|
|
36
36
|
* Get QLS order id(s) for a Vendure order (for Order.qlsOrderIds field).
|
|
37
37
|
*/
|
|
38
38
|
getQlsOrderIdsForOrder(ctx: RequestContext, orderId: ID): Promise<string[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Get the direct URL to the most recent QLS order for a Vendure order (for Order.qlsOrderUrl field).
|
|
41
|
+
* Returns the URL or null if no QLS order exists.
|
|
42
|
+
*/
|
|
43
|
+
getQlsOrderUrl(ctx: RequestContext, orderId: ID): Promise<string | null>;
|
|
39
44
|
getServicePoints(ctx: RequestContext, input: QlsServicePointSearchInput): Promise<QlsServicePoint[]>;
|
|
40
45
|
private getVendureOrderState;
|
|
41
46
|
}
|
|
@@ -281,9 +281,30 @@ let QlsOrderService = class QlsOrderService {
|
|
|
281
281
|
.getRepository(ctx, qls_order_entity_entity_1.QlsOrderEntity)
|
|
282
282
|
.find({
|
|
283
283
|
where: { vendureOrderId: orderId },
|
|
284
|
+
order: { createdAt: 'DESC' },
|
|
284
285
|
});
|
|
285
286
|
return entities.map((e) => String(e.qlsOrderId));
|
|
286
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Get the direct URL to the most recent QLS order for a Vendure order (for Order.qlsOrderUrl field).
|
|
290
|
+
* Returns the URL or null if no QLS order exists.
|
|
291
|
+
*/
|
|
292
|
+
async getQlsOrderUrl(ctx, orderId) {
|
|
293
|
+
const entities = await this.connection
|
|
294
|
+
.getRepository(ctx, qls_order_entity_entity_1.QlsOrderEntity)
|
|
295
|
+
.find({
|
|
296
|
+
where: { vendureOrderId: orderId },
|
|
297
|
+
order: { createdAt: 'DESC' },
|
|
298
|
+
});
|
|
299
|
+
if (entities.length === 0) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
const config = await Promise.resolve(this.options.getConfig(ctx));
|
|
303
|
+
if (!config) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
return `https://mijn.pakketdienstqls.nl/company/${config.companyId}/fulfillment/order/${entities[0].qlsOrderId}`;
|
|
307
|
+
}
|
|
287
308
|
async getServicePoints(ctx, input) {
|
|
288
309
|
const client = await (0, qls_client_1.getQlsClient)(ctx, this.options);
|
|
289
310
|
if (!client) {
|
|
@@ -55,15 +55,21 @@ export declare class QlsProductService implements OnModuleInit, OnApplicationBoo
|
|
|
55
55
|
* Trigger a product sync job for particular product variants
|
|
56
56
|
*/
|
|
57
57
|
triggerSyncVariants(ctx: RequestContext, productVariantIds: ID[]): Promise<import("@vendure/core/dist/job-queue/subscribable-job").SubscribableJob<QlsProductJobData> | undefined>;
|
|
58
|
+
/**
|
|
59
|
+
* Get the direct URL to the QLS product for a Vendure product variant.
|
|
60
|
+
* Returns the URL or null if no QLS product exists.
|
|
61
|
+
*/
|
|
62
|
+
getQlsProductUrl(ctx: RequestContext, productVariant: ProductVariant): Promise<string | null>;
|
|
58
63
|
/**
|
|
59
64
|
* Add additional EANs/barcodes to an existing QLS product.
|
|
60
65
|
* Removal of EANs must be done in QLS itself.
|
|
61
66
|
*/
|
|
62
67
|
addAdditionalEANsToQls(ctx: RequestContext, variantId: ID, additionalEANs: string[]): Promise<string[]>;
|
|
63
68
|
/**
|
|
64
|
-
* Update the stock level for a variant based on the given available stock
|
|
69
|
+
* Update the stock level for a variant based on the given available stock.
|
|
70
|
+
* Returns true if the stock changed
|
|
65
71
|
*/
|
|
66
|
-
updateStockBySku(ctx: RequestContext, sku: string, availableStock: number): Promise<
|
|
72
|
+
updateStockBySku(ctx: RequestContext, sku: string, availableStock: number): Promise<boolean>;
|
|
67
73
|
/**
|
|
68
74
|
* Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
|
|
69
75
|
*/
|
|
@@ -80,7 +86,8 @@ export declare class QlsProductService implements OnModuleInit, OnApplicationBoo
|
|
|
80
86
|
*/
|
|
81
87
|
private getAllVariants;
|
|
82
88
|
/**
|
|
83
|
-
* Update stock level for a variant based on the given available stock
|
|
89
|
+
* Update stock level for a variant based on the given available stock.
|
|
90
|
+
* Returns true if the stock changed.
|
|
84
91
|
*/
|
|
85
92
|
private updateStock;
|
|
86
93
|
}
|
|
@@ -269,6 +269,21 @@ let QlsProductService = class QlsProductService {
|
|
|
269
269
|
productVariantIds,
|
|
270
270
|
}, { retries: 1 });
|
|
271
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Get the direct URL to the QLS product for a Vendure product variant.
|
|
274
|
+
* Returns the URL or null if no QLS product exists.
|
|
275
|
+
*/
|
|
276
|
+
async getQlsProductUrl(ctx, productVariant) {
|
|
277
|
+
const qlsProductId = productVariant.customFields?.qlsProductId;
|
|
278
|
+
if (!qlsProductId) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const config = await Promise.resolve(this.options.getConfig(ctx));
|
|
282
|
+
if (!config) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
return `https://mijn.pakketdienstqls.nl/company/${config.companyId}/fulfillment/product/${qlsProductId}`;
|
|
286
|
+
}
|
|
272
287
|
/**
|
|
273
288
|
* Add additional EANs/barcodes to an existing QLS product.
|
|
274
289
|
* Removal of EANs must be done in QLS itself.
|
|
@@ -297,20 +312,22 @@ let QlsProductService = class QlsProductService {
|
|
|
297
312
|
return eansToAdd;
|
|
298
313
|
}
|
|
299
314
|
/**
|
|
300
|
-
* Update the stock level for a variant based on the given available stock
|
|
315
|
+
* Update the stock level for a variant based on the given available stock.
|
|
316
|
+
* Returns true if the stock changed
|
|
301
317
|
*/
|
|
302
318
|
async updateStockBySku(ctx, sku, availableStock) {
|
|
303
319
|
const result = await this.variantService.findAll(ctx, {
|
|
304
320
|
filter: { sku: { eq: sku } },
|
|
305
321
|
});
|
|
306
322
|
if (!result.items.length) {
|
|
307
|
-
|
|
323
|
+
core_1.Logger.info(`Variant with sku '${sku}' not found, not updating stock`, constants_1.loggerCtx);
|
|
324
|
+
return false;
|
|
308
325
|
}
|
|
309
326
|
const variant = result.items[0];
|
|
310
327
|
if (result.items.length > 1) {
|
|
311
328
|
core_1.Logger.error(`Multiple variants found for sku '${sku}', using '${variant.id}'`, constants_1.loggerCtx);
|
|
312
329
|
}
|
|
313
|
-
await this.updateStock(ctx, variant.id, availableStock);
|
|
330
|
+
return await this.updateStock(ctx, variant.id, availableStock);
|
|
314
331
|
}
|
|
315
332
|
/**
|
|
316
333
|
* Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
|
|
@@ -416,24 +433,43 @@ let QlsProductService = class QlsProductService {
|
|
|
416
433
|
return allVariants;
|
|
417
434
|
}
|
|
418
435
|
/**
|
|
419
|
-
* Update stock level for a variant based on the given available stock
|
|
436
|
+
* Update stock level for a variant based on the given available stock.
|
|
437
|
+
* Returns true if the stock changed.
|
|
420
438
|
*/
|
|
421
439
|
async updateStock(ctx, variantId, availableStock) {
|
|
422
440
|
if (!this.options.productSync.synchronizeStockLevels) {
|
|
423
441
|
core_1.Logger.warn(`Stock sync disabled. Not updating stock for variant '${variantId}'`, constants_1.loggerCtx);
|
|
424
|
-
return;
|
|
442
|
+
return false;
|
|
425
443
|
}
|
|
426
444
|
// Find default Stock Location
|
|
427
445
|
const defaultStockLocation = await this.stockLocationService.defaultStockLocation(ctx);
|
|
428
|
-
// Get current stock level
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
446
|
+
// Get current stock level
|
|
447
|
+
const stockLevel = await this.stockLevelService.getStockLevel(ctx, variantId, defaultStockLocation.id);
|
|
448
|
+
const delta = availableStock - stockLevel.stockOnHand;
|
|
449
|
+
if (delta !== 0) {
|
|
450
|
+
const adjustment = new core_1.StockAdjustment({
|
|
451
|
+
quantity: delta,
|
|
452
|
+
stockLocation: { id: defaultStockLocation.id },
|
|
453
|
+
productVariant: { id: variantId },
|
|
454
|
+
});
|
|
455
|
+
// Update stock level
|
|
456
|
+
await this.connection.getRepository(ctx, core_1.StockLevel).save({
|
|
457
|
+
id: stockLevel.id,
|
|
458
|
+
stockOnHand: availableStock,
|
|
459
|
+
stockAllocated: 0, // Reset allocations, because allocation is handled by QLS
|
|
460
|
+
});
|
|
461
|
+
await this.eventBus.publish(new core_1.StockMovementEvent(ctx, [adjustment]));
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
// No stock change, but still reset allocations to ensure consistency
|
|
465
|
+
await this.connection.getRepository(ctx, core_1.StockLevel).save({
|
|
466
|
+
id: stockLevel.id,
|
|
467
|
+
stockOnHand: availableStock,
|
|
468
|
+
stockAllocated: 0, // Reset allocations, because allocation is handled by QLS
|
|
469
|
+
});
|
|
470
|
+
}
|
|
436
471
|
core_1.Logger.info(`Updated stock for variant ${variantId} to ${availableStock}`, constants_1.loggerCtx);
|
|
472
|
+
return delta !== 0;
|
|
437
473
|
}
|
|
438
474
|
};
|
|
439
475
|
exports.QlsProductService = QlsProductService;
|
package/dist/ui/providers.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
addActionBarDropdownMenuItem,
|
|
3
|
+
addActionBarItem,
|
|
3
4
|
ModalService,
|
|
4
5
|
} from '@vendure/admin-ui/core';
|
|
5
|
-
import { firstValueFrom } from 'rxjs';
|
|
6
|
+
import { firstValueFrom, of } from 'rxjs';
|
|
7
|
+
import { map, catchError } from 'rxjs/operators';
|
|
6
8
|
import gql from 'graphql-tag';
|
|
7
9
|
|
|
8
10
|
export default [
|
|
@@ -38,6 +40,82 @@ export default [
|
|
|
38
40
|
});
|
|
39
41
|
},
|
|
40
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
|
+
}),
|
|
41
119
|
// Push order to QLS button on order detail page
|
|
42
120
|
addActionBarDropdownMenuItem({
|
|
43
121
|
id: 'qls-fulfillment-push-order',
|