@pinelab/vendure-plugin-sendcloud 1.5.3 → 2.0.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 +10 -0
- package/README.md +49 -12
- package/dist/{vendure-plugin-sendcloud/src/api → api}/sendcloud.client.d.ts +0 -5
- package/dist/{vendure-plugin-sendcloud/src/api → api}/sendcloud.client.js +0 -15
- package/dist/{vendure-plugin-sendcloud/src/api → api}/sendcloud.service.d.ts +30 -14
- package/dist/{vendure-plugin-sendcloud/src/api → api}/sendcloud.service.js +154 -62
- package/dist/{vendure-plugin-sendcloud/src/api → api}/types/sendcloud-api.types.d.ts +0 -5
- package/dist/{vendure-plugin-sendcloud/src/api → api}/types/sendcloud.types.d.ts +0 -9
- package/dist/api/types/sendcloud.types.js +2 -0
- package/dist/config/fulfill-settled-orders-task.d.ts +26 -0
- package/dist/config/fulfill-settled-orders-task.js +40 -0
- package/dist/{vendure-plugin-sendcloud/src/index.d.ts → index.d.ts} +1 -0
- package/dist/{vendure-plugin-sendcloud/src/index.js → index.js} +1 -0
- package/dist/{vendure-plugin-sendcloud/src/sendcloud.plugin.js → sendcloud.plugin.js} +1 -9
- package/package.json +5 -5
- package/dist/util/src/index.d.ts +0 -1
- package/dist/util/src/index.js +0 -17
- package/dist/util/src/order-state-util.d.ts +0 -22
- package/dist/util/src/order-state-util.js +0 -78
- package/dist/util/src/raw-body.middleware.d.ts +0 -9
- package/dist/util/src/raw-body.middleware.js +0 -17
- package/dist/vendure-plugin-sendcloud/src/api/sendcloud.controller.d.ts +0 -7
- package/dist/vendure-plugin-sendcloud/src/api/sendcloud.controller.js +0 -80
- package/dist/vendure-plugin-sendcloud/src/api/types/sendcloud.types.js +0 -157
- /package/dist/{vendure-plugin-sendcloud/src/api → api}/additional-parcel-input-items.d.ts +0 -0
- /package/dist/{vendure-plugin-sendcloud/src/api → api}/additional-parcel-input-items.js +0 -0
- /package/dist/{vendure-plugin-sendcloud/src/api → api}/constants.d.ts +0 -0
- /package/dist/{vendure-plugin-sendcloud/src/api → api}/constants.js +0 -0
- /package/dist/{vendure-plugin-sendcloud/src/api → api}/sendcloud-config.entity.d.ts +0 -0
- /package/dist/{vendure-plugin-sendcloud/src/api → api}/sendcloud-config.entity.js +0 -0
- /package/dist/{vendure-plugin-sendcloud/src/api → api}/sendcloud.adapter.d.ts +0 -0
- /package/dist/{vendure-plugin-sendcloud/src/api → api}/sendcloud.adapter.js +0 -0
- /package/dist/{vendure-plugin-sendcloud/src/api → api}/sendcloud.handler.d.ts +0 -0
- /package/dist/{vendure-plugin-sendcloud/src/api → api}/sendcloud.handler.js +0 -0
- /package/dist/{vendure-plugin-sendcloud/src/api → api}/sendcloud.resolver.d.ts +0 -0
- /package/dist/{vendure-plugin-sendcloud/src/api → api}/sendcloud.resolver.js +0 -0
- /package/dist/{vendure-plugin-sendcloud/src/api → api}/types/sendcloud-api.types.js +0 -0
- /package/dist/{vendure-plugin-sendcloud/src/sendcloud.plugin.d.ts → sendcloud.plugin.d.ts} +0 -0
- /package/dist/{vendure-plugin-sendcloud/src/ui → ui}/history-entry.component.ts +0 -0
- /package/dist/{vendure-plugin-sendcloud/src/ui → ui}/queries.ts +0 -0
- /package/dist/{vendure-plugin-sendcloud/src/ui → ui}/sendcloud-nav.module.ts +0 -0
- /package/dist/{vendure-plugin-sendcloud/src/ui → ui}/sendcloud.component.ts +0 -0
- /package/dist/{vendure-plugin-sendcloud/src/ui → ui}/sendcloud.module.ts +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
# 2.0.0 (2026-04-12)
|
|
2
|
+
|
|
3
|
+
- **Breaking**: Removed all webhook handling. Sendcloud is responsible for fulfillment.
|
|
4
|
+
- Added `fulfillSettledOrdersTask` scheduled task (opt-in) to fulfill settled orders to `Delivered` on a nightly basis.
|
|
5
|
+
- Orders are no longer automatically fulfilled on placement. They stay in `PaymentSettled` until the scheduled task runs to prevent misaligned stock in Vendure.
|
|
6
|
+
|
|
7
|
+
# 1.6.0 (2026-02-05)
|
|
8
|
+
|
|
9
|
+
- Upgraded to Vendure 3.5.3
|
|
10
|
+
|
|
1
11
|
# 1.5.3 (2025-11-13)
|
|
2
12
|
|
|
3
13
|
- Documentation update
|
package/README.md
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
This plugin syncs orders to the SendCloud fulfillment platform.
|
|
6
6
|
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
1. When an order is placed, it is automatically sent to SendCloud as a parcel (for label creation).
|
|
10
|
+
2. The order stays in `PaymentSettled` state in Vendure — **no fulfillment is created at this point**.
|
|
11
|
+
3. Once an order has been sent to SendCloud, SendCloud is responsible for tracking status. The Vendure order will simply be transitioned to `Delivered` by the included scheduled task. This is because orders are sometimes cancelled and duplicated in Sendcloud, resulting in misaligned stock.
|
|
12
|
+
|
|
7
13
|
## Getting started
|
|
8
14
|
|
|
9
15
|
1. Add the plugin to your `vendure-config.ts`:
|
|
@@ -31,17 +37,46 @@ AdminUiPlugin.init({
|
|
|
31
37
|
3. Run a DB [migration](https://www.vendure.io/docs/developer-guide/migrations/) to add the new SendCloudConfigEntity to
|
|
32
38
|
the database.
|
|
33
39
|
4. Go to your SendCloud account and go to `Settings > Integrations` and create an integration.
|
|
34
|
-
5. Write down the `secret` and `publicKey` of the created integration
|
|
35
|
-
6.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
10. You can fill in your SendCloud `secret` and `public key` here and click save.
|
|
41
|
-
11. Additionally, you can set a fallback phone number, for when a customer hasn't filled out one. A phone number is
|
|
40
|
+
5. Write down the `secret` and `publicKey` of the created integration.
|
|
41
|
+
6. Start Vendure and login as admin.
|
|
42
|
+
7. Make sure you have the permission `SetSendCloudConfig`.
|
|
43
|
+
8. Go to `Settings > SendCloud`.
|
|
44
|
+
9. Fill in your SendCloud `secret` and `public key` here and click save.
|
|
45
|
+
10. Additionally, you can set a fallback phone number, for when a customer hasn't filled out one. A phone number is
|
|
42
46
|
required by Sendcloud in some cases.
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
## Scheduled task: fulfill settled orders
|
|
49
|
+
|
|
50
|
+
An optional scheduled task `fulfillSettledOrdersTask` is included to automatically transition settled orders to `Delivered`.
|
|
51
|
+
It is **not auto-registered** — you need to add it to your `schedulerOptions.tasks` yourself.
|
|
52
|
+
|
|
53
|
+
The task runs nightly (default 2:00 AM) and processes all `PaymentSettled` orders placed within the last N days
|
|
54
|
+
(default 7) that use the SendCloud fulfillment handler. Each qualifying order is fulfilled and transitioned to `Delivered`.
|
|
55
|
+
|
|
56
|
+
Do not manually change order statuses in Vendure if you use this scheduled task. The task will fulfill orders and transition to delivered every night.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { DefaultSchedulerPlugin } from '@vendure/core';
|
|
60
|
+
import {
|
|
61
|
+
SendcloudPlugin,
|
|
62
|
+
fulfillSettledOrdersTask,
|
|
63
|
+
} from '@pinelab/vendure-plugin-sendcloud';
|
|
64
|
+
|
|
65
|
+
const config: VendureConfig = {
|
|
66
|
+
plugins: [SendcloudPlugin.init({}), DefaultSchedulerPlugin.init()],
|
|
67
|
+
schedulerOptions: {
|
|
68
|
+
tasks: [
|
|
69
|
+
// Use defaults (every day at 2:00 AM, look back 7 days)
|
|
70
|
+
fulfillSettledOrdersTask,
|
|
71
|
+
// Or configure the task
|
|
72
|
+
fulfillSettledOrdersTask.configure({
|
|
73
|
+
schedule: (cron) => cron.everyDayAt(3, 0),
|
|
74
|
+
params: { settledSinceDays: 14 },
|
|
75
|
+
}),
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
```
|
|
45
80
|
|
|
46
81
|
## Additional configuration
|
|
47
82
|
|
|
@@ -49,10 +84,11 @@ You can choose to send additional info to SendCloud: `weight`, `hsCode`, `origin
|
|
|
49
84
|
Parcel items will show up as rows on your SendCloud packaging slips.
|
|
50
85
|
|
|
51
86
|
```ts
|
|
52
|
-
import
|
|
87
|
+
import {
|
|
88
|
+
SendcloudPlugin,
|
|
89
|
+
getNrOfOrders,
|
|
90
|
+
} from '@pinelab/vendure-plugin-sendcloud';
|
|
53
91
|
|
|
54
|
-
from;
|
|
55
|
-
('vendure-plugin-sendcloud');
|
|
56
92
|
plugins: [
|
|
57
93
|
SendcloudPlugin.init({
|
|
58
94
|
/**
|
|
@@ -77,6 +113,7 @@ plugins: [
|
|
|
77
113
|
* This example adds the nr of previous orders of the current customer to SendCloud
|
|
78
114
|
*/
|
|
79
115
|
additionalParcelItemsFn: async (ctx, injector, order) => {
|
|
116
|
+
const additionalInputs = [];
|
|
80
117
|
additionalInputs.push(await getNrOfOrders(ctx, injector, order));
|
|
81
118
|
return additionalInputs;
|
|
82
119
|
},
|
|
@@ -3,16 +3,11 @@ import { Parcel, ParcelInput } from './types/sendcloud-api.types';
|
|
|
3
3
|
export declare class SendcloudClient {
|
|
4
4
|
private publicKey;
|
|
5
5
|
private secret;
|
|
6
|
-
static signatureHeader: string;
|
|
7
6
|
endpoint: string;
|
|
8
7
|
headers: {
|
|
9
8
|
[key: string]: string;
|
|
10
9
|
};
|
|
11
10
|
constructor(publicKey: string, secret: string);
|
|
12
11
|
createParcel(parcelInput: ParcelInput): Promise<Parcel>;
|
|
13
|
-
/**
|
|
14
|
-
* Verifies if the incoming webhook is actually from SendCloud
|
|
15
|
-
*/
|
|
16
|
-
isValidWebhook(body: string, signature: string): boolean;
|
|
17
12
|
fetch(path: string, body: any): Promise<Response>;
|
|
18
13
|
}
|
|
@@ -5,7 +5,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.SendcloudClient = void 0;
|
|
7
7
|
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
8
|
-
const crypto_1 = __importDefault(require("crypto"));
|
|
9
8
|
const core_1 = require("@vendure/core");
|
|
10
9
|
const constants_1 = require("./constants");
|
|
11
10
|
class SendcloudClient {
|
|
@@ -29,19 +28,6 @@ class SendcloudClient {
|
|
|
29
28
|
core_1.Logger.info(`Created parcel in SendCloud with for order ${parcelInput.order_number} with id ${json.parcel?.id}`, constants_1.loggerCtx);
|
|
30
29
|
return json.parcel;
|
|
31
30
|
}
|
|
32
|
-
/**
|
|
33
|
-
* Verifies if the incoming webhook is actually from SendCloud
|
|
34
|
-
*/
|
|
35
|
-
isValidWebhook(body, signature) {
|
|
36
|
-
if (!body || !signature) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
const hash = crypto_1.default
|
|
40
|
-
.createHmac('sha256', this.secret)
|
|
41
|
-
.update(body)
|
|
42
|
-
.digest('hex');
|
|
43
|
-
return hash === signature;
|
|
44
|
-
}
|
|
45
31
|
async fetch(path, body) {
|
|
46
32
|
const res = await (0, node_fetch_1.default)(`${this.endpoint}/${path}`, {
|
|
47
33
|
method: 'POST',
|
|
@@ -56,4 +42,3 @@ class SendcloudClient {
|
|
|
56
42
|
}
|
|
57
43
|
}
|
|
58
44
|
exports.SendcloudClient = SendcloudClient;
|
|
59
|
-
SendcloudClient.signatureHeader = 'sendcloud-signature';
|
|
@@ -4,7 +4,7 @@ import { ChannelService, EntityHydrator, EventBus, HistoryService, ID, JobQueueS
|
|
|
4
4
|
import { SendcloudConfigEntity } from './sendcloud-config.entity';
|
|
5
5
|
import { SendcloudClient } from './sendcloud.client';
|
|
6
6
|
import { Parcel } from './types/sendcloud-api.types';
|
|
7
|
-
import {
|
|
7
|
+
import { SendcloudPluginOptions } from './types/sendcloud.types';
|
|
8
8
|
export declare class SendcloudService implements OnApplicationBootstrap {
|
|
9
9
|
private eventBus;
|
|
10
10
|
private connection;
|
|
@@ -19,19 +19,6 @@ export declare class SendcloudService implements OnApplicationBootstrap {
|
|
|
19
19
|
constructor(eventBus: EventBus, connection: TransactionalConnection, orderService: OrderService, channelService: ChannelService, jobQueueService: JobQueueService, moduleRef: ModuleRef, options: SendcloudPluginOptions, entityHydrator: EntityHydrator, historyService: HistoryService);
|
|
20
20
|
onApplicationBootstrap(): Promise<void>;
|
|
21
21
|
createOrderInSendcloud(userCtx: RequestContext, order: Order): Promise<Parcel | undefined>;
|
|
22
|
-
/**
|
|
23
|
-
* Update order by given orderCode, returns undefined if no action was taken
|
|
24
|
-
* Returns order if transition was successful
|
|
25
|
-
*/
|
|
26
|
-
updateOrderStatus(ctx: RequestContext, sendcloudStatus: SendcloudParcelStatus, orderCode: string): Promise<void>;
|
|
27
|
-
/**
|
|
28
|
-
* Ship all items
|
|
29
|
-
*/
|
|
30
|
-
shipAll(ctx: RequestContext, order: Order): Promise<void>;
|
|
31
|
-
/**
|
|
32
|
-
* Fulfill without throwing errors. Logs an error if fulfilment fails
|
|
33
|
-
*/
|
|
34
|
-
private safeFulfill;
|
|
35
22
|
upsertConfig(ctx: RequestContext, config: {
|
|
36
23
|
secret: string;
|
|
37
24
|
publicKey: string;
|
|
@@ -48,4 +35,33 @@ export declare class SendcloudService implements OnApplicationBootstrap {
|
|
|
48
35
|
*/
|
|
49
36
|
private syncOrder;
|
|
50
37
|
logHistoryEntry(ctx: RequestContext, orderId: ID, error?: unknown): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Find orders placed within the last N days that use the SendCloud fulfillment handler
|
|
40
|
+
* and process them based on their current state:
|
|
41
|
+
* - PaymentAuthorized: create fulfillment only (no shipping transitions)
|
|
42
|
+
* - PaymentSettled: fulfill + transition to Shipped + transition to Delivered
|
|
43
|
+
* - Shipped: transition existing fulfillment(s) to Delivered
|
|
44
|
+
*
|
|
45
|
+
* Intended to be called by the `fulfillSettledOrdersTask` scheduled task.
|
|
46
|
+
*/
|
|
47
|
+
fulfillPlacedOrders(settledSinceDays: number): Promise<{
|
|
48
|
+
fulfilled: number;
|
|
49
|
+
skipped: number;
|
|
50
|
+
failed: number;
|
|
51
|
+
}>;
|
|
52
|
+
/**
|
|
53
|
+
* Create a fulfillment for all order lines without any state transitions.
|
|
54
|
+
* Used for PaymentAuthorized orders where we only want to register the fulfillment.
|
|
55
|
+
*/
|
|
56
|
+
private createFulfillmentOnly;
|
|
57
|
+
/**
|
|
58
|
+
* Fulfill all order lines and transition the fulfillment to Shipped then Delivered.
|
|
59
|
+
* Also works for orders that already have a fulfillment and/or are already Shipped
|
|
60
|
+
*/
|
|
61
|
+
private fulfillOrderToDelivered;
|
|
62
|
+
/**
|
|
63
|
+
* Throws if a fulfillment state transition failed.
|
|
64
|
+
* Ignores errors where fromState === toState (already in the desired state).
|
|
65
|
+
*/
|
|
66
|
+
private throwIfTransitionError;
|
|
51
67
|
}
|
|
@@ -11,9 +11,6 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
11
11
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
12
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
13
|
};
|
|
14
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
-
};
|
|
17
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
15
|
exports.SendcloudService = void 0;
|
|
19
16
|
const common_1 = require("@nestjs/common");
|
|
@@ -24,8 +21,6 @@ const constants_1 = require("./constants");
|
|
|
24
21
|
const sendcloud_config_entity_1 = require("./sendcloud-config.entity");
|
|
25
22
|
const sendcloud_client_1 = require("./sendcloud.client");
|
|
26
23
|
const sendcloud_handler_1 = require("./sendcloud.handler");
|
|
27
|
-
const src_1 = require("../../../util/src");
|
|
28
|
-
const util_1 = __importDefault(require("util"));
|
|
29
24
|
let SendcloudService = class SendcloudService {
|
|
30
25
|
constructor(eventBus, connection,
|
|
31
26
|
// private rawConnection: Connection,
|
|
@@ -93,62 +88,6 @@ let SendcloudService = class SendcloudService {
|
|
|
93
88
|
throw err;
|
|
94
89
|
}
|
|
95
90
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Update order by given orderCode, returns undefined if no action was taken
|
|
98
|
-
* Returns order if transition was successful
|
|
99
|
-
*/
|
|
100
|
-
async updateOrderStatus(ctx, sendcloudStatus, orderCode) {
|
|
101
|
-
let order = await this.connection
|
|
102
|
-
.getRepository(ctx, core_2.Order)
|
|
103
|
-
.findOne({ where: { code: orderCode }, relations: ['lines'] });
|
|
104
|
-
if (!order) {
|
|
105
|
-
core_2.Logger.warn(`Cannot update status from SendCloud: No order with code ${orderCode} found`, constants_1.loggerCtx);
|
|
106
|
-
throw Error(`Cannot update status from SendCloud: No order with code ${orderCode} found`);
|
|
107
|
-
}
|
|
108
|
-
if (order.state === sendcloudStatus.orderState) {
|
|
109
|
-
return core_2.Logger.info(`Not updating order with code ${orderCode}: Order already has state ${order.state}`, constants_1.loggerCtx);
|
|
110
|
-
}
|
|
111
|
-
if (sendcloudStatus.orderState === 'Shipped') {
|
|
112
|
-
await this.shipAll(ctx, order);
|
|
113
|
-
return core_2.Logger.info(`Successfully updated order ${orderCode} to Shipped`, constants_1.loggerCtx);
|
|
114
|
-
}
|
|
115
|
-
order = await this.connection
|
|
116
|
-
.getRepository(ctx, core_2.Order)
|
|
117
|
-
.findOneOrFail({ where: { code: orderCode }, relations: ['lines'] }); // Refetch in case state was updated
|
|
118
|
-
if (sendcloudStatus.orderState === 'Delivered') {
|
|
119
|
-
await (0, src_1.transitionToDelivered)(this.orderService, ctx, order, {
|
|
120
|
-
code: sendcloud_handler_1.sendcloudHandler.code,
|
|
121
|
-
arguments: [],
|
|
122
|
-
});
|
|
123
|
-
return core_2.Logger.info(`Successfully updated order ${orderCode} to Delivered`, constants_1.loggerCtx);
|
|
124
|
-
}
|
|
125
|
-
// Fall through, means unhandled state
|
|
126
|
-
core_2.Logger.info(`Not handling state ${sendcloudStatus.orderState}`, constants_1.loggerCtx);
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Ship all items
|
|
130
|
-
*/
|
|
131
|
-
async shipAll(ctx, order) {
|
|
132
|
-
await (0, src_1.transitionToShipped)(this.orderService, ctx, order, {
|
|
133
|
-
code: sendcloud_handler_1.sendcloudHandler.code,
|
|
134
|
-
arguments: [],
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Fulfill without throwing errors. Logs an error if fulfilment fails
|
|
139
|
-
*/
|
|
140
|
-
async safeFulfill(ctx, order) {
|
|
141
|
-
try {
|
|
142
|
-
const fulfillment = await (0, src_1.fulfillAll)(ctx, this.orderService, order, {
|
|
143
|
-
code: sendcloud_handler_1.sendcloudHandler.code,
|
|
144
|
-
arguments: [],
|
|
145
|
-
});
|
|
146
|
-
core_2.Logger.info(`Created fulfillment (${fulfillment.id}) for order ${order.code}`, constants_1.loggerCtx);
|
|
147
|
-
}
|
|
148
|
-
catch (e) {
|
|
149
|
-
core_2.Logger.error(`Failed to fulfill order ${order.code}: ${e?.message}. Transition this order manually to 'Delivered' after checking that it exists in Sendcloud.`, constants_1.loggerCtx, util_1.default.inspect(e));
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
91
|
async upsertConfig(ctx, config) {
|
|
153
92
|
const repo = this.connection.getRepository(ctx, sendcloud_config_entity_1.SendcloudConfigEntity);
|
|
154
93
|
const existing = await repo.findOne({
|
|
@@ -216,7 +155,6 @@ let SendcloudService = class SendcloudService {
|
|
|
216
155
|
if (!hasSendcloudHandler) {
|
|
217
156
|
return core_2.Logger.info(`Order ${order.code} does not have SendCloud set as handler. Not syncing this order.`, constants_1.loggerCtx);
|
|
218
157
|
}
|
|
219
|
-
await this.safeFulfill(ctx, order);
|
|
220
158
|
core_2.Logger.info(`Syncing order ${orderCode} for channel ${ctx.channel.token}`, constants_1.loggerCtx);
|
|
221
159
|
const result = await this.createOrderInSendcloud(ctx, order);
|
|
222
160
|
if (result) {
|
|
@@ -238,6 +176,160 @@ let SendcloudService = class SendcloudService {
|
|
|
238
176
|
},
|
|
239
177
|
}, false);
|
|
240
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Find orders placed within the last N days that use the SendCloud fulfillment handler
|
|
181
|
+
* and process them based on their current state:
|
|
182
|
+
* - PaymentAuthorized: create fulfillment only (no shipping transitions)
|
|
183
|
+
* - PaymentSettled: fulfill + transition to Shipped + transition to Delivered
|
|
184
|
+
* - Shipped: transition existing fulfillment(s) to Delivered
|
|
185
|
+
*
|
|
186
|
+
* Intended to be called by the `fulfillSettledOrdersTask` scheduled task.
|
|
187
|
+
*/
|
|
188
|
+
async fulfillPlacedOrders(settledSinceDays) {
|
|
189
|
+
const ctx = await this.createContext((await this.channelService.getDefaultChannel()).token);
|
|
190
|
+
const channels = await this.channelService.findAll(ctx);
|
|
191
|
+
const summary = { fulfilled: 0, skipped: 0, failed: 0 };
|
|
192
|
+
const sinceDate = new Date();
|
|
193
|
+
sinceDate.setDate(sinceDate.getDate() - settledSinceDays);
|
|
194
|
+
for (const channel of channels.items) {
|
|
195
|
+
const channelCtx = new core_2.RequestContext({
|
|
196
|
+
apiType: 'admin',
|
|
197
|
+
channel,
|
|
198
|
+
isAuthorized: true,
|
|
199
|
+
authorizedAsOwnerOnly: false,
|
|
200
|
+
});
|
|
201
|
+
let skip = 0;
|
|
202
|
+
const take = 100;
|
|
203
|
+
while (true) {
|
|
204
|
+
const orderList = await this.orderService.findAll(channelCtx, {
|
|
205
|
+
filter: {
|
|
206
|
+
state: {
|
|
207
|
+
in: ['PaymentAuthorized', 'PaymentSettled', 'Shipped'],
|
|
208
|
+
},
|
|
209
|
+
orderPlacedAt: { after: sinceDate.toISOString() },
|
|
210
|
+
},
|
|
211
|
+
skip,
|
|
212
|
+
take,
|
|
213
|
+
}, ['lines', 'shippingLines', 'shippingLines.shippingMethod']);
|
|
214
|
+
for (const order of orderList.items) {
|
|
215
|
+
const hasSendcloudHandler = order.shippingLines?.find((line) => line.shippingMethod?.fulfillmentHandlerCode ===
|
|
216
|
+
sendcloud_handler_1.sendcloudHandler.code);
|
|
217
|
+
if (!hasSendcloudHandler) {
|
|
218
|
+
summary.skipped++;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
if (order.state === 'PaymentAuthorized') {
|
|
223
|
+
await this.createFulfillmentOnly(channelCtx, order);
|
|
224
|
+
core_2.Logger.info(`Created fulfillment for authorized order ${order.code}`, constants_1.loggerCtx);
|
|
225
|
+
summary.fulfilled++;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
// Handle Shipped and PaymentSettled orders the same: fulfill + transition to Delivered
|
|
229
|
+
await this.fulfillOrderToDelivered(channelCtx, order);
|
|
230
|
+
core_2.Logger.info(`Fulfilled order ${order.code} (state: ${order.state}) to Delivered`, constants_1.loggerCtx);
|
|
231
|
+
summary.fulfilled++;
|
|
232
|
+
}
|
|
233
|
+
catch (e) {
|
|
234
|
+
summary.failed++;
|
|
235
|
+
core_2.Logger.error(`Failed to process order ${order.code} (${order.state}): ${e?.message}`, constants_1.loggerCtx);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (orderList.items.length < take) {
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
skip += take;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
core_2.Logger.info(`Finished: ${summary.fulfilled} fulfilled, ${summary.skipped} skipped, ${summary.failed} failed`, constants_1.loggerCtx);
|
|
245
|
+
return summary;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Create a fulfillment for all order lines without any state transitions.
|
|
249
|
+
* Used for PaymentAuthorized orders where we only want to register the fulfillment.
|
|
250
|
+
*/
|
|
251
|
+
async createFulfillmentOnly(ctx, order) {
|
|
252
|
+
if (!order.lines?.length) {
|
|
253
|
+
const fullOrder = await this.orderService.findOne(ctx, order.id, [
|
|
254
|
+
'lines',
|
|
255
|
+
]);
|
|
256
|
+
if (!fullOrder) {
|
|
257
|
+
throw new Error(`Order ${order.code} not found`);
|
|
258
|
+
}
|
|
259
|
+
order = fullOrder;
|
|
260
|
+
}
|
|
261
|
+
const lines = order.lines.map((line) => ({
|
|
262
|
+
orderLineId: line.id,
|
|
263
|
+
quantity: line.quantity,
|
|
264
|
+
}));
|
|
265
|
+
const fulfillmentResult = await this.orderService.createFulfillment(ctx, {
|
|
266
|
+
handler: { code: sendcloud_handler_1.sendcloudHandler.code, arguments: [] },
|
|
267
|
+
lines,
|
|
268
|
+
});
|
|
269
|
+
if (fulfillmentResult.errorCode ===
|
|
270
|
+
'ITEMS_ALREADY_FULFILLED_ERROR') {
|
|
271
|
+
return; // Already fulfilled, nothing to do
|
|
272
|
+
}
|
|
273
|
+
const error = fulfillmentResult;
|
|
274
|
+
if (error.errorCode) {
|
|
275
|
+
throw new Error(`${error.errorCode}: ${error.message}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Fulfill all order lines and transition the fulfillment to Shipped then Delivered.
|
|
280
|
+
* Also works for orders that already have a fulfillment and/or are already Shipped
|
|
281
|
+
*/
|
|
282
|
+
async fulfillOrderToDelivered(ctx, order) {
|
|
283
|
+
if (!order.lines?.length) {
|
|
284
|
+
const fullOrder = await this.orderService.findOne(ctx, order.id, [
|
|
285
|
+
'lines',
|
|
286
|
+
]);
|
|
287
|
+
if (!fullOrder) {
|
|
288
|
+
throw new Error(`Order ${order.code} not found`);
|
|
289
|
+
}
|
|
290
|
+
order = fullOrder;
|
|
291
|
+
}
|
|
292
|
+
const lines = order.lines.map((line) => ({
|
|
293
|
+
orderLineId: line.id,
|
|
294
|
+
quantity: line.quantity,
|
|
295
|
+
}));
|
|
296
|
+
const fulfillmentResult = await this.orderService.createFulfillment(ctx, {
|
|
297
|
+
handler: { code: sendcloud_handler_1.sendcloudHandler.code, arguments: [] },
|
|
298
|
+
lines,
|
|
299
|
+
});
|
|
300
|
+
let fulfillment;
|
|
301
|
+
if (fulfillmentResult.errorCode ===
|
|
302
|
+
'ITEMS_ALREADY_FULFILLED_ERROR') {
|
|
303
|
+
const fulfillments = await this.orderService.getOrderFulfillments(ctx, order);
|
|
304
|
+
fulfillment = fulfillments[0];
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
const error = fulfillmentResult;
|
|
308
|
+
if (error.errorCode) {
|
|
309
|
+
throw new Error(`${error.errorCode}: ${error.message}`);
|
|
310
|
+
}
|
|
311
|
+
fulfillment = fulfillmentResult;
|
|
312
|
+
}
|
|
313
|
+
if (order.state !== 'Delivered') {
|
|
314
|
+
const shippedResult = await this.orderService.transitionFulfillmentToState(ctx, fulfillment.id, 'Shipped');
|
|
315
|
+
this.throwIfTransitionError(shippedResult);
|
|
316
|
+
}
|
|
317
|
+
const deliveredResult = await this.orderService.transitionFulfillmentToState(ctx, fulfillment.id, 'Delivered');
|
|
318
|
+
this.throwIfTransitionError(deliveredResult);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Throws if a fulfillment state transition failed.
|
|
322
|
+
* Ignores errors where fromState === toState (already in the desired state).
|
|
323
|
+
*/
|
|
324
|
+
throwIfTransitionError(result) {
|
|
325
|
+
const stateError = result;
|
|
326
|
+
if (stateError.transitionError) {
|
|
327
|
+
if (stateError.fromState === stateError.toState) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
throw new Error(`Transition error: ${stateError.fromState} -> ${stateError.toState}: ${stateError.transitionError}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
241
333
|
};
|
|
242
334
|
exports.SendcloudService = SendcloudService;
|
|
243
335
|
exports.SendcloudService = SendcloudService = __decorate([
|
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
import { Injector, Order, OrderLine, RequestContext } from '@vendure/core';
|
|
2
2
|
import { ParcelInputItem } from './sendcloud-api.types';
|
|
3
|
-
export interface SendcloudParcelStatus {
|
|
4
|
-
id: number;
|
|
5
|
-
message: string;
|
|
6
|
-
/**
|
|
7
|
-
* Corresponding orderState for the sendcloud status
|
|
8
|
-
*/
|
|
9
|
-
orderState?: 'Shipped' | 'Delivered' | 'Cancelled';
|
|
10
|
-
}
|
|
11
|
-
export declare const sendcloudStates: SendcloudParcelStatus[];
|
|
12
3
|
export interface SendcloudPluginOptions {
|
|
13
4
|
/**
|
|
14
5
|
* Function to specify the weight of a parcelItem
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ScheduledTask } from '@vendure/core';
|
|
2
|
+
/**
|
|
3
|
+
* Scheduled task that finds all PaymentSettled orders placed within the last N days
|
|
4
|
+
* that use the SendCloud fulfillment handler, and transitions them to Delivered.
|
|
5
|
+
*
|
|
6
|
+
* This task is opt-in: add it to `schedulerOptions.tasks` in your Vendure config.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { fulfillSettledOrdersTask } from '@pinelab/vendure-plugin-sendcloud';
|
|
11
|
+
*
|
|
12
|
+
* const config: VendureConfig = {
|
|
13
|
+
* schedulerOptions: {
|
|
14
|
+
* tasks: [
|
|
15
|
+
* fulfillSettledOrdersTask.configure({
|
|
16
|
+
* params: { settledSinceDays: 14 },
|
|
17
|
+
* }),
|
|
18
|
+
* ],
|
|
19
|
+
* },
|
|
20
|
+
* };
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare const fulfillSettledOrdersTask: ScheduledTask<{
|
|
24
|
+
/** Number of days to look back for settled orders */
|
|
25
|
+
settledSinceDays: number;
|
|
26
|
+
}>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fulfillSettledOrdersTask = void 0;
|
|
4
|
+
const core_1 = require("@vendure/core");
|
|
5
|
+
const sendcloud_service_1 = require("../api/sendcloud.service");
|
|
6
|
+
/**
|
|
7
|
+
* Scheduled task that finds all PaymentSettled orders placed within the last N days
|
|
8
|
+
* that use the SendCloud fulfillment handler, and transitions them to Delivered.
|
|
9
|
+
*
|
|
10
|
+
* This task is opt-in: add it to `schedulerOptions.tasks` in your Vendure config.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { fulfillSettledOrdersTask } from '@pinelab/vendure-plugin-sendcloud';
|
|
15
|
+
*
|
|
16
|
+
* const config: VendureConfig = {
|
|
17
|
+
* schedulerOptions: {
|
|
18
|
+
* tasks: [
|
|
19
|
+
* fulfillSettledOrdersTask.configure({
|
|
20
|
+
* params: { settledSinceDays: 14 },
|
|
21
|
+
* }),
|
|
22
|
+
* ],
|
|
23
|
+
* },
|
|
24
|
+
* };
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
exports.fulfillSettledOrdersTask = new core_1.ScheduledTask({
|
|
28
|
+
id: 'sendcloud-fulfill-settled-orders',
|
|
29
|
+
description: 'Fulfill settled SendCloud orders to Delivered. Only processes orders in PaymentSettled state with the SendCloud handler.',
|
|
30
|
+
params: {
|
|
31
|
+
/** Number of days to look back for settled orders */
|
|
32
|
+
settledSinceDays: 90,
|
|
33
|
+
},
|
|
34
|
+
schedule: (cron) => cron.everyDayAt(2, 0),
|
|
35
|
+
async execute({ injector, params }) {
|
|
36
|
+
return injector
|
|
37
|
+
.get(sendcloud_service_1.SendcloudService)
|
|
38
|
+
.fulfillPlacedOrders(params.settledSinceDays);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -19,3 +19,4 @@ __exportStar(require("./api/sendcloud.handler"), exports);
|
|
|
19
19
|
__exportStar(require("./api/additional-parcel-input-items"), exports);
|
|
20
20
|
__exportStar(require("./api/types/sendcloud.types"), exports);
|
|
21
21
|
__exportStar(require("./api/types/sendcloud-api.types"), exports);
|
|
22
|
+
__exportStar(require("./config/fulfill-settled-orders-task"), exports);
|
|
@@ -17,10 +17,8 @@ const path_1 = __importDefault(require("path"));
|
|
|
17
17
|
const sendcloud_resolver_1 = require("./api/sendcloud.resolver");
|
|
18
18
|
const sendcloud_service_1 = require("./api/sendcloud.service");
|
|
19
19
|
const constants_1 = require("./api/constants");
|
|
20
|
-
const sendcloud_controller_1 = require("./api/sendcloud.controller");
|
|
21
20
|
const sendcloud_config_entity_1 = require("./api/sendcloud-config.entity");
|
|
22
21
|
const sendcloud_handler_1 = require("./api/sendcloud.handler");
|
|
23
|
-
const raw_body_middleware_1 = require("../../util/src/raw-body.middleware");
|
|
24
22
|
let SendcloudPlugin = SendcloudPlugin_1 = class SendcloudPlugin {
|
|
25
23
|
static init(options) {
|
|
26
24
|
this.options = options;
|
|
@@ -80,18 +78,12 @@ exports.SendcloudPlugin = SendcloudPlugin = SendcloudPlugin_1 = __decorate([
|
|
|
80
78
|
},
|
|
81
79
|
],
|
|
82
80
|
imports: [core_1.PluginCommonModule],
|
|
83
|
-
controllers: [sendcloud_controller_1.SendcloudController],
|
|
84
81
|
entities: [sendcloud_config_entity_1.SendcloudConfigEntity],
|
|
85
82
|
configuration: (config) => {
|
|
86
|
-
config.apiOptions.middleware.push({
|
|
87
|
-
route: '/sendcloud/webhook*splat',
|
|
88
|
-
handler: raw_body_middleware_1.rawBodyMiddleware,
|
|
89
|
-
beforeListen: true,
|
|
90
|
-
});
|
|
91
83
|
config.shippingOptions.fulfillmentHandlers.push(sendcloud_handler_1.sendcloudHandler);
|
|
92
84
|
config.authOptions.customPermissions.push(sendcloud_resolver_1.sendcloudPermission);
|
|
93
85
|
return config;
|
|
94
86
|
},
|
|
95
|
-
compatibility: '>=3.
|
|
87
|
+
compatibility: '>=3.1.0',
|
|
96
88
|
})
|
|
97
89
|
], SendcloudPlugin);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pinelab/vendure-plugin-sendcloud",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Vendure plugin for syncing orders with SendCloud",
|
|
5
5
|
"author": "Martijn van de Brug <martijn@pinelab.studio>",
|
|
6
6
|
"homepage": "https://plugins.pinelab.studio/",
|
|
@@ -10,18 +10,18 @@
|
|
|
10
10
|
"publishConfig": {
|
|
11
11
|
"access": "public"
|
|
12
12
|
},
|
|
13
|
-
"main": "dist/
|
|
14
|
-
"types": "dist/
|
|
13
|
+
"main": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
17
|
"README.md",
|
|
18
18
|
"CHANGELOG.md"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
-
"build": "rimraf dist && tsc && copyfiles -u 1 'src/ui/**/*' dist/
|
|
21
|
+
"build": "rimraf dist && tsc && copyfiles -u 1 'src/ui/**/*' dist/",
|
|
22
22
|
"test": "vitest run",
|
|
23
23
|
"start": "yarn ts-node test/dev-server.ts",
|
|
24
24
|
"lint": "echo 'No linting configured'"
|
|
25
25
|
},
|
|
26
|
-
"gitHead": "
|
|
26
|
+
"gitHead": "476f36da3aafea41fbf21c70774a30306f1d238f"
|
|
27
27
|
}
|
package/dist/util/src/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './order-state-util';
|
package/dist/util/src/index.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
-
};
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
__exportStar(require("./order-state-util"), exports);
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { FulfillmentStateTransitionError, Order, OrderService, RequestContext } from '@vendure/core';
|
|
2
|
-
import { AddFulfillmentToOrderResult, ConfigurableOperationInput } from '@vendure/common/lib/generated-types';
|
|
3
|
-
import { Fulfillment } from '@vendure/core/dist/entity/fulfillment/fulfillment.entity';
|
|
4
|
-
export declare const loggerCtx = "OrderStateUtil";
|
|
5
|
-
/**
|
|
6
|
-
* Create a fulfillment for all orderlines. Returns fulfillments[0] if already fulfilled
|
|
7
|
-
*/
|
|
8
|
-
export declare function fulfillAll(ctx: RequestContext, orderService: OrderService, order: Order, handler: ConfigurableOperationInput): Promise<Fulfillment>;
|
|
9
|
-
/**
|
|
10
|
-
* Fulfills all items to shipped using transitionFulfillmentToState
|
|
11
|
-
*/
|
|
12
|
-
export declare function transitionToShipped(orderService: OrderService, ctx: RequestContext, order: Order, handler: ConfigurableOperationInput): Promise<Fulfillment | FulfillmentStateTransitionError>;
|
|
13
|
-
/**
|
|
14
|
-
* Fulfills all items to shipped, then to delivered using transitionFulfillmentToState
|
|
15
|
-
*/
|
|
16
|
-
export declare function transitionToDelivered(orderService: OrderService, ctx: RequestContext, order: Order, handler: ConfigurableOperationInput): Promise<(Fulfillment | FulfillmentStateTransitionError)[]>;
|
|
17
|
-
/**
|
|
18
|
-
* Throws the error result if the transition failed
|
|
19
|
-
* Ignores transition errors where from and to state are the same,
|
|
20
|
-
* because that still results in the situation we want
|
|
21
|
-
*/
|
|
22
|
-
export declare function throwIfTransitionFailed(result: FulfillmentStateTransitionError | Fulfillment | AddFulfillmentToOrderResult): void;
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.loggerCtx = void 0;
|
|
4
|
-
exports.fulfillAll = fulfillAll;
|
|
5
|
-
exports.transitionToShipped = transitionToShipped;
|
|
6
|
-
exports.transitionToDelivered = transitionToDelivered;
|
|
7
|
-
exports.throwIfTransitionFailed = throwIfTransitionFailed;
|
|
8
|
-
const core_1 = require("@vendure/core");
|
|
9
|
-
exports.loggerCtx = 'OrderStateUtil';
|
|
10
|
-
/**
|
|
11
|
-
* Create a fulfillment for all orderlines. Returns fulfillments[0] if already fulfilled
|
|
12
|
-
*/
|
|
13
|
-
async function fulfillAll(ctx, orderService, order, handler) {
|
|
14
|
-
const lines = order.lines.map((line) => ({
|
|
15
|
-
orderLineId: line.id,
|
|
16
|
-
quantity: line.quantity,
|
|
17
|
-
}));
|
|
18
|
-
const fulfillment = await orderService.createFulfillment(ctx, {
|
|
19
|
-
handler,
|
|
20
|
-
lines,
|
|
21
|
-
});
|
|
22
|
-
if (fulfillment.errorCode ===
|
|
23
|
-
'ITEMS_ALREADY_FULFILLED_ERROR') {
|
|
24
|
-
const fulfillments = await orderService.getOrderFulfillments(ctx, order);
|
|
25
|
-
const fulfillment = fulfillments[0];
|
|
26
|
-
core_1.Logger.info(`Items already fulfilled for order ${order.code}. Returning fulfillment '${fulfillment.id}'. Existing fulfillments: ${fulfillments.map((f) => f.id).join(', ')}`, exports.loggerCtx);
|
|
27
|
-
return fulfillment;
|
|
28
|
-
}
|
|
29
|
-
throwIfTransitionFailed(fulfillment);
|
|
30
|
-
return fulfillment;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Fulfills all items to shipped using transitionFulfillmentToState
|
|
34
|
-
*/
|
|
35
|
-
async function transitionToShipped(orderService, ctx, order, handler) {
|
|
36
|
-
const fulfillment = await fulfillAll(ctx, orderService, order, handler);
|
|
37
|
-
const result = await orderService.transitionFulfillmentToState(ctx, fulfillment.id, 'Shipped');
|
|
38
|
-
throwIfTransitionFailed(result);
|
|
39
|
-
return result;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Fulfills all items to shipped, then to delivered using transitionFulfillmentToState
|
|
43
|
-
*/
|
|
44
|
-
async function transitionToDelivered(orderService, ctx, order, handler) {
|
|
45
|
-
const shippedResult = await transitionToShipped(orderService, ctx, order, handler);
|
|
46
|
-
let fulfillments = [];
|
|
47
|
-
if (shippedResult.errorCode) {
|
|
48
|
-
fulfillments = await orderService.getOrderFulfillments(ctx, order);
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
// if not an error, shippedResult is the only fulfillment
|
|
52
|
-
fulfillments.push(shippedResult);
|
|
53
|
-
}
|
|
54
|
-
const results = [];
|
|
55
|
-
for (const fulfillment of fulfillments) {
|
|
56
|
-
const result = await orderService.transitionFulfillmentToState(ctx, fulfillment.id, 'Delivered');
|
|
57
|
-
throwIfTransitionFailed(result);
|
|
58
|
-
results.push(result);
|
|
59
|
-
}
|
|
60
|
-
return results;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Throws the error result if the transition failed
|
|
64
|
-
* Ignores transition errors where from and to state are the same,
|
|
65
|
-
* because that still results in the situation we want
|
|
66
|
-
*/
|
|
67
|
-
function throwIfTransitionFailed(result) {
|
|
68
|
-
const stateError = result;
|
|
69
|
-
if (stateError.transitionError &&
|
|
70
|
-
stateError.fromState === stateError.toState) {
|
|
71
|
-
return; // If already 'Shipped', don't count this as an error
|
|
72
|
-
}
|
|
73
|
-
// It's not a stateTransition error
|
|
74
|
-
const error = result;
|
|
75
|
-
if (error.errorCode) {
|
|
76
|
-
throw error;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { Request } from 'express';
|
|
2
|
-
export interface RequestWithRawBody extends Request {
|
|
3
|
-
rawBody: Buffer;
|
|
4
|
-
}
|
|
5
|
-
/**
|
|
6
|
-
* Middleware which adds the raw request body to the incoming message object. This is needed by
|
|
7
|
-
* Stripe to properly verify webhook events.
|
|
8
|
-
*/
|
|
9
|
-
export declare const rawBodyMiddleware: import("connect").NextHandleFunction;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.rawBodyMiddleware = void 0;
|
|
4
|
-
const body_parser_1 = require("body-parser");
|
|
5
|
-
/**
|
|
6
|
-
* Middleware which adds the raw request body to the incoming message object. This is needed by
|
|
7
|
-
* Stripe to properly verify webhook events.
|
|
8
|
-
*/
|
|
9
|
-
exports.rawBodyMiddleware = (0, body_parser_1.raw)({
|
|
10
|
-
type: '*/*',
|
|
11
|
-
verify(req, res, buf, encoding) {
|
|
12
|
-
if (Buffer.isBuffer(buf)) {
|
|
13
|
-
req.rawBody = Buffer.from(buf);
|
|
14
|
-
}
|
|
15
|
-
return true;
|
|
16
|
-
},
|
|
17
|
-
});
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { Request } from 'express';
|
|
2
|
-
import { SendcloudService } from './sendcloud.service';
|
|
3
|
-
export declare class SendcloudController {
|
|
4
|
-
private sendcloudService;
|
|
5
|
-
constructor(sendcloudService: SendcloudService);
|
|
6
|
-
webhook(req: Request, signature: string, channelToken: string): Promise<unknown>;
|
|
7
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
-
};
|
|
8
|
-
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
-
};
|
|
11
|
-
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
-
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
-
};
|
|
14
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.SendcloudController = void 0;
|
|
16
|
-
const common_1 = require("@nestjs/common");
|
|
17
|
-
const sendcloud_service_1 = require("./sendcloud.service");
|
|
18
|
-
const sendcloud_client_1 = require("./sendcloud.client");
|
|
19
|
-
const core_1 = require("@vendure/core");
|
|
20
|
-
const constants_1 = require("./constants");
|
|
21
|
-
const sendcloud_types_1 = require("./types/sendcloud.types");
|
|
22
|
-
const util_1 = require("util");
|
|
23
|
-
let SendcloudController = class SendcloudController {
|
|
24
|
-
constructor(sendcloudService) {
|
|
25
|
-
this.sendcloudService = sendcloudService;
|
|
26
|
-
}
|
|
27
|
-
async webhook(req, signature, channelToken) {
|
|
28
|
-
let body;
|
|
29
|
-
if (!Buffer.isBuffer(req.body)) {
|
|
30
|
-
core_1.Logger.warn(`Incoming webhook body is not a Buffer. This means the body was already parsed by some other middleware. This might cause problems when validating the incoming webhook signature.`, constants_1.loggerCtx);
|
|
31
|
-
body = req.body;
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
// Else we try and parse the body
|
|
35
|
-
try {
|
|
36
|
-
body = JSON.parse(req.body.toString());
|
|
37
|
-
}
|
|
38
|
-
catch (e) {
|
|
39
|
-
core_1.Logger.error(`Error parsing incoming webhook body: ${e?.message ?? e}`, constants_1.loggerCtx, (0, util_1.inspect)(req.body));
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
const rawBody = req.rawBody;
|
|
44
|
-
const ctx = await this.sendcloudService.createContext(channelToken);
|
|
45
|
-
const { client } = await this.sendcloudService.getClient(ctx);
|
|
46
|
-
if (!client.isValidWebhook(rawBody, signature)) {
|
|
47
|
-
core_1.Logger.warn(`Ignoring incoming webhook for channel ${channelToken}, because it has an invalid signature`, constants_1.loggerCtx);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (body.action !== 'parcel_status_changed') {
|
|
51
|
-
return core_1.Logger.info(`Incoming webhook: ${body.action}. skipping...`, constants_1.loggerCtx);
|
|
52
|
-
}
|
|
53
|
-
core_1.Logger.info(`Incoming Sendcloud webhook: ${body.action} - ${body.parcel?.id} - ${body.parcel?.order_number} - ${body.parcel?.status.id} (${body.parcel?.status.message})`, constants_1.loggerCtx);
|
|
54
|
-
const status = sendcloud_types_1.sendcloudStates.find((s) => s.id === body.parcel?.status?.id);
|
|
55
|
-
if (!status) {
|
|
56
|
-
return core_1.Logger.warn(`Unknown SendCloud status "${body.parcel?.status?.message}", not handling this webhook.`, constants_1.loggerCtx);
|
|
57
|
-
}
|
|
58
|
-
if (!status.orderState) {
|
|
59
|
-
return core_1.Logger.info(`Ignoring incoming webhook status "${body.parcel?.status?.message}", because we don't update Vendure order status for this sendcloud status.`, constants_1.loggerCtx);
|
|
60
|
-
}
|
|
61
|
-
if (!body.parcel?.order_number) {
|
|
62
|
-
return core_1.Logger.warn(`No order_number in incoming Sendcloud webhook: ${JSON.stringify(body.parcel)}`, constants_1.loggerCtx);
|
|
63
|
-
}
|
|
64
|
-
await this.sendcloudService.updateOrderStatus(ctx, status, body.parcel.order_number);
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
exports.SendcloudController = SendcloudController;
|
|
68
|
-
__decorate([
|
|
69
|
-
(0, common_1.Post)('webhook/:channelToken'),
|
|
70
|
-
__param(0, (0, common_1.Req)()),
|
|
71
|
-
__param(1, (0, common_1.Headers)(sendcloud_client_1.SendcloudClient.signatureHeader)),
|
|
72
|
-
__param(2, (0, common_1.Param)('channelToken')),
|
|
73
|
-
__metadata("design:type", Function),
|
|
74
|
-
__metadata("design:paramtypes", [Object, String, String]),
|
|
75
|
-
__metadata("design:returntype", Promise)
|
|
76
|
-
], SendcloudController.prototype, "webhook", null);
|
|
77
|
-
exports.SendcloudController = SendcloudController = __decorate([
|
|
78
|
-
(0, common_1.Controller)('sendcloud'),
|
|
79
|
-
__metadata("design:paramtypes", [sendcloud_service_1.SendcloudService])
|
|
80
|
-
], SendcloudController);
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.sendcloudStates = void 0;
|
|
4
|
-
exports.sendcloudStates = [
|
|
5
|
-
{
|
|
6
|
-
id: 62989,
|
|
7
|
-
message: 'The package has been held at customs',
|
|
8
|
-
},
|
|
9
|
-
{
|
|
10
|
-
id: 6,
|
|
11
|
-
message: 'Not sorted',
|
|
12
|
-
orderState: 'Shipped',
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
id: 15,
|
|
16
|
-
message: 'Error collecting',
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
id: 62990,
|
|
20
|
-
message: 'The package is in the sorting centre',
|
|
21
|
-
orderState: 'Shipped',
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
id: 62991,
|
|
25
|
-
message: 'The package was refused by the recipient when delivered',
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
id: 62992,
|
|
29
|
-
message: 'The package was returned to the sender due to an issue',
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
id: 62993,
|
|
33
|
-
message: 'The delivery method was changed by the request of the recipient or due to other circumstances.',
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
id: 1002,
|
|
37
|
-
message: 'Announcement failed',
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
id: 1999,
|
|
41
|
-
message: 'Cancellation requested',
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: 62994,
|
|
45
|
-
message: 'The delivery date was changed by the request of the recipient or due to other.',
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
id: 62995,
|
|
49
|
-
message: 'The delivery address was changed by the request of the recipient or due to other circumstances.',
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
id: 62996,
|
|
53
|
-
message: 'For unusual cases: lost, damaged, destroyed, etc.',
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
id: 1998,
|
|
57
|
-
message: 'Cancelled upstream',
|
|
58
|
-
orderState: 'Cancelled',
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
id: 1000,
|
|
62
|
-
message: 'Ready to send',
|
|
63
|
-
orderState: 'Shipped',
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
id: 62997,
|
|
67
|
-
message: 'The address is incorrect and the carrier needs address correction from the sender or the recipient.',
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
id: 12,
|
|
71
|
-
message: 'Awaiting customer pickup',
|
|
72
|
-
orderState: 'Shipped',
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
id: 11,
|
|
76
|
-
message: 'Delivered',
|
|
77
|
-
orderState: 'Delivered',
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
id: 93,
|
|
81
|
-
message: 'Shipment collected by customer',
|
|
82
|
-
orderState: 'Delivered',
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
id: 91,
|
|
86
|
-
message: 'Parcel en route',
|
|
87
|
-
orderState: 'Shipped',
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
id: 80,
|
|
91
|
-
message: 'Unable to deliver',
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
id: 22,
|
|
95
|
-
message: 'Shipment picked up by driver',
|
|
96
|
-
orderState: 'Shipped',
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
id: 13,
|
|
100
|
-
message: 'Announced: not collected',
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
id: 8,
|
|
104
|
-
message: 'Delivery attempt failed',
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
id: 7,
|
|
108
|
-
message: 'Being sorted',
|
|
109
|
-
orderState: 'Shipped',
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
id: 5,
|
|
113
|
-
message: 'Sorted',
|
|
114
|
-
orderState: 'Shipped',
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
id: 4,
|
|
118
|
-
message: 'Delivery delayed',
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
id: 3,
|
|
122
|
-
message: 'En route to sorting center',
|
|
123
|
-
orderState: 'Shipped',
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
id: 1,
|
|
127
|
-
message: 'Announced',
|
|
128
|
-
orderState: 'Shipped',
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
id: 1337,
|
|
132
|
-
message: 'Unknown status - check carrier track & trace page for more insights',
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
id: 999,
|
|
136
|
-
message: 'No label',
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
id: 1001,
|
|
140
|
-
message: 'Being announced',
|
|
141
|
-
orderState: 'Shipped',
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
id: 2000,
|
|
145
|
-
message: 'Cancelled',
|
|
146
|
-
orderState: 'Cancelled',
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
id: 2001,
|
|
150
|
-
message: 'Submitting cancellation request',
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
id: 92,
|
|
154
|
-
message: 'Driver en route',
|
|
155
|
-
orderState: 'Shipped',
|
|
156
|
-
},
|
|
157
|
-
];
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|