Parcourir la source

feat(core): Allow cancellation of order by id

Michael Bromley il y a 6 ans
Parent
commit
8d0a0eb13c

+ 2 - 1
packages/common/src/generated-types.ts

@@ -185,7 +185,8 @@ export type Cancellation = Node & StockMovement & {
 };
 
 export type CancelOrderInput = {
-  lines: Array<OrderLineInput>,
+  orderId: Scalars['ID'],
+  lines?: Maybe<Array<OrderLineInput>>,
   reason?: Maybe<Scalars['String']>,
 };
 

+ 2 - 1
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -186,7 +186,8 @@ export type Cancellation = Node &
     };
 
 export type CancelOrderInput = {
-    lines: Array<OrderLineInput>;
+    orderId: Scalars['ID'];
+    lines?: Maybe<Array<OrderLineInput>>;
     reason?: Maybe<Scalars['String']>;
 };
 

Fichier diff supprimé car celui-ci est trop grand
+ 382 - 266
packages/core/e2e/order.e2e-spec.ts


+ 2 - 2
packages/core/e2e/shop-order.e2e-spec.ts

@@ -279,7 +279,7 @@ describe('Shop orders', () => {
         it('nextOrderStates returns next valid states', async () => {
             const result = await shopClient.query<GetNextOrderStates.Query>(GET_NEXT_STATES);
 
-            expect(result.nextOrderStates).toEqual(['ArrangingPayment']);
+            expect(result.nextOrderStates).toEqual(['ArrangingPayment', 'Cancelled']);
         });
 
         it(
@@ -531,7 +531,7 @@ describe('Shop orders', () => {
         it('nextOrderStates returns next valid states', async () => {
             const result = await shopClient.query<GetNextOrderStates.Query>(GET_NEXT_STATES);
 
-            expect(result.nextOrderStates).toEqual(['ArrangingPayment']);
+            expect(result.nextOrderStates).toEqual(['ArrangingPayment', 'Cancelled']);
         });
 
         it('logging out and back in again resumes the last active order', async () => {

+ 4 - 1
packages/core/src/api/schema/admin-api/order.api.graphql

@@ -22,7 +22,10 @@ input FulfillOrderInput {
 }
 
 input CancelOrderInput {
-    lines: [OrderLineInput!]!
+    "The id of the order to be cancelled"
+    orderId: ID!
+    "Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled"
+    lines: [OrderLineInput!]
     reason: String
 }
 

+ 10 - 5
packages/core/src/service/helpers/order-state-machine/order-state-machine.ts

@@ -18,10 +18,12 @@ export class OrderStateMachine {
     private readonly config: StateMachineConfig<OrderState, OrderTransitionData>;
     private readonly initialState: OrderState = 'AddingItems';
 
-    constructor(private configService: ConfigService,
-                private stockMovementService: StockMovementService,
-                private historyService: HistoryService,
-                private eventBus: EventBus) {
+    constructor(
+        private configService: ConfigService,
+        private stockMovementService: StockMovementService,
+        private historyService: HistoryService,
+        private eventBus: EventBus,
+    ) {
         this.config = this.initConfig();
     }
 
@@ -30,7 +32,7 @@ export class OrderStateMachine {
     }
 
     canTransition(currentState: OrderState, newState: OrderState): boolean {
-        return  new FSM(this.config, currentState).canTransitionTo(newState);
+        return new FSM(this.config, currentState).canTransitionTo(newState);
     }
 
     getNextStates(order: Order): OrderState[] {
@@ -67,6 +69,9 @@ export class OrderStateMachine {
             data.order.orderPlacedAt = new Date();
             await this.stockMovementService.createSalesForOrder(data.order);
         }
+        if (toState === 'Cancelled') {
+            data.order.active = false;
+        }
         this.eventBus.publish(new OrderStateTransitionEvent(fromState, toState, data.ctx, data.order));
         await this.historyService.createHistoryEntryForOrder({
             orderId: data.order.id,

+ 2 - 2
packages/core/src/service/helpers/order-state-machine/order-state.ts

@@ -20,10 +20,10 @@ export type OrderState =
 
 export const orderStateTransitions: Transitions<OrderState> = {
     AddingItems: {
-        to: ['ArrangingPayment'],
+        to: ['ArrangingPayment', 'Cancelled'],
     },
     ArrangingPayment: {
-        to: ['PaymentAuthorized', 'PaymentSettled', 'AddingItems'],
+        to: ['PaymentAuthorized', 'PaymentSettled', 'AddingItems', 'Cancelled'],
     },
     PaymentAuthorized: {
         to: ['PaymentSettled', 'Cancelled'],

+ 43 - 17
packages/core/src/service/services/order.service.ts

@@ -468,15 +468,41 @@ export class OrderService {
     }
 
     async cancelOrder(ctx: RequestContext, input: CancelOrderInput): Promise<Order> {
-        if (
-            !input.lines ||
-            input.lines.length === 0 ||
-            input.lines.reduce((total, line) => total + line.quantity, 0) === 0
-        ) {
+        let allOrderItemsCancelled = false;
+        if (input.lines != null) {
+            allOrderItemsCancelled = await this.cancelOrderByOrderLines(ctx, input, input.lines);
+        } else {
+            allOrderItemsCancelled = await this.cancelOrderById(ctx, input);
+        }
+        if (allOrderItemsCancelled) {
+            await this.transitionToState(ctx, input.orderId, 'Cancelled');
+        }
+        return assertFound(this.findOne(ctx, input.orderId));
+    }
+
+    private async cancelOrderById(ctx: RequestContext, input: CancelOrderInput): Promise<boolean> {
+        const order = await this.getOrderOrThrow(ctx, input.orderId);
+        if (order.state === 'AddingItems' || order.state === 'ArrangingPayment') {
+            return true;
+        } else {
+            const lines: OrderLineInput[] = order.lines.map(l => ({
+                orderLineId: l.id as string,
+                quantity: l.quantity,
+            }));
+            return this.cancelOrderByOrderLines(ctx, input, lines);
+        }
+    }
+
+    private async cancelOrderByOrderLines(
+        ctx: RequestContext,
+        input: CancelOrderInput,
+        lines: OrderLineInput[],
+    ): Promise<boolean> {
+        if (lines.length === 0 || lines.reduce((total, line) => total + line.quantity, 0) === 0) {
             throw new UserInputError('error.cancel-order-lines-nothing-to-cancel');
         }
         const { items, orders } = await this.getOrdersAndItemsFromLines(
-            input.lines,
+            lines,
             i => !i.cancellationId,
             'error.cancel-order-lines-quantity-too-high',
         );
@@ -484,12 +510,22 @@ export class OrderService {
             throw new IllegalOperationError('error.order-lines-must-belong-to-same-order');
         }
         const order = orders[0];
+        if (!idsAreEqual(order.id, input.orderId)) {
+            throw new IllegalOperationError('error.order-lines-must-belong-to-same-order');
+        }
         if (order.state === 'AddingItems' || order.state === 'ArrangingPayment') {
             throw new IllegalOperationError('error.cancel-order-lines-invalid-order-state', {
                 state: order.state,
             });
         }
         await this.stockMovementService.createCancellationsForOrderItems(items);
+
+        const orderWithItems = await this.connection.getRepository(Order).findOne(order.id, {
+            relations: ['lines', 'lines.items'],
+        });
+        if (!orderWithItems) {
+            throw new InternalServerError('error.could-not-find-order');
+        }
         await this.historyService.createHistoryEntryForOrder({
             ctx,
             orderId: order.id,
@@ -499,20 +535,10 @@ export class OrderService {
                 reason: input.reason || undefined,
             },
         });
-
-        const orderWithItems = await this.connection.getRepository(Order).findOne(order.id, {
-            relations: ['lines', 'lines.items'],
-        });
-        if (!orderWithItems) {
-            throw new InternalServerError('error.could-not-find-order');
-        }
         const allOrderItemsCancelled = orderWithItems.lines
             .reduce((orderItems, line) => [...orderItems, ...line.items], [] as OrderItem[])
             .every(orderItem => !!orderItem.cancellationId);
-        if (allOrderItemsCancelled) {
-            await this.transitionToState(ctx, order.id, 'Cancelled');
-        }
-        return assertFound(this.findOne(ctx, order.id));
+        return allOrderItemsCancelled;
     }
 
     async refundOrder(ctx: RequestContext, input: RefundOrderInput): Promise<Refund> {

+ 15 - 2
packages/core/src/service/services/stock-movement.service.ts

@@ -47,7 +47,11 @@ export class StockMovementService {
             });
     }
 
-    async adjustProductVariantStock(productVariantId: ID, oldStockLevel: number, newStockLevel: number): Promise<StockAdjustment | undefined> {
+    async adjustProductVariantStock(
+        productVariantId: ID,
+        oldStockLevel: number,
+        newStockLevel: number,
+    ): Promise<StockAdjustment | undefined> {
         if (oldStockLevel === newStockLevel) {
             return;
         }
@@ -87,8 +91,17 @@ export class StockMovementService {
             relations: ['line', 'line.productVariant'],
         });
         const cancellations: Cancellation[] = [];
+        const variantsMap = new Map<ID, ProductVariant>();
         for (const item of orderItems) {
-            const { productVariant } = item.line;
+            let productVariant: ProductVariant;
+            const productVariantId = item.line.productVariant.id;
+            if (variantsMap.has(productVariantId)) {
+                // tslint:disable-next-line:no-non-null-assertion
+                productVariant = variantsMap.get(productVariantId)!;
+            } else {
+                productVariant = item.line.productVariant;
+                variantsMap.set(productVariantId, productVariant);
+            }
             const cancellation = new Cancellation({
                 productVariant,
                 quantity: 1,

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
schema-admin.json


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff