@pinelab/vendure-plugin-sendcloud 1.1.0 → 1.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 CHANGED
@@ -1,3 +1,11 @@
1
+ # 1.2.1 (2022-01-16)
2
+
3
+ - Correctly transition order to Delivered after it has been Shipped
4
+
5
+ # 1.2.0 (2023-11-16)
6
+
7
+ - Fulfill on order placement, to prevent stock levels going out of sync
8
+
1
9
  # 1.1.0 (2023-10-24)
2
10
 
3
11
  - Updated vendure to 2.1.1
package/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Vendure SendCloud plugin
2
2
 
3
- ![Vendure version](https://img.shields.io/badge/dynamic/json.svg?url=https%3A%2F%2Fraw.githubusercontent.com%2FPinelab-studio%2Fpinelab-vendure-plugins%2Fmain%2Fpackage.json&query=$.devDependencies[%27@vendure/core%27]&colorB=blue&label=Built%20on%20Vendure)
4
-
5
- ### Visit [pinelab-plugins.com](https://pinelab-plugins.com/plugin/vendure-plugin-sendcloud) for more documentation and examples.
3
+ ### [Official documentation here](https://pinelab-plugins.com/plugin/vendure-plugin-sendcloud)
6
4
 
7
5
  This plugin syncs orders to the SendCloud fulfillment platform.
8
6
 
@@ -8,12 +8,14 @@ export declare function fulfillAll(ctx: RequestContext, orderService: OrderServi
8
8
  /**
9
9
  * Fulfills all items to shipped using transitionFulfillmentToState
10
10
  */
11
- export declare function transitionToShipped(orderService: OrderService, ctx: RequestContext, order: Order, handler: ConfigurableOperationInput): Promise<Fulfillment>;
11
+ export declare function transitionToShipped(orderService: OrderService, ctx: RequestContext, order: Order, handler: ConfigurableOperationInput): Promise<Fulfillment | FulfillmentStateTransitionError>;
12
12
  /**
13
13
  * Fulfills all items to shipped, then to delivered using transitionFulfillmentToState
14
14
  */
15
- export declare function transitionToDelivered(orderService: OrderService, ctx: RequestContext, order: Order, handler: ConfigurableOperationInput): Promise<Fulfillment>;
15
+ export declare function transitionToDelivered(orderService: OrderService, ctx: RequestContext, order: Order, handler: ConfigurableOperationInput): Promise<(Fulfillment | FulfillmentStateTransitionError)[]>;
16
16
  /**
17
17
  * Throws the error result if the transition failed
18
+ * Ignores transition errors where from and to state are the same,
19
+ * because that still results in the situation we want
18
20
  */
19
21
  export declare function throwIfTransitionFailed(result: FulfillmentStateTransitionError | Fulfillment | AddFulfillmentToOrderResult): void;
@@ -36,14 +36,28 @@ exports.transitionToShipped = transitionToShipped;
36
36
  * Fulfills all items to shipped, then to delivered using transitionFulfillmentToState
37
37
  */
38
38
  async function transitionToDelivered(orderService, ctx, order, handler) {
39
- const fulfillment = await transitionToShipped(orderService, ctx, order, handler);
40
- const result = await orderService.transitionFulfillmentToState(ctx, fulfillment.id, 'Delivered');
41
- throwIfTransitionFailed(result);
42
- return result;
39
+ const shippedResult = await transitionToShipped(orderService, ctx, order, handler);
40
+ let fulfillments = [];
41
+ if (shippedResult.errorCode) {
42
+ fulfillments = await orderService.getOrderFulfillments(ctx, order);
43
+ }
44
+ else {
45
+ // if not an error, shippedResult is the only fulfillment
46
+ fulfillments.push(shippedResult);
47
+ }
48
+ const results = [];
49
+ for (const fulfillment of fulfillments) {
50
+ const result = await orderService.transitionFulfillmentToState(ctx, fulfillment.id, 'Delivered');
51
+ throwIfTransitionFailed(result);
52
+ results.push(result);
53
+ }
54
+ return results;
43
55
  }
44
56
  exports.transitionToDelivered = transitionToDelivered;
45
57
  /**
46
58
  * Throws the error result if the transition failed
59
+ * Ignores transition errors where from and to state are the same,
60
+ * because that still results in the situation we want
47
61
  */
48
62
  function throwIfTransitionFailed(result) {
49
63
  const stateError = result;
@@ -46,7 +46,7 @@ let SendcloudController = class SendcloudController {
46
46
  if (!body.parcel?.order_number) {
47
47
  return core_1.Logger.warn(`No order_number in incoming Sendcloud webhook: ${JSON.stringify(body.parcel)}`, constants_1.loggerCtx);
48
48
  }
49
- await this.sendcloudService.updateOrderStatus(ctx, status, body.parcel.order_number, body.parcel.tracking_number);
49
+ await this.sendcloudService.updateOrderStatus(ctx, status, body.parcel.order_number);
50
50
  }
51
51
  };
52
52
  __decorate([
@@ -1,7 +1,2 @@
1
1
  import { FulfillmentHandler } from '@vendure/core';
2
- export declare const sendcloudHandler: FulfillmentHandler<{
3
- trackingNumber: {
4
- type: "string";
5
- required: false;
6
- };
7
- }>;
2
+ export declare const sendcloudHandler: FulfillmentHandler<{}>;
@@ -11,18 +11,12 @@ exports.sendcloudHandler = new core_1.FulfillmentHandler({
11
11
  value: 'Send order to SendCloud',
12
12
  },
13
13
  ],
14
- args: {
15
- trackingNumber: {
16
- type: 'string',
17
- required: false,
18
- },
19
- },
14
+ args: {},
20
15
  createFulfillment: async (ctx, orders, orderItems, args) => {
21
16
  const orderCodes = orders.map((o) => o.code);
22
17
  core_1.Logger.info(`Fulfilled orders ${orderCodes.join(',')}`, constants_1.loggerCtx);
23
18
  return {
24
- method: `SendCloud - ${args.trackingNumber || orderCodes.join(',')} `,
25
- trackingCode: args.trackingNumber,
19
+ method: `SendCloud - ${orderCodes.join(',')} `,
26
20
  };
27
21
  },
28
22
  });
@@ -1,11 +1,11 @@
1
- import { OnApplicationBootstrap, OnModuleInit } from '@nestjs/common';
1
+ import { OnApplicationBootstrap } from '@nestjs/common';
2
2
  import { ModuleRef } from '@nestjs/core';
3
3
  import { ChannelService, EntityHydrator, EventBus, HistoryService, ID, JobQueueService, Order, OrderService, RequestContext, TransactionalConnection } from '@vendure/core';
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
7
  import { SendcloudParcelStatus, SendcloudPluginOptions } from './types/sendcloud.types';
8
- export declare class SendcloudService implements OnApplicationBootstrap, OnModuleInit {
8
+ export declare class SendcloudService implements OnApplicationBootstrap {
9
9
  private eventBus;
10
10
  private connection;
11
11
  private orderService;
@@ -17,18 +17,21 @@ export declare class SendcloudService implements OnApplicationBootstrap, OnModul
17
17
  private historyService;
18
18
  private jobQueue;
19
19
  constructor(eventBus: EventBus, connection: TransactionalConnection, orderService: OrderService, channelService: ChannelService, jobQueueService: JobQueueService, moduleRef: ModuleRef, options: SendcloudPluginOptions, entityHydrator: EntityHydrator, historyService: HistoryService);
20
- onModuleInit(): Promise<void>;
21
- onApplicationBootstrap(): void;
20
+ onApplicationBootstrap(): Promise<void>;
22
21
  createOrderInSendcloud(userCtx: RequestContext, order: Order): Promise<Parcel | undefined>;
23
22
  /**
24
23
  * Update order by given orderCode, returns undefined if no action was taken
25
24
  * Returns order if transition was successful
26
25
  */
27
- updateOrderStatus(ctx: RequestContext, sendcloudStatus: SendcloudParcelStatus, orderCode: string, trackingNumber: string): Promise<void>;
26
+ updateOrderStatus(ctx: RequestContext, sendcloudStatus: SendcloudParcelStatus, orderCode: string): Promise<void>;
28
27
  /**
29
- * Fulfill and ship all items
28
+ * Ship all items
30
29
  */
31
- shipAll(ctx: RequestContext, order: Order, trackingNumber: string): Promise<void>;
30
+ shipAll(ctx: RequestContext, order: Order): Promise<void>;
31
+ /**
32
+ * Fulfill without throwing errors. Logs an error if fulfilment fails
33
+ */
34
+ private safeFulfill;
32
35
  upsertConfig(ctx: RequestContext, config: {
33
36
  secret: string;
34
37
  publicKey: string;
@@ -11,6 +11,9 @@ 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
+ };
14
17
  Object.defineProperty(exports, "__esModule", { value: true });
15
18
  exports.SendcloudService = void 0;
16
19
  const common_1 = require("@nestjs/common");
@@ -22,6 +25,7 @@ const sendcloud_config_entity_1 = require("./sendcloud-config.entity");
22
25
  const sendcloud_client_1 = require("./sendcloud.client");
23
26
  const sendcloud_handler_1 = require("./sendcloud.handler");
24
27
  const src_1 = require("../../../util/src");
28
+ const util_1 = __importDefault(require("util"));
25
29
  let SendcloudService = class SendcloudService {
26
30
  constructor(eventBus, connection,
27
31
  // private rawConnection: Connection,
@@ -36,7 +40,15 @@ let SendcloudService = class SendcloudService {
36
40
  this.entityHydrator = entityHydrator;
37
41
  this.historyService = historyService;
38
42
  }
39
- async onModuleInit() {
43
+ async onApplicationBootstrap() {
44
+ // Listen for Settled orders to sync to sendcloud
45
+ this.eventBus.ofType(core_2.OrderPlacedEvent).subscribe(async (event) => {
46
+ await this.jobQueue.add({
47
+ orderCode: event.order.code,
48
+ ctx: event.ctx.serialize(),
49
+ }, { retries: 20 });
50
+ });
51
+ // Handle jobs
40
52
  this.jobQueue = await this.jobQueueService.createQueue({
41
53
  name: 'sendcloud',
42
54
  process: async ({ data }) => {
@@ -51,15 +63,6 @@ let SendcloudService = class SendcloudService {
51
63
  },
52
64
  });
53
65
  }
54
- onApplicationBootstrap() {
55
- // Listen for Settled orders to sync to sendcloud
56
- this.eventBus.ofType(core_2.OrderPlacedEvent).subscribe(async (event) => {
57
- await this.jobQueue.add({
58
- orderCode: event.order.code,
59
- ctx: event.ctx.serialize(),
60
- }, { retries: 20 });
61
- });
62
- }
63
66
  async createOrderInSendcloud(userCtx, order) {
64
67
  if (this.options.disabled) {
65
68
  core_2.Logger.info(`Plugin is disabled, not syncing order ${order.code}`, constants_1.loggerCtx);
@@ -94,7 +97,7 @@ let SendcloudService = class SendcloudService {
94
97
  * Update order by given orderCode, returns undefined if no action was taken
95
98
  * Returns order if transition was successful
96
99
  */
97
- async updateOrderStatus(ctx, sendcloudStatus, orderCode, trackingNumber) {
100
+ async updateOrderStatus(ctx, sendcloudStatus, orderCode) {
98
101
  let order = await this.connection
99
102
  .getRepository(ctx, core_2.Order)
100
103
  .findOne({ where: { code: orderCode }, relations: ['lines'] });
@@ -106,42 +109,46 @@ let SendcloudService = class SendcloudService {
106
109
  return core_2.Logger.info(`Not updating order with code ${orderCode}: Order already has state ${order.state}`, constants_1.loggerCtx);
107
110
  }
108
111
  if (sendcloudStatus.orderState === 'Shipped') {
109
- await this.shipAll(ctx, order, trackingNumber);
110
- return core_2.Logger.info(`Successfully update order ${orderCode} to ${sendcloudStatus.orderState}`, constants_1.loggerCtx);
112
+ await this.shipAll(ctx, order);
113
+ return core_2.Logger.info(`Successfully updated order ${orderCode} to Shipped`, constants_1.loggerCtx);
111
114
  }
112
115
  order = await this.connection
113
116
  .getRepository(ctx, core_2.Order)
114
117
  .findOneOrFail({ where: { code: orderCode }, relations: ['lines'] }); // Refetch in case state was updated
115
118
  if (sendcloudStatus.orderState === 'Delivered') {
116
- //FIX ME
117
119
  await (0, src_1.transitionToDelivered)(this.orderService, ctx, order, {
118
120
  code: sendcloud_handler_1.sendcloudHandler.code,
119
- arguments: [
120
- {
121
- name: 'trackingNumber',
122
- value: trackingNumber,
123
- },
124
- ],
121
+ arguments: [],
125
122
  });
126
- return core_2.Logger.info(`Successfully update order ${orderCode} to ${sendcloudStatus.orderState}`, constants_1.loggerCtx);
123
+ return core_2.Logger.info(`Successfully updated order ${orderCode} to Delivered`, constants_1.loggerCtx);
127
124
  }
128
125
  // Fall through, means unhandled state
129
126
  core_2.Logger.info(`Not handling state ${sendcloudStatus.orderState}`, constants_1.loggerCtx);
130
127
  }
131
128
  /**
132
- * Fulfill and ship all items
129
+ * Ship all items
133
130
  */
134
- async shipAll(ctx, order, trackingNumber) {
131
+ async shipAll(ctx, order) {
135
132
  await (0, src_1.transitionToShipped)(this.orderService, ctx, order, {
136
133
  code: sendcloud_handler_1.sendcloudHandler.code,
137
- arguments: [
138
- {
139
- name: 'trackingNumber',
140
- value: trackingNumber,
141
- },
142
- ],
134
+ arguments: [],
143
135
  });
144
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
+ }
145
152
  async upsertConfig(ctx, config) {
146
153
  const repo = this.connection.getRepository(ctx, sendcloud_config_entity_1.SendcloudConfigEntity);
147
154
  const existing = await repo.findOne({
@@ -209,6 +216,7 @@ let SendcloudService = class SendcloudService {
209
216
  if (!hasSendcloudHandler) {
210
217
  return core_2.Logger.info(`Order ${order.code} does not have SendCloud set as handler. Not syncing this order.`, constants_1.loggerCtx);
211
218
  }
219
+ await this.safeFulfill(ctx, order);
212
220
  core_2.Logger.info(`Syncing order ${orderCode} for channel ${ctx.channel.token}`, constants_1.loggerCtx);
213
221
  const result = await this.createOrderInSendcloud(ctx, order);
214
222
  if (result) {
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@pinelab/vendure-plugin-sendcloud",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Vendure plugin for syncing orders with SendCloud",
5
+ "icon": "truck",
5
6
  "author": "Martijn van de Brug <martijn@pinelab.studio>",
6
7
  "homepage": "https://pinelab-plugins.com/",
7
8
  "repository": "https://github.com/Pinelab-studio/pinelab-vendure-plugins",
@@ -22,5 +23,5 @@
22
23
  "test": "vitest run",
23
24
  "start": "yarn ts-node test/dev-server.ts"
24
25
  },
25
- "gitHead": "985a9e6ffd5cec67d1a3a4d924c9bd4bf7faa982"
26
+ "gitHead": "f2584b3c12624d023dcf9a10383e1c074e35b5d5"
26
27
  }