Browse Source

feat(core): Implement cancelOrder mutation

Relates to #120
Michael Bromley 6 years ago
parent
commit
a03fec7077

+ 1 - 0
packages/common/src/generated-shop-types.ts

@@ -1323,6 +1323,7 @@ export type Order = Node & {
     lines: Array<OrderLine>;
     adjustments: Array<Adjustment>;
     payments?: Maybe<Array<Payment>>;
+    fulfillments?: Maybe<Array<Fulfillment>>;
     subTotalBeforeTax: Scalars['Int'];
     subTotal: Scalars['Int'];
     currencyCode: CurrencyCode;

+ 7 - 0
packages/common/src/generated-types.ts

@@ -1544,6 +1544,7 @@ export type Mutation = {
   importProducts?: Maybe<ImportInfo>,
   settlePayment?: Maybe<Payment>,
   createFulfillment?: Maybe<Fulfillment>,
+  cancelOrder?: Maybe<Order>,
   /** Update an existing PaymentMethod */
   updatePaymentMethod: PaymentMethod,
   /** Create a new ProductOptionGroup */
@@ -1771,6 +1772,11 @@ export type MutationCreateFulfillmentArgs = {
 };
 
 
+export type MutationCancelOrderArgs = {
+  id: Scalars['ID']
+};
+
+
 export type MutationUpdatePaymentMethodArgs = {
   input: UpdatePaymentMethodInput
 };
@@ -1940,6 +1946,7 @@ export type Order = Node & {
   lines: Array<OrderLine>,
   adjustments: Array<Adjustment>,
   payments?: Maybe<Array<Payment>>,
+  fulfillments?: Maybe<Array<Fulfillment>>,
   subTotalBeforeTax: Scalars['Int'],
   subTotal: Scalars['Int'],
   currencyCode: CurrencyCode,

+ 16 - 0
packages/core/e2e/graphql/fragments.ts

@@ -452,3 +452,19 @@ export const CURRENT_USER_FRAGMENT = gql`
         channelTokens
     }
 `;
+export const VARIANT_WITH_STOCK_FRAGMENT = gql`
+    fragment VariantWithStock on ProductVariant {
+        id
+        stockOnHand
+        stockMovements {
+            items {
+                ... on StockMovement {
+                    id
+                    type
+                    quantity
+                }
+            }
+            totalItems
+        }
+    }
+`;

+ 96 - 77
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -1493,18 +1493,18 @@ export type Mutation = {
     createChannel: Channel;
     /** Update an existing Channel */
     updateChannel: Channel;
-    /** Create a new Country */
-    createCountry: Country;
-    /** Update an existing Country */
-    updateCountry: Country;
-    /** Delete a Country */
-    deleteCountry: DeletionResponse;
     /** Create a new Collection */
     createCollection: Collection;
     /** Update an existing Collection */
     updateCollection: Collection;
     /** Move a Collection to a different parent or index */
     moveCollection: Collection;
+    /** Create a new Country */
+    createCountry: Country;
+    /** Update an existing Country */
+    updateCountry: Country;
+    /** Delete a Country */
+    deleteCountry: DeletionResponse;
     /** Create a new CustomerGroup */
     createCustomerGroup: CustomerGroup;
     /** Update an existing CustomerGroup */
@@ -1537,10 +1537,11 @@ export type Mutation = {
     updateFacetValues: Array<FacetValue>;
     /** Delete one or more FacetValues */
     deleteFacetValues: Array<DeletionResponse>;
-    importProducts?: Maybe<ImportInfo>;
     updateGlobalSettings: GlobalSettings;
+    importProducts?: Maybe<ImportInfo>;
     settlePayment?: Maybe<Payment>;
     createFulfillment?: Maybe<Fulfillment>;
+    cancelOrder?: Maybe<Order>;
     /** Update an existing PaymentMethod */
     updatePaymentMethod: PaymentMethod;
     /** Create a new ProductOptionGroup */
@@ -1624,18 +1625,6 @@ export type MutationUpdateChannelArgs = {
     input: UpdateChannelInput;
 };
 
-export type MutationCreateCountryArgs = {
-    input: CreateCountryInput;
-};
-
-export type MutationUpdateCountryArgs = {
-    input: UpdateCountryInput;
-};
-
-export type MutationDeleteCountryArgs = {
-    id: Scalars['ID'];
-};
-
 export type MutationCreateCollectionArgs = {
     input: CreateCollectionInput;
 };
@@ -1648,6 +1637,18 @@ export type MutationMoveCollectionArgs = {
     input: MoveCollectionInput;
 };
 
+export type MutationCreateCountryArgs = {
+    input: CreateCountryInput;
+};
+
+export type MutationUpdateCountryArgs = {
+    input: UpdateCountryInput;
+};
+
+export type MutationDeleteCountryArgs = {
+    id: Scalars['ID'];
+};
+
 export type MutationCreateCustomerGroupArgs = {
     input: CreateCustomerGroupInput;
 };
@@ -1718,14 +1719,14 @@ export type MutationDeleteFacetValuesArgs = {
     force?: Maybe<Scalars['Boolean']>;
 };
 
-export type MutationImportProductsArgs = {
-    csvFile: Scalars['Upload'];
-};
-
 export type MutationUpdateGlobalSettingsArgs = {
     input: UpdateGlobalSettingsInput;
 };
 
+export type MutationImportProductsArgs = {
+    csvFile: Scalars['Upload'];
+};
+
 export type MutationSettlePaymentArgs = {
     id: Scalars['ID'];
 };
@@ -1734,6 +1735,10 @@ export type MutationCreateFulfillmentArgs = {
     input: CreateFulfillmentInput;
 };
 
+export type MutationCancelOrderArgs = {
+    id: Scalars['ID'];
+};
+
 export type MutationUpdatePaymentMethodArgs = {
     input: UpdatePaymentMethodInput;
 };
@@ -2333,20 +2338,20 @@ export type Query = {
     channels: Array<Channel>;
     channel?: Maybe<Channel>;
     activeChannel: Channel;
-    countries: CountryList;
-    country?: Maybe<Country>;
     collections: CollectionList;
     collection?: Maybe<Collection>;
     collectionFilters: Array<ConfigurableOperation>;
+    countries: CountryList;
+    country?: Maybe<Country>;
     customerGroups: Array<CustomerGroup>;
     customerGroup?: Maybe<CustomerGroup>;
     customers: CustomerList;
     customer?: Maybe<Customer>;
     facets: FacetList;
     facet?: Maybe<Facet>;
+    globalSettings: GlobalSettings;
     job?: Maybe<JobInfo>;
     jobs: Array<JobInfo>;
-    globalSettings: GlobalSettings;
     order?: Maybe<Order>;
     orders: OrderList;
     paymentMethods: PaymentMethodList;
@@ -2394,14 +2399,6 @@ export type QueryChannelArgs = {
     id: Scalars['ID'];
 };
 
-export type QueryCountriesArgs = {
-    options?: Maybe<CountryListOptions>;
-};
-
-export type QueryCountryArgs = {
-    id: Scalars['ID'];
-};
-
 export type QueryCollectionsArgs = {
     languageCode?: Maybe<LanguageCode>;
     options?: Maybe<CollectionListOptions>;
@@ -2412,6 +2409,14 @@ export type QueryCollectionArgs = {
     languageCode?: Maybe<LanguageCode>;
 };
 
+export type QueryCountriesArgs = {
+    options?: Maybe<CountryListOptions>;
+};
+
+export type QueryCountryArgs = {
+    id: Scalars['ID'];
+};
+
 export type QueryCustomerGroupArgs = {
     id: Scalars['ID'];
 };
@@ -3630,6 +3635,20 @@ export type CurrentUserFragment = { __typename?: 'CurrentUser' } & Pick<
     'id' | 'identifier' | 'channelTokens'
 >;
 
+export type VariantWithStockFragment = { __typename?: 'ProductVariant' } & Pick<
+    ProductVariant,
+    'id' | 'stockOnHand'
+> & {
+        stockMovements: { __typename?: 'StockMovementList' } & Pick<StockMovementList, 'totalItems'> & {
+                items: Array<
+                    { __typename?: 'StockAdjustment' | 'Sale' | 'Cancellation' | 'Return' } & Pick<
+                        StockMovement,
+                        'id' | 'type' | 'quantity'
+                    >
+                >;
+            };
+    };
+
 export type CreateAdministratorMutationVariables = {
     input: CreateAdministratorInput;
 };
@@ -3838,6 +3857,18 @@ export type GetProductSimpleQuery = { __typename?: 'Query' } & {
     product: Maybe<{ __typename?: 'Product' } & Pick<Product, 'id' | 'slug'>>;
 };
 
+export type GetStockMovementQueryVariables = {
+    id: Scalars['ID'];
+};
+
+export type GetStockMovementQuery = { __typename?: 'Query' } & {
+    product: Maybe<
+        { __typename?: 'Product' } & Pick<Product, 'id'> & {
+                variants: Array<{ __typename?: 'ProductVariant' } & VariantWithStockFragment>;
+            }
+    >;
+};
+
 export type GetProductsQueryVariables = {
     options?: Maybe<ProductListOptions>;
 };
@@ -3983,6 +4014,14 @@ export type GetOrderFulfillmentItemsQuery = { __typename?: 'Query' } & {
     >;
 };
 
+export type CancelOrderMutationVariables = {
+    id: Scalars['ID'];
+};
+
+export type CancelOrderMutation = { __typename?: 'Mutation' } & {
+    cancelOrder: Maybe<{ __typename?: 'Order' } & Pick<Order, 'id' | 'state' | 'active'>>;
+};
+
 export type AddOptionGroupToProductMutationVariables = {
     productId: Scalars['ID'];
     optionGroupId: Scalars['ID'];
@@ -4205,32 +4244,6 @@ export type GetCustomerIdsQuery = { __typename?: 'Query' } & {
     };
 };
 
-export type VariantWithStockFragment = { __typename?: 'ProductVariant' } & Pick<
-    ProductVariant,
-    'id' | 'stockOnHand'
-> & {
-        stockMovements: { __typename?: 'StockMovementList' } & Pick<StockMovementList, 'totalItems'> & {
-                items: Array<
-                    { __typename?: 'StockAdjustment' | 'Sale' | 'Cancellation' | 'Return' } & Pick<
-                        StockMovement,
-                        'id' | 'type' | 'quantity'
-                    >
-                >;
-            };
-    };
-
-export type GetStockMovementQueryVariables = {
-    id: Scalars['ID'];
-};
-
-export type GetStockMovementQuery = { __typename?: 'Query' } & {
-    product: Maybe<
-        { __typename?: 'Product' } & Pick<Product, 'id'> & {
-                variants: Array<{ __typename?: 'ProductVariant' } & VariantWithStockFragment>;
-            }
-    >;
-};
-
 export type UpdateStockMutationVariables = {
     input: Array<UpdateProductVariantInput>;
 };
@@ -4698,6 +4711,16 @@ export namespace CurrentUser {
     export type Fragment = CurrentUserFragment;
 }
 
+export namespace VariantWithStock {
+    export type Fragment = VariantWithStockFragment;
+    export type StockMovements = VariantWithStockFragment['stockMovements'];
+    export type Items = NonNullable<VariantWithStockFragment['stockMovements']['items'][0]>;
+    export type StockMovementInlineFragment = DiscriminateUnion<
+        RequireField<NonNullable<VariantWithStockFragment['stockMovements']['items'][0]>, '__typename'>,
+        { __typename: 'StockMovement' }
+    >;
+}
+
 export namespace CreateAdministrator {
     export type Variables = CreateAdministratorMutationVariables;
     export type Mutation = CreateAdministratorMutation;
@@ -4836,6 +4859,13 @@ export namespace GetProductSimple {
     export type Product = NonNullable<GetProductSimpleQuery['product']>;
 }
 
+export namespace GetStockMovement {
+    export type Variables = GetStockMovementQueryVariables;
+    export type Query = GetStockMovementQuery;
+    export type Product = NonNullable<GetStockMovementQuery['product']>;
+    export type Variants = VariantWithStockFragment;
+}
+
 export namespace GetProducts {
     export type Variables = GetProductsQueryVariables;
     export type Query = GetProductsQuery;
@@ -4961,6 +4991,12 @@ export namespace GetOrderFulfillmentItems {
     >;
 }
 
+export namespace CancelOrder {
+    export type Variables = CancelOrderMutationVariables;
+    export type Mutation = CancelOrderMutation;
+    export type CancelOrder = NonNullable<CancelOrderMutation['cancelOrder']>;
+}
+
 export namespace AddOptionGroupToProduct {
     export type Variables = AddOptionGroupToProductMutationVariables;
     export type Mutation = AddOptionGroupToProductMutation;
@@ -5137,23 +5173,6 @@ export namespace GetCustomerIds {
     export type Items = NonNullable<GetCustomerIdsQuery['customers']['items'][0]>;
 }
 
-export namespace VariantWithStock {
-    export type Fragment = VariantWithStockFragment;
-    export type StockMovements = VariantWithStockFragment['stockMovements'];
-    export type Items = NonNullable<VariantWithStockFragment['stockMovements']['items'][0]>;
-    export type StockMovementInlineFragment = DiscriminateUnion<
-        RequireField<NonNullable<VariantWithStockFragment['stockMovements']['items'][0]>, '__typename'>,
-        { __typename: 'StockMovement' }
-    >;
-}
-
-export namespace GetStockMovement {
-    export type Variables = GetStockMovementQueryVariables;
-    export type Query = GetStockMovementQuery;
-    export type Product = NonNullable<GetStockMovementQuery['product']>;
-    export type Variants = VariantWithStockFragment;
-}
-
 export namespace UpdateStock {
     export type Variables = UpdateStockMutationVariables;
     export type Mutation = UpdateStockMutation;

+ 14 - 1
packages/core/e2e/graphql/shared-definitions.ts

@@ -11,7 +11,8 @@ import {
     PRODUCT_VARIANT_FRAGMENT,
     PRODUCT_WITH_VARIANTS_FRAGMENT,
     ROLE_FRAGMENT,
-    TAX_RATE_FRAGMENT
+    TAX_RATE_FRAGMENT,
+    VARIANT_WITH_STOCK_FRAGMENT,
 } from './fragments';
 
 export const CREATE_ADMINISTRATOR = gql`
@@ -245,3 +246,15 @@ export const GET_PRODUCT_SIMPLE = gql`
         }
     }
 `;
+
+export const GET_STOCK_MOVEMENT = gql`
+    query GetStockMovement($id: ID!) {
+        product(id: $id) {
+            id
+            variants {
+                ...VariantWithStock
+            }
+        }
+    }
+    ${VARIANT_WITH_STOCK_FRAGMENT}
+`;

+ 144 - 16
packages/core/e2e/order.e2e-spec.ts

@@ -2,12 +2,15 @@
 import gql from 'graphql-tag';
 import path from 'path';
 
+import { StockMovementType } from '../../common/lib/generated-types';
+import { pick } from '../../common/lib/pick';
 import { ID } from '../../common/lib/shared-types';
 import { PaymentMethodHandler } from '../src/config/payment-method/payment-method-handler';
 
 import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
 import { ORDER_FRAGMENT, ORDER_WITH_LINES_FRAGMENT } from './graphql/fragments';
 import {
+    CancelOrder,
     CreateFulfillment,
     GetCustomerList,
     GetOrder,
@@ -15,8 +18,11 @@ import {
     GetOrderFulfillments,
     GetOrderList,
     GetOrderListFulfillments,
+    GetProductWithVariants,
+    GetStockMovement,
     OrderItemFragment,
     SettlePayment,
+    UpdateProductVariants,
 } from './graphql/generated-e2e-admin-types';
 import {
     AddItemToOrder,
@@ -26,7 +32,7 @@ import {
     SetShippingMethod,
     TransitionToState,
 } from './graphql/generated-e2e-shop-types';
-import { GET_CUSTOMER_LIST } from './graphql/shared-definitions';
+import { GET_CUSTOMER_LIST, GET_PRODUCT_WITH_VARIANTS, GET_STOCK_MOVEMENT, UPDATE_PRODUCT_VARIANTS, } from './graphql/shared-definitions';
 import {
     ADD_ITEM_TO_ORDER,
     ADD_PAYMENT,
@@ -112,7 +118,7 @@ describe('Orders resolver', () => {
             const { addPaymentToOrder } = await shopClient.query<
                 AddPaymentToOrder.Mutation,
                 AddPaymentToOrder.Variables
-                >(ADD_PAYMENT, {
+            >(ADD_PAYMENT, {
                 input: {
                     method: failsToSettlePaymentMethod.code,
                     metadata: {
@@ -128,7 +134,7 @@ describe('Orders resolver', () => {
             const { settlePayment } = await adminClient.query<
                 SettlePayment.Mutation,
                 SettlePayment.Variables
-                >(SETTLE_PAYMENT, {
+            >(SETTLE_PAYMENT, {
                 id: payment.id,
             });
 
@@ -149,7 +155,7 @@ describe('Orders resolver', () => {
             const { addPaymentToOrder } = await shopClient.query<
                 AddPaymentToOrder.Mutation,
                 AddPaymentToOrder.Variables
-                >(ADD_PAYMENT, {
+            >(ADD_PAYMENT, {
                 input: {
                     method: twoStagePaymentMethod.code,
                     metadata: {
@@ -165,7 +171,7 @@ describe('Orders resolver', () => {
             const { settlePayment } = await adminClient.query<
                 SettlePayment.Mutation,
                 SettlePayment.Variables
-                >(SETTLE_PAYMENT, {
+            >(SETTLE_PAYMENT, {
                 id: payment.id,
             });
 
@@ -255,7 +261,7 @@ describe('Orders resolver', () => {
             const { createFulfillment } = await adminClient.query<
                 CreateFulfillment.Mutation,
                 CreateFulfillment.Variables
-                >(CREATE_FULFILLMENT, {
+            >(CREATE_FULFILLMENT, {
                 input: {
                     lines: lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
                     method: 'Test1',
@@ -291,7 +297,7 @@ describe('Orders resolver', () => {
             const { createFulfillment } = await adminClient.query<
                 CreateFulfillment.Mutation,
                 CreateFulfillment.Variables
-                >(CREATE_FULFILLMENT, {
+            >(CREATE_FULFILLMENT, {
                 input: {
                     lines: [{ orderLineId: lines[1].id, quantity: 1 }],
                     method: 'Test2',
@@ -347,7 +353,7 @@ describe('Orders resolver', () => {
             const { createFulfillment } = await adminClient.query<
                 CreateFulfillment.Mutation,
                 CreateFulfillment.Variables
-                >(CREATE_FULFILLMENT, {
+            >(CREATE_FULFILLMENT, {
                 input: {
                     lines: [
                         {
@@ -374,7 +380,7 @@ describe('Orders resolver', () => {
             const { order } = await adminClient.query<
                 GetOrderFulfillments.Query,
                 GetOrderFulfillments.Variables
-                >(GET_ORDER_FULFILLMENTS, {
+            >(GET_ORDER_FULFILLMENTS, {
                 id: 'T_2',
             });
 
@@ -402,16 +408,128 @@ describe('Orders resolver', () => {
             const { order } = await adminClient.query<
                 GetOrderFulfillmentItems.Query,
                 GetOrderFulfillmentItems.Variables
-                >(GET_ORDER_FULFILLMENT_ITEMS, {
+            >(GET_ORDER_FULFILLMENT_ITEMS, {
                 id: 'T_2',
             });
 
-            expect(order!.fulfillments![0].orderItems).toEqual([
-                { id: 'T_3' },
-                { id: 'T_4' },
+            expect(order!.fulfillments![0].orderItems).toEqual([{ id: 'T_3' }, { id: 'T_4' }]);
+            expect(order!.fulfillments![1].orderItems).toEqual([{ id: 'T_5' }]);
+        });
+    });
+
+    describe('cancellation', () => {
+        let orderId: string;
+        let product: GetProductWithVariants.Product;
+        let productVariantId: string;
+
+        beforeAll(async () => {
+            const result = await adminClient.query<
+                GetProductWithVariants.Query,
+                GetProductWithVariants.Variables
+            >(GET_PRODUCT_WITH_VARIANTS, {
+                id: 'T_3',
+            });
+            product = result.product!;
+            productVariantId = product.variants[0].id;
+
+            // Set the ProductVariant to trackInventory
+            const { updateProductVariants } = await adminClient.query<
+                UpdateProductVariants.Mutation,
+                UpdateProductVariants.Variables
+            >(UPDATE_PRODUCT_VARIANTS, {
+                input: [
+                    {
+                        id: productVariantId,
+                        trackInventory: true,
+                    },
+                ],
+            });
+
+            // Add the ProductVariant to the Order
+            await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
+            const { addItemToOrder } = await shopClient.query<
+                AddItemToOrder.Mutation,
+                AddItemToOrder.Variables
+            >(ADD_ITEM_TO_ORDER, {
+                productVariantId,
+                quantity: 1,
+            });
+            orderId = addItemToOrder!.id;
+        });
+
+        it(
+            'cannot cancel from AddingItems state',
+            assertThrowsWithMessage(async () => {
+                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                    id: orderId,
+                });
+                expect(order!.state).toBe('AddingItems');
+                await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
+                    id: orderId,
+                });
+            }, 'Cannot transition Order from "AddingItems" to "Cancelled"'),
+        );
+
+        it(
+            'cannot cancel from ArrangingPayment state',
+            assertThrowsWithMessage(async () => {
+                await proceedToArrangingPayment(shopClient);
+                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                    id: orderId,
+                });
+                expect(order!.state).toBe('ArrangingPayment');
+                await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
+                    id: orderId,
+                });
+            }, 'Cannot transition Order from "ArrangingPayment" to "Cancelled"'),
+        );
+
+        it('reallocates stock back in', async () => {
+            const { addPaymentToOrder } = await shopClient.query<
+                AddPaymentToOrder.Mutation,
+                AddPaymentToOrder.Variables
+            >(ADD_PAYMENT, {
+                input: {
+                    method: twoStagePaymentMethod.code,
+                    metadata: {
+                        baz: 'quux',
+                    },
+                },
+            });
+
+            expect(addPaymentToOrder!.state).toBe('PaymentAuthorized');
+
+            const result1 = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
+                GET_STOCK_MOVEMENT,
+                {
+                    id: product.id,
+                },
+            );
+            const variant1 = result1.product!.variants[0];
+            expect(variant1.stockOnHand).toBe(99);
+            expect(variant1.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
+                { type: StockMovementType.ADJUSTMENT, quantity: 100 },
+                { type: StockMovementType.SALE, quantity: -1 },
             ]);
-            expect(order!.fulfillments![1].orderItems).toEqual([
-                { id: 'T_5' },
+
+            const { cancelOrder } = await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
+                id: orderId,
+            });
+
+            expect(cancelOrder!.state).toBe('Cancelled');
+
+            const result2 = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
+                GET_STOCK_MOVEMENT,
+                {
+                    id: product.id,
+                },
+            );
+            const variant2 = result2.product!.variants[0];
+            expect(variant2.stockOnHand).toBe(100);
+            expect(variant2.stockMovements.items.map(pick(['type', 'quantity']))).toEqual([
+                { type: StockMovementType.ADJUSTMENT, quantity: 100 },
+                { type: StockMovementType.SALE, quantity: -1 },
+                { type: StockMovementType.CANCELLATION, quantity: 1 },
             ]);
         });
     });
@@ -487,7 +605,7 @@ async function proceedToArrangingPayment(shopClient: TestShopClient): Promise<ID
     const { transitionOrderToState } = await shopClient.query<
         TransitionToState.Mutation,
         TransitionToState.Variables
-        >(TRANSITION_TO_STATE, { state: 'ArrangingPayment' });
+    >(TRANSITION_TO_STATE, { state: 'ArrangingPayment' });
 
     return transitionOrderToState!.id;
 }
@@ -575,3 +693,13 @@ export const GET_ORDER_FULFILLMENT_ITEMS = gql`
         }
     }
 `;
+
+export const CANCEL_ORDER = gql`
+    mutation CancelOrder($id: ID!) {
+        cancelOrder(id: $id) {
+            id
+            state
+            active
+        }
+    }
+`;

+ 5 - 29
packages/core/e2e/stock-control.e2e-spec.ts

@@ -6,6 +6,7 @@ import { PaymentMethodHandler } from '../src/config/payment-method/payment-metho
 import { OrderState } from '../src/service/helpers/order-state-machine/order-state';
 
 import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { VARIANT_WITH_STOCK_FRAGMENT } from './graphql/fragments';
 import {
     CreateAddressInput,
     GetStockMovement,
@@ -15,6 +16,7 @@ import {
     VariantWithStockFragment,
 } from './graphql/generated-e2e-admin-types';
 import { AddItemToOrder, AddPaymentToOrder, PaymentInput, SetShippingAddress, TransitionToState } from './graphql/generated-e2e-shop-types';
+import { GET_STOCK_MOVEMENT } from './graphql/shared-definitions';
 import { ADD_ITEM_TO_ORDER, ADD_PAYMENT, SET_SHIPPING_ADDRESS, TRANSITION_TO_STATE } from './graphql/shop-definitions';
 import { TestAdminClient, TestShopClient } from './test-client';
 import { TestServer } from './test-server';
@@ -235,37 +237,11 @@ const testPaymentMethod = new PaymentMethodHandler({
             metadata,
         };
     },
+    settlePayment: order => ({
+        success: true,
+    }),
 });
 
-const VARIANT_WITH_STOCK_FRAGMENT = gql`
-    fragment VariantWithStock on ProductVariant {
-        id
-        stockOnHand
-        stockMovements {
-            items {
-                ... on StockMovement {
-                    id
-                    type
-                    quantity
-                }
-            }
-            totalItems
-        }
-    }
-`;
-
-const GET_STOCK_MOVEMENT = gql`
-    query GetStockMovement($id: ID!) {
-        product(id: $id) {
-            id
-            variants {
-                ...VariantWithStock
-            }
-        }
-    }
-    ${VARIANT_WITH_STOCK_FRAGMENT}
-`;
-
 const UPDATE_STOCK_ON_HAND = gql`
     mutation UpdateStock($input: [UpdateProductVariantInput!]!) {
         updateProductVariants(input: $input) {

+ 8 - 1
packages/core/src/api/resolvers/admin/order.resolver.ts

@@ -1,10 +1,11 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
+    MutationCancelOrderArgs,
     MutationCreateFulfillmentArgs,
     MutationSettlePaymentArgs,
     Permission,
     QueryOrderArgs,
-    QueryOrdersArgs
+    QueryOrdersArgs,
 } from '@vendure/common/lib/generated-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 
@@ -44,4 +45,10 @@ export class OrderResolver {
     async createFulfillment(@Ctx() ctx: RequestContext, @Args() args: MutationCreateFulfillmentArgs) {
         return this.orderService.createFulfillment(ctx, args.input);
     }
+
+    @Mutation()
+    @Allow(Permission.UpdateOrder)
+    async cancelOrder(@Ctx() ctx: RequestContext, @Args() args: MutationCancelOrderArgs) {
+        return this.orderService.cancelOrder(ctx, args.id);
+    }
 }

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

@@ -6,6 +6,7 @@ type Query {
 type Mutation {
     settlePayment(id: ID!): Payment
     createFulfillment(input: CreateFulfillmentInput!): Fulfillment
+    cancelOrder(id: ID!): Order
 }
 
 # generated by generateListOptions function

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

@@ -26,6 +26,10 @@ export class OrderStateMachine {
         return this.initialState;
     }
 
+    canTransition(currentState: OrderState, newState: OrderState): boolean {
+        return  new FSM(this.config, currentState).canTransitionTo(newState);
+    }
+
     getNextStates(order: Order): OrderState[] {
         const fsm = new FSM(this.config, order.state);
         return fsm.getNextStates();
@@ -60,6 +64,9 @@ export class OrderStateMachine {
             data.order.orderPlacedAt = new Date();
             await this.stockMovementService.createSalesForOrder(data.order);
         }
+        if (toState === 'Cancelled') {
+            await this.stockMovementService.createCancellationsForOrder(data.order);
+        }
         this.eventBus.publish(new OrderStateTransitionEvent(fromState, toState, data.ctx, data.order));
     }
 

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

@@ -26,16 +26,16 @@ export const orderStateTransitions: Transitions<OrderState> = {
         to: ['PaymentAuthorized', 'PaymentSettled', 'AddingItems'],
     },
     PaymentAuthorized: {
-        to: ['PaymentSettled'],
+        to: ['PaymentSettled', 'Cancelled'],
     },
     PaymentSettled: {
-        to: ['PartiallyFulfilled', 'Fulfilled', 'Cancelled'],
+        to: ['PartiallyFulfilled', 'Fulfilled'],
     },
     PartiallyFulfilled: {
-        to: ['Cancelled', 'Fulfilled', 'PartiallyFulfilled'],
+        to: ['Fulfilled', 'PartiallyFulfilled'],
     },
     Fulfilled: {
-        to: ['Cancelled'],
+        to: [],
     },
     Cancelled: {
         to: [],

+ 4 - 0
packages/core/src/service/services/order.service.ts

@@ -410,6 +410,10 @@ export class OrderService {
         return fulfillment.orderItems;
     }
 
+    async cancelOrder(ctx: RequestContext, id: ID): Promise<Order> {
+        return this.transitionToState(ctx, id, 'Cancelled');
+    }
+
     async addCustomerToOrder(ctx: RequestContext, orderId: ID, customer: Customer): Promise<Order> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         if (order.customer && !idsAreEqual(order.customer.id, customer.id)) {

+ 20 - 0
packages/core/src/service/services/stock-movement.service.ts

@@ -11,6 +11,7 @@ import { ShippingEligibilityChecker } from '../../config/shipping-method/shippin
 import { Order } from '../../entity/order/order.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { ShippingMethod } from '../../entity/shipping-method/shipping-method.entity';
+import { Cancellation } from '../../entity/stock-movement/cancellation.entity';
 import { Sale } from '../../entity/stock-movement/sale.entity';
 import { StockAdjustment } from '../../entity/stock-movement/stock-adjustment.entity';
 import { StockMovement } from '../../entity/stock-movement/stock-movement.entity';
@@ -79,4 +80,23 @@ export class StockMovementService {
         }
         return this.connection.getRepository(Sale).save(sales);
     }
+
+    async createCancellationsForOrder(order: Order): Promise<Cancellation[]> {
+        const cancellations: Cancellation[] = [];
+        for (const line of order.lines) {
+            const { productVariant } = line;
+            const cancellation = new Cancellation({
+                productVariant,
+                quantity: line.quantity,
+                orderLine: line,
+            });
+            cancellations.push(cancellation);
+
+            if (productVariant.trackInventory === true) {
+                productVariant.stockOnHand += line.quantity;
+                await this.connection.getRepository(ProductVariant).save(productVariant);
+            }
+        }
+        return this.connection.getRepository(Cancellation).save(cancellations);
+    }
 }

File diff suppressed because it is too large
+ 0 - 0
schema-admin.json


Some files were not shown because too many files changed in this diff