@pinelab/vendure-plugin-qls-fulfillment 2.3.0 → 3.1.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 +13 -0
- package/dist/api/qls-webhooks-controller.js +1 -1
- package/dist/dashboard/components/AddAdditionalEanButton.tsx +121 -0
- package/dist/dashboard/components/PushOrderToQlsMenuItem.tsx +99 -0
- package/dist/dashboard/components/SyncProductsMenuItem.tsx +31 -0
- package/dist/dashboard/components/VisitQlsOrderButton.tsx +52 -0
- package/dist/dashboard/components/VisitQlsProductButton.tsx +49 -0
- package/dist/dashboard/index.tsx +48 -0
- package/dist/dashboard/qls-dashboard.graphql.ts +49 -0
- package/dist/lib/client-types.d.ts +1 -1
- package/dist/qls-plugin.js +1 -0
- package/dist/services/qls-product.service.d.ts +6 -3
- package/dist/services/qls-product.service.js +28 -14
- package/package.json +4 -2
- package/dist/ui/providers.ts +0 -246
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
# 3.1.0 (2026-08-12)
|
|
2
|
+
|
|
3
|
+
- Trigger `saveAdditionalVariantData` from incoming stock webhooks using the webhook payload directly.
|
|
4
|
+
- Avoid fetching the QLS product entity during this flow.
|
|
5
|
+
- Add webhook coverage for additional variant data.
|
|
6
|
+
|
|
7
|
+
# 3.0.0 (2026-07-29)
|
|
8
|
+
|
|
9
|
+
- Migrated to React Dashboard.
|
|
10
|
+
- Added React Dashboard action bar items for product sync, order link, push order, variant link, and adding additional EANs.
|
|
11
|
+
- Kept legacy Angular Admin UI action bar items for backward compatibility.
|
|
12
|
+
- Corrected the UI permission for the "Push order to QLS" action to `QLSPushOrder`.
|
|
13
|
+
|
|
1
14
|
# 2.3.0 (2026-08-05)
|
|
2
15
|
|
|
3
16
|
- Extra prompt before adding an additional EAN
|
|
@@ -40,7 +40,7 @@ let QlsWebhooksController = class QlsWebhooksController {
|
|
|
40
40
|
return core_1.Logger.error(`Incoming webhook with invalid channel token for channel '${channelToken}' to '${request.url}'`, constants_1.loggerCtx, JSON.stringify(body));
|
|
41
41
|
}
|
|
42
42
|
if (isStockWebhook(body)) {
|
|
43
|
-
await this.qlsProductService.
|
|
43
|
+
await this.qlsProductService.handleStockWebhook(ctx, body);
|
|
44
44
|
}
|
|
45
45
|
else if (isOrderWebhook(body)) {
|
|
46
46
|
await this.qlsOrderService.handleOrderStatusUpdate(ctx, body);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
api,
|
|
3
|
+
Button,
|
|
4
|
+
ConfirmationDialog,
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogFooter,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
Input,
|
|
12
|
+
Label,
|
|
13
|
+
} from '@vendure/dashboard';
|
|
14
|
+
import { useMutation } from '@tanstack/react-query';
|
|
15
|
+
import { QrCode } from 'lucide-react';
|
|
16
|
+
import { useState } from 'react';
|
|
17
|
+
import { toast } from 'sonner';
|
|
18
|
+
|
|
19
|
+
import { addAdditionalEanToQlsDocument } from '../qls-dashboard.graphql';
|
|
20
|
+
|
|
21
|
+
interface AddAdditionalEanButtonProps {
|
|
22
|
+
context: { entity?: any };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Action bar button on the product variant detail page that prompts
|
|
27
|
+
* for an additional EAN and sends it to QLS.
|
|
28
|
+
*
|
|
29
|
+
* NOTE: This is deliberately a regular action bar button and NOT a dropdown
|
|
30
|
+
* (`type: 'dropdown'`) action bar item. Dropdown items are rendered inside a
|
|
31
|
+
* Base UI menu popup, which unmounts its children when the menu closes, and
|
|
32
|
+
* while the (modal) menu is open it intercepts keyboard events. Both make it
|
|
33
|
+
* impossible to have a working dialog with a text input inside a dropdown
|
|
34
|
+
* item. Vendure itself only uses button-only AlertDialogs inside dropdown
|
|
35
|
+
* menus; dialogs with inputs are always opened from outside a menu.
|
|
36
|
+
*/
|
|
37
|
+
export function AddAdditionalEanButton({
|
|
38
|
+
context,
|
|
39
|
+
}: AddAdditionalEanButtonProps) {
|
|
40
|
+
const variantId = context.entity?.id;
|
|
41
|
+
const variantName = context.entity?.name;
|
|
42
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
43
|
+
const [ean, setEan] = useState('');
|
|
44
|
+
|
|
45
|
+
const { mutate: addEan, isPending } = useMutation({
|
|
46
|
+
mutationFn: () =>
|
|
47
|
+
api.mutate(addAdditionalEanToQlsDocument, {
|
|
48
|
+
variantId: variantId!,
|
|
49
|
+
additionalEANS: [ean],
|
|
50
|
+
}),
|
|
51
|
+
onSuccess: () => {
|
|
52
|
+
toast.success(`Added EAN ${ean} to QLS`);
|
|
53
|
+
setEan('');
|
|
54
|
+
setDialogOpen(false);
|
|
55
|
+
},
|
|
56
|
+
onError: (err: Error) => {
|
|
57
|
+
toast.error(err.message ?? 'Failed to add EAN to QLS');
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const handleConfirm = () => {
|
|
62
|
+
if (!ean.trim()) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
addEan();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
<Button
|
|
71
|
+
variant="outline"
|
|
72
|
+
disabled={!variantId}
|
|
73
|
+
onClick={() => setDialogOpen(true)}
|
|
74
|
+
>
|
|
75
|
+
<QrCode className="mr-2 h-4 w-4" />
|
|
76
|
+
Add EAN
|
|
77
|
+
</Button>
|
|
78
|
+
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
|
79
|
+
<DialogContent>
|
|
80
|
+
<DialogHeader>
|
|
81
|
+
<DialogTitle>Add additional EAN to QLS</DialogTitle>
|
|
82
|
+
<DialogDescription>
|
|
83
|
+
Enter the additional EAN to add to this variant in QLS.
|
|
84
|
+
</DialogDescription>
|
|
85
|
+
</DialogHeader>
|
|
86
|
+
<div className="grid gap-4 py-4">
|
|
87
|
+
<div className="grid gap-2">
|
|
88
|
+
<Label htmlFor="ean">EAN</Label>
|
|
89
|
+
<Input
|
|
90
|
+
id="ean"
|
|
91
|
+
value={ean}
|
|
92
|
+
onChange={(e) => setEan(e.target.value)}
|
|
93
|
+
placeholder="EAN"
|
|
94
|
+
disabled={isPending}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<DialogFooter>
|
|
99
|
+
<Button
|
|
100
|
+
type="button"
|
|
101
|
+
variant="secondary"
|
|
102
|
+
disabled={isPending}
|
|
103
|
+
onClick={() => setDialogOpen(false)}
|
|
104
|
+
>
|
|
105
|
+
Cancel
|
|
106
|
+
</Button>
|
|
107
|
+
<ConfirmationDialog
|
|
108
|
+
title="Weet je het zeker?"
|
|
109
|
+
description={`Wil je EAN "${ean}" toevoegen aan product "${variantName}"?`}
|
|
110
|
+
onConfirm={handleConfirm}
|
|
111
|
+
>
|
|
112
|
+
<Button type="button" disabled={!ean.trim() || isPending}>
|
|
113
|
+
Add EAN
|
|
114
|
+
</Button>
|
|
115
|
+
</ConfirmationDialog>
|
|
116
|
+
</DialogFooter>
|
|
117
|
+
</DialogContent>
|
|
118
|
+
</Dialog>
|
|
119
|
+
</>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
api,
|
|
3
|
+
Button,
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogDescription,
|
|
7
|
+
DialogFooter,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
DropdownMenuItem,
|
|
11
|
+
} from '@vendure/dashboard';
|
|
12
|
+
import { useQuery, useMutation } from '@tanstack/react-query';
|
|
13
|
+
import { RefreshCw } from 'lucide-react';
|
|
14
|
+
import { useState } from 'react';
|
|
15
|
+
import { toast } from 'sonner';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
orderQlsIdsDocument,
|
|
19
|
+
pushOrderToQlsDocument,
|
|
20
|
+
} from '../qls-dashboard.graphql';
|
|
21
|
+
|
|
22
|
+
interface PushOrderToQlsMenuItemProps {
|
|
23
|
+
context: { entity?: any };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Dropdown action bar item on the order detail page that pushes the order
|
|
28
|
+
* to QLS. If the order already exists in QLS, a confirmation dialog is shown.
|
|
29
|
+
*/
|
|
30
|
+
export function PushOrderToQlsMenuItem({
|
|
31
|
+
context,
|
|
32
|
+
}: PushOrderToQlsMenuItemProps) {
|
|
33
|
+
const orderId = context.entity?.id;
|
|
34
|
+
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
35
|
+
|
|
36
|
+
const { data: orderData } = useQuery({
|
|
37
|
+
queryKey: ['qls-order-ids', orderId],
|
|
38
|
+
queryFn: () => api.query(orderQlsIdsDocument, { id: orderId! }),
|
|
39
|
+
enabled: !!orderId,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const { mutate: pushOrder, isPending } = useMutation({
|
|
43
|
+
mutationFn: () => api.mutate(pushOrderToQlsDocument, { orderId: orderId! }),
|
|
44
|
+
onSuccess: (message) => {
|
|
45
|
+
toast.success(message ?? 'Order pushed to QLS');
|
|
46
|
+
setConfirmOpen(false);
|
|
47
|
+
},
|
|
48
|
+
onError: (err: Error) => {
|
|
49
|
+
toast.error(err.message ?? 'Failed to push order to QLS');
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const handleClick = () => {
|
|
54
|
+
const existingIds = orderData?.order?.qlsOrderIds ?? [];
|
|
55
|
+
if (existingIds.length > 0) {
|
|
56
|
+
setConfirmOpen(true);
|
|
57
|
+
} else {
|
|
58
|
+
pushOrder();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<>
|
|
64
|
+
<DropdownMenuItem disabled={!orderId || isPending} onClick={handleClick}>
|
|
65
|
+
<RefreshCw
|
|
66
|
+
className={`mr-2 h-4 w-4 ${isPending ? 'animate-spin' : ''}`}
|
|
67
|
+
/>
|
|
68
|
+
Push order to QLS
|
|
69
|
+
</DropdownMenuItem>
|
|
70
|
+
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
|
71
|
+
<DialogContent>
|
|
72
|
+
<DialogHeader>
|
|
73
|
+
<DialogTitle>Push order to QLS</DialogTitle>
|
|
74
|
+
<DialogDescription>
|
|
75
|
+
This order already exists in QLS. Are you sure you want to push it
|
|
76
|
+
again?
|
|
77
|
+
</DialogDescription>
|
|
78
|
+
</DialogHeader>
|
|
79
|
+
<DialogFooter>
|
|
80
|
+
<Button
|
|
81
|
+
type="button"
|
|
82
|
+
variant="secondary"
|
|
83
|
+
onClick={() => setConfirmOpen(false)}
|
|
84
|
+
>
|
|
85
|
+
Cancel
|
|
86
|
+
</Button>
|
|
87
|
+
<Button
|
|
88
|
+
type="button"
|
|
89
|
+
disabled={isPending}
|
|
90
|
+
onClick={() => pushOrder()}
|
|
91
|
+
>
|
|
92
|
+
Push to QLS
|
|
93
|
+
</Button>
|
|
94
|
+
</DialogFooter>
|
|
95
|
+
</DialogContent>
|
|
96
|
+
</Dialog>
|
|
97
|
+
</>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { api, DropdownMenuItem } from '@vendure/dashboard';
|
|
2
|
+
import { useMutation } from '@tanstack/react-query';
|
|
3
|
+
import { RefreshCw } from 'lucide-react';
|
|
4
|
+
import { toast } from 'sonner';
|
|
5
|
+
|
|
6
|
+
import { triggerQlsProductSyncDocument } from '../qls-dashboard.graphql';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Dropdown action bar item on the product list that triggers a full
|
|
10
|
+
* product synchronization with QLS.
|
|
11
|
+
*/
|
|
12
|
+
export function SyncProductsMenuItem() {
|
|
13
|
+
const { mutate: sync, isPending } = useMutation({
|
|
14
|
+
mutationFn: () => api.mutate(triggerQlsProductSyncDocument, {}),
|
|
15
|
+
onSuccess: () => {
|
|
16
|
+
toast.success('Triggered QLS full product sync...');
|
|
17
|
+
},
|
|
18
|
+
onError: (err: Error) => {
|
|
19
|
+
toast.error(err.message ?? 'Failed to trigger QLS product sync');
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<DropdownMenuItem disabled={isPending} onClick={() => sync()}>
|
|
25
|
+
<RefreshCw
|
|
26
|
+
className={`mr-2 h-4 w-4 ${isPending ? 'animate-spin' : ''}`}
|
|
27
|
+
/>
|
|
28
|
+
Synchronize with QLS
|
|
29
|
+
</DropdownMenuItem>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { api, Button } from '@vendure/dashboard';
|
|
2
|
+
import { ExternalLink } from 'lucide-react';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { toast } from 'sonner';
|
|
5
|
+
|
|
6
|
+
import { orderQlsUrlDocument } from '../qls-dashboard.graphql';
|
|
7
|
+
|
|
8
|
+
interface VisitQlsOrderButtonProps {
|
|
9
|
+
context: { entity?: any };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Action bar button on the order detail page that opens the linked QLS order
|
|
14
|
+
* in a new tab. Only renders when the order has been placed.
|
|
15
|
+
*/
|
|
16
|
+
export function VisitQlsOrderButton({ context }: VisitQlsOrderButtonProps) {
|
|
17
|
+
const orderId = context.entity?.id;
|
|
18
|
+
const orderPlacedAt = context.entity?.orderPlacedAt;
|
|
19
|
+
const [isPending, setIsPending] = useState(false);
|
|
20
|
+
|
|
21
|
+
if (!orderPlacedAt) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const handleClick = async () => {
|
|
26
|
+
if (!orderId) {
|
|
27
|
+
toast.error('No order selected.');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
setIsPending(true);
|
|
31
|
+
try {
|
|
32
|
+
const result = await api.query(orderQlsUrlDocument, { id: orderId });
|
|
33
|
+
const url = result.order?.qlsOrderUrl;
|
|
34
|
+
if (url) {
|
|
35
|
+
window.open(url, '_blank');
|
|
36
|
+
} else {
|
|
37
|
+
toast.error('No QLS order found for this order.');
|
|
38
|
+
}
|
|
39
|
+
} catch (err: any) {
|
|
40
|
+
toast.error(err?.message ?? 'Failed to load QLS order URL');
|
|
41
|
+
} finally {
|
|
42
|
+
setIsPending(false);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Button variant="outline" disabled={isPending} onClick={handleClick}>
|
|
48
|
+
<ExternalLink className="mr-2 h-4 w-4" />
|
|
49
|
+
QLS
|
|
50
|
+
</Button>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { api, Button } from '@vendure/dashboard';
|
|
2
|
+
import { ExternalLink } from 'lucide-react';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { toast } from 'sonner';
|
|
5
|
+
|
|
6
|
+
import { productVariantQlsUrlDocument } from '../qls-dashboard.graphql';
|
|
7
|
+
|
|
8
|
+
interface VisitQlsProductButtonProps {
|
|
9
|
+
context: { entity?: any };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Action bar button on the product variant detail page that opens the linked
|
|
14
|
+
* QLS product in a new tab.
|
|
15
|
+
*/
|
|
16
|
+
export function VisitQlsProductButton({ context }: VisitQlsProductButtonProps) {
|
|
17
|
+
const variantId = context.entity?.id;
|
|
18
|
+
const [isPending, setIsPending] = useState(false);
|
|
19
|
+
|
|
20
|
+
const handleClick = async () => {
|
|
21
|
+
if (!variantId) {
|
|
22
|
+
toast.error('No variant selected.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
setIsPending(true);
|
|
26
|
+
try {
|
|
27
|
+
const result = await api.query(productVariantQlsUrlDocument, {
|
|
28
|
+
id: variantId,
|
|
29
|
+
});
|
|
30
|
+
const url = result.productVariant?.qlsProductUrl;
|
|
31
|
+
if (url) {
|
|
32
|
+
window.open(url, '_blank');
|
|
33
|
+
} else {
|
|
34
|
+
toast.error('No QLS product found for this variant.');
|
|
35
|
+
}
|
|
36
|
+
} catch (err: any) {
|
|
37
|
+
toast.error(err?.message ?? 'Failed to load QLS product URL');
|
|
38
|
+
} finally {
|
|
39
|
+
setIsPending(false);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Button variant="outline" disabled={isPending} onClick={handleClick}>
|
|
45
|
+
<ExternalLink className="mr-2 h-4 w-4" />
|
|
46
|
+
QLS
|
|
47
|
+
</Button>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { defineDashboardExtension } from '@vendure/dashboard';
|
|
2
|
+
|
|
3
|
+
import { AddAdditionalEanButton } from './components/AddAdditionalEanButton';
|
|
4
|
+
import { PushOrderToQlsMenuItem } from './components/PushOrderToQlsMenuItem';
|
|
5
|
+
import { SyncProductsMenuItem } from './components/SyncProductsMenuItem';
|
|
6
|
+
import { VisitQlsOrderButton } from './components/VisitQlsOrderButton';
|
|
7
|
+
import { VisitQlsProductButton } from './components/VisitQlsProductButton';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* React Dashboard extension for the QLS fulfillment plugin.
|
|
11
|
+
*
|
|
12
|
+
* Replicates the legacy Angular action-bar items as dashboard action bar items.
|
|
13
|
+
*/
|
|
14
|
+
defineDashboardExtension({
|
|
15
|
+
actionBarItems: [
|
|
16
|
+
{
|
|
17
|
+
id: 'qls-sync-products',
|
|
18
|
+
pageId: 'product-list',
|
|
19
|
+
type: 'dropdown',
|
|
20
|
+
requiresPermission: 'QLSFullSync',
|
|
21
|
+
component: () => <SyncProductsMenuItem />,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'qls-visit-order',
|
|
25
|
+
pageId: 'order-detail',
|
|
26
|
+
component: ({ context }) => <VisitQlsOrderButton context={context} />,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'qls-push-order',
|
|
30
|
+
pageId: 'order-detail',
|
|
31
|
+
type: 'dropdown',
|
|
32
|
+
requiresPermission: 'QLSPushOrder',
|
|
33
|
+
component: ({ context }) => <PushOrderToQlsMenuItem context={context} />,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'qls-visit-product',
|
|
37
|
+
pageId: 'product-variant-detail',
|
|
38
|
+
component: ({ context }) => <VisitQlsProductButton context={context} />,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'qls-add-ean',
|
|
42
|
+
pageId: 'product-variant-detail',
|
|
43
|
+
// NOTE: intentionally not `type: 'dropdown'`, see AddAdditionalEanButton
|
|
44
|
+
requiresPermission: 'QLSFullSync',
|
|
45
|
+
component: ({ context }) => <AddAdditionalEanButton context={context} />,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { graphql } from '@/gql';
|
|
2
|
+
|
|
3
|
+
export const orderQlsUrlDocument = graphql(`
|
|
4
|
+
query OrderQlsUrl($id: ID!) {
|
|
5
|
+
order(id: $id) {
|
|
6
|
+
id
|
|
7
|
+
qlsOrderUrl
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
`);
|
|
11
|
+
|
|
12
|
+
export const orderQlsIdsDocument = graphql(`
|
|
13
|
+
query OrderQlsIds($id: ID!) {
|
|
14
|
+
order(id: $id) {
|
|
15
|
+
id
|
|
16
|
+
qlsOrderIds
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
`);
|
|
20
|
+
|
|
21
|
+
export const productVariantQlsUrlDocument = graphql(`
|
|
22
|
+
query ProductVariantQlsUrl($id: ID!) {
|
|
23
|
+
productVariant(id: $id) {
|
|
24
|
+
id
|
|
25
|
+
qlsProductUrl
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
`);
|
|
29
|
+
|
|
30
|
+
export const triggerQlsProductSyncDocument = graphql(`
|
|
31
|
+
mutation TriggerQlsProductSync {
|
|
32
|
+
triggerQlsProductSync
|
|
33
|
+
}
|
|
34
|
+
`);
|
|
35
|
+
|
|
36
|
+
export const pushOrderToQlsDocument = graphql(`
|
|
37
|
+
mutation PushOrderToQls($orderId: ID!) {
|
|
38
|
+
pushOrderToQls(orderId: $orderId)
|
|
39
|
+
}
|
|
40
|
+
`);
|
|
41
|
+
|
|
42
|
+
export const addAdditionalEanToQlsDocument = graphql(`
|
|
43
|
+
mutation AddAdditionalEanToQls($variantId: ID!, $additionalEANS: [String!]!) {
|
|
44
|
+
addAdditionalEANSToQLS(
|
|
45
|
+
variantId: $variantId
|
|
46
|
+
additionalEANS: $additionalEANS
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
`);
|
|
@@ -163,7 +163,7 @@ export interface FulfillmentOrder {
|
|
|
163
163
|
product: unknown;
|
|
164
164
|
}>;
|
|
165
165
|
}
|
|
166
|
-
export type IncomingStockWebhook =
|
|
166
|
+
export type IncomingStockWebhook = FulfillmentProductDetail;
|
|
167
167
|
export type IncomingOrderWebhook = Pick<FulfillmentOrder, 'customer_reference' | 'status' | 'cancelled' | 'amount_delivered' | 'amount_total'>;
|
|
168
168
|
/**
|
|
169
169
|
* This is the full data returend by the QLS API when fetching a fulfillment product by ID
|
package/dist/qls-plugin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { OnApplicationBootstrap, OnModuleInit } from '@nestjs/common';
|
|
2
2
|
import { EventBus, ID, Job, JobQueueService, ListQueryBuilder, ProductPriceApplicator, ProductVariant, ProductVariantService, RequestContext, StockLevelService, StockLocationService, TransactionalConnection } from '@vendure/core';
|
|
3
|
-
import { FulfillmentProduct } from '../lib/client-types';
|
|
3
|
+
import { FulfillmentProduct, IncomingStockWebhook } from '../lib/client-types';
|
|
4
4
|
import { QlsClient } from '../lib/qls-client';
|
|
5
5
|
import { QlsPluginOptions, QlsProductJobData } from '../types';
|
|
6
6
|
import { ModuleRef } from '@nestjs/core';
|
|
@@ -66,10 +66,13 @@ export declare class QlsProductService implements OnModuleInit, OnApplicationBoo
|
|
|
66
66
|
*/
|
|
67
67
|
addAdditionalEANsToQls(ctx: RequestContext, variantId: ID, additionalEANs: string[]): Promise<string[]>;
|
|
68
68
|
/**
|
|
69
|
-
* Update the stock level for a variant based on
|
|
70
|
-
* Returns true if the stock changed
|
|
69
|
+
* Update the stock level for a variant based on its SKU.
|
|
71
70
|
*/
|
|
72
71
|
updateStockBySku(ctx: RequestContext, sku: string, availableStock: number): Promise<boolean>;
|
|
72
|
+
/**
|
|
73
|
+
* Handles an incoming stock webhook and saves any additional variant data.
|
|
74
|
+
*/
|
|
75
|
+
handleStockWebhook(ctx: RequestContext, qlsProduct: IncomingStockWebhook): Promise<boolean>;
|
|
73
76
|
/**
|
|
74
77
|
* Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
|
|
75
78
|
*/
|
|
@@ -217,18 +217,6 @@ let QlsProductService = class QlsProductService {
|
|
|
217
217
|
else if (result.status === 'updated') {
|
|
218
218
|
updatedInQls.push(variant);
|
|
219
219
|
}
|
|
220
|
-
if (result.qlsProductId &&
|
|
221
|
-
this.options.productSync.saveAdditionalVariantData) {
|
|
222
|
-
try {
|
|
223
|
-
const qlsProduct = await client.getFulfillmentProductById(result.qlsProductId);
|
|
224
|
-
if (qlsProduct) {
|
|
225
|
-
await this.options.productSync.saveAdditionalVariantData(ctx, new core_1.Injector(this.moduleRef), qlsProduct, variant);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
catch (e) {
|
|
229
|
-
core_1.Logger.error(`Error in saveAdditionalData for variant '${variant.sku}': ${(0, catch_unknown_1.asError)(e).message}`, constants_1.loggerCtx);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
220
|
}
|
|
233
221
|
catch (e) {
|
|
234
222
|
const error = (0, catch_unknown_1.asError)(e);
|
|
@@ -312,8 +300,7 @@ let QlsProductService = class QlsProductService {
|
|
|
312
300
|
return eansToAdd;
|
|
313
301
|
}
|
|
314
302
|
/**
|
|
315
|
-
* Update the stock level for a variant based on
|
|
316
|
-
* Returns true if the stock changed
|
|
303
|
+
* Update the stock level for a variant based on its SKU.
|
|
317
304
|
*/
|
|
318
305
|
async updateStockBySku(ctx, sku, availableStock) {
|
|
319
306
|
const result = await this.variantService.findAll(ctx, {
|
|
@@ -329,6 +316,33 @@ let QlsProductService = class QlsProductService {
|
|
|
329
316
|
}
|
|
330
317
|
return await this.updateStock(ctx, variant.id, availableStock);
|
|
331
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Handles an incoming stock webhook and saves any additional variant data.
|
|
321
|
+
*/
|
|
322
|
+
async handleStockWebhook(ctx, qlsProduct) {
|
|
323
|
+
const result = await this.variantService.findAll(ctx, {
|
|
324
|
+
filter: { sku: { eq: qlsProduct.sku } },
|
|
325
|
+
});
|
|
326
|
+
if (!result.items.length) {
|
|
327
|
+
core_1.Logger.info(`Variant with sku '${qlsProduct.sku}' not found, not handling stock webhook`, constants_1.loggerCtx);
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
const variant = result.items[0];
|
|
331
|
+
if (result.items.length > 1) {
|
|
332
|
+
core_1.Logger.error(`Multiple variants found for sku '${qlsProduct.sku}', using '${variant.id}'`, constants_1.loggerCtx);
|
|
333
|
+
}
|
|
334
|
+
await this.updateStockBySku(ctx, qlsProduct.sku, qlsProduct.amount_available);
|
|
335
|
+
if (!this.options.productSync.saveAdditionalVariantData) {
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
await this.options.productSync.saveAdditionalVariantData(ctx, new core_1.Injector(this.moduleRef), qlsProduct, variant);
|
|
340
|
+
}
|
|
341
|
+
catch (e) {
|
|
342
|
+
core_1.Logger.error(`Error in saveAdditionalData for variant '${variant.sku}': ${(0, catch_unknown_1.asError)(e).message}`, constants_1.loggerCtx);
|
|
343
|
+
}
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
332
346
|
/**
|
|
333
347
|
* Determines if a product needs to be created or updated in QLS based on the given variant and existing QLS product.
|
|
334
348
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pinelab/vendure-plugin-qls-fulfillment",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Vendure plugin to fulfill orders via QLS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fulfillment",
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"CHANGELOG.md"
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
|
-
"build": "rimraf dist && yarn generate && tsc && copyfiles -u 1 'src/ui/**/*' dist/",
|
|
26
|
+
"build": "rimraf dist && yarn generate && tsc && copyfiles -u 1 'src/ui/**/*' 'src/dashboard/**/*' dist/",
|
|
27
|
+
"build:dashboard": "vite build --config ./vite.config.mts",
|
|
28
|
+
"dev:dashboard": "vite --clearScreen false",
|
|
27
29
|
"start": "nodemon --watch src --exec ts-node test/dev-server.ts",
|
|
28
30
|
"generate": "graphql-codegen",
|
|
29
31
|
"test": "vitest run --bail 1",
|
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
|
-
];
|