Bläddra i källkod

fix(core): Correctly cancel sales when cancelling Fulfillment

Fixes #1198
Michael Bromley 4 år sedan
förälder
incheckning
00ac70dd63

+ 4 - 4
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -3177,7 +3177,7 @@ export type OrderItem = Node & {
   /** The price of a single unit including discounts and tax */
   discountedUnitPriceWithTax: Scalars['Int'];
   /**
-   * The actual unit price, taking into account both item discounts _and_ prorated (proportially-distributed)
+   * The actual unit price, taking into account both item discounts _and_ prorated (proportionally-distributed)
    * Order-level discounts. This value is the true economic value of the OrderItem, and is used in tax
    * and refund calculations.
    */
@@ -3227,7 +3227,7 @@ export type OrderLine = Node & {
   /** The price of a single unit including discounts and tax */
   discountedUnitPriceWithTax: Scalars['Int'];
   /**
-   * The actual unit price, taking into account both item discounts _and_ prorated (proportially-distributed)
+   * The actual unit price, taking into account both item discounts _and_ prorated (proportionally-distributed)
    * Order-level discounts. This value is the true economic value of the OrderItem, and is used in tax
    * and refund calculations.
    */
@@ -3239,14 +3239,14 @@ export type OrderLine = Node & {
   taxRate: Scalars['Float'];
   /** The total price of the line excluding tax and discounts. */
   linePrice: Scalars['Int'];
-  /** The total price of the line including tax bit excluding discounts. */
+  /** The total price of the line including tax but excluding discounts. */
   linePriceWithTax: Scalars['Int'];
   /** The price of the line including discounts, excluding tax */
   discountedLinePrice: Scalars['Int'];
   /** The price of the line including discounts and tax */
   discountedLinePriceWithTax: Scalars['Int'];
   /**
-   * The actual line price, taking into account both item discounts _and_ prorated (proportially-distributed)
+   * The actual line price, taking into account both item discounts _and_ prorated (proportionally-distributed)
    * Order-level discounts. This value is the true economic value of the OrderLine, and is used in tax
    * and refund calculations.
    */

+ 4 - 4
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -2966,7 +2966,7 @@ export type OrderItem = Node & {
     /** The price of a single unit including discounts and tax */
     discountedUnitPriceWithTax: Scalars['Int'];
     /**
-     * The actual unit price, taking into account both item discounts _and_ prorated (proportially-distributed)
+     * The actual unit price, taking into account both item discounts _and_ prorated (proportionally-distributed)
      * Order-level discounts. This value is the true economic value of the OrderItem, and is used in tax
      * and refund calculations.
      */
@@ -3014,7 +3014,7 @@ export type OrderLine = Node & {
     /** The price of a single unit including discounts and tax */
     discountedUnitPriceWithTax: Scalars['Int'];
     /**
-     * The actual unit price, taking into account both item discounts _and_ prorated (proportially-distributed)
+     * The actual unit price, taking into account both item discounts _and_ prorated (proportionally-distributed)
      * Order-level discounts. This value is the true economic value of the OrderItem, and is used in tax
      * and refund calculations.
      */
@@ -3026,14 +3026,14 @@ export type OrderLine = Node & {
     taxRate: Scalars['Float'];
     /** The total price of the line excluding tax and discounts. */
     linePrice: Scalars['Int'];
-    /** The total price of the line including tax bit excluding discounts. */
+    /** The total price of the line including tax but excluding discounts. */
     linePriceWithTax: Scalars['Int'];
     /** The price of the line including discounts, excluding tax */
     discountedLinePrice: Scalars['Int'];
     /** The price of the line including discounts and tax */
     discountedLinePriceWithTax: Scalars['Int'];
     /**
-     * The actual line price, taking into account both item discounts _and_ prorated (proportially-distributed)
+     * The actual line price, taking into account both item discounts _and_ prorated (proportionally-distributed)
      * Order-level discounts. This value is the true economic value of the OrderLine, and is used in tax
      * and refund calculations.
      */

+ 4 - 4
packages/common/src/generated-shop-types.ts

@@ -1898,7 +1898,7 @@ export type OrderItem = Node & {
     /** The price of a single unit including discounts and tax */
     discountedUnitPriceWithTax: Scalars['Int'];
     /**
-     * The actual unit price, taking into account both item discounts _and_ prorated (proportially-distributed)
+     * The actual unit price, taking into account both item discounts _and_ prorated (proportionally-distributed)
      * Order-level discounts. This value is the true economic value of the OrderItem, and is used in tax
      * and refund calculations.
      */
@@ -1948,7 +1948,7 @@ export type OrderLine = Node & {
     /** The price of a single unit including discounts and tax */
     discountedUnitPriceWithTax: Scalars['Int'];
     /**
-     * The actual unit price, taking into account both item discounts _and_ prorated (proportially-distributed)
+     * The actual unit price, taking into account both item discounts _and_ prorated (proportionally-distributed)
      * Order-level discounts. This value is the true economic value of the OrderItem, and is used in tax
      * and refund calculations.
      */
@@ -1960,14 +1960,14 @@ export type OrderLine = Node & {
     taxRate: Scalars['Float'];
     /** The total price of the line excluding tax and discounts. */
     linePrice: Scalars['Int'];
-    /** The total price of the line including tax bit excluding discounts. */
+    /** The total price of the line including tax but excluding discounts. */
     linePriceWithTax: Scalars['Int'];
     /** The price of the line including discounts, excluding tax */
     discountedLinePrice: Scalars['Int'];
     /** The price of the line including discounts and tax */
     discountedLinePriceWithTax: Scalars['Int'];
     /**
-     * The actual line price, taking into account both item discounts _and_ prorated (proportially-distributed)
+     * The actual line price, taking into account both item discounts _and_ prorated (proportionally-distributed)
      * Order-level discounts. This value is the true economic value of the OrderLine, and is used in tax
      * and refund calculations.
      */

+ 4 - 4
packages/common/src/generated-types.ts

@@ -3125,7 +3125,7 @@ export type OrderItem = Node & {
   /** The price of a single unit including discounts and tax */
   discountedUnitPriceWithTax: Scalars['Int'];
   /**
-   * The actual unit price, taking into account both item discounts _and_ prorated (proportially-distributed)
+   * The actual unit price, taking into account both item discounts _and_ prorated (proportionally-distributed)
    * Order-level discounts. This value is the true economic value of the OrderItem, and is used in tax
    * and refund calculations.
    */
@@ -3175,7 +3175,7 @@ export type OrderLine = Node & {
   /** The price of a single unit including discounts and tax */
   discountedUnitPriceWithTax: Scalars['Int'];
   /**
-   * The actual unit price, taking into account both item discounts _and_ prorated (proportially-distributed)
+   * The actual unit price, taking into account both item discounts _and_ prorated (proportionally-distributed)
    * Order-level discounts. This value is the true economic value of the OrderItem, and is used in tax
    * and refund calculations.
    */
@@ -3187,14 +3187,14 @@ export type OrderLine = Node & {
   taxRate: Scalars['Float'];
   /** The total price of the line excluding tax and discounts. */
   linePrice: Scalars['Int'];
-  /** The total price of the line including tax bit excluding discounts. */
+  /** The total price of the line including tax but excluding discounts. */
   linePriceWithTax: Scalars['Int'];
   /** The price of the line including discounts, excluding tax */
   discountedLinePrice: Scalars['Int'];
   /** The price of the line including discounts and tax */
   discountedLinePriceWithTax: Scalars['Int'];
   /**
-   * The actual line price, taking into account both item discounts _and_ prorated (proportially-distributed)
+   * The actual line price, taking into account both item discounts _and_ prorated (proportionally-distributed)
    * Order-level discounts. This value is the true economic value of the OrderLine, and is used in tax
    * and refund calculations.
    */

+ 35 - 4
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -2966,7 +2966,7 @@ export type OrderItem = Node & {
     /** The price of a single unit including discounts and tax */
     discountedUnitPriceWithTax: Scalars['Int'];
     /**
-     * The actual unit price, taking into account both item discounts _and_ prorated (proportially-distributed)
+     * The actual unit price, taking into account both item discounts _and_ prorated (proportionally-distributed)
      * Order-level discounts. This value is the true economic value of the OrderItem, and is used in tax
      * and refund calculations.
      */
@@ -3014,7 +3014,7 @@ export type OrderLine = Node & {
     /** The price of a single unit including discounts and tax */
     discountedUnitPriceWithTax: Scalars['Int'];
     /**
-     * The actual unit price, taking into account both item discounts _and_ prorated (proportially-distributed)
+     * The actual unit price, taking into account both item discounts _and_ prorated (proportionally-distributed)
      * Order-level discounts. This value is the true economic value of the OrderItem, and is used in tax
      * and refund calculations.
      */
@@ -3026,14 +3026,14 @@ export type OrderLine = Node & {
     taxRate: Scalars['Float'];
     /** The total price of the line excluding tax and discounts. */
     linePrice: Scalars['Int'];
-    /** The total price of the line including tax bit excluding discounts. */
+    /** The total price of the line including tax but excluding discounts. */
     linePriceWithTax: Scalars['Int'];
     /** The price of the line including discounts, excluding tax */
     discountedLinePrice: Scalars['Int'];
     /** The price of the line including discounts and tax */
     discountedLinePriceWithTax: Scalars['Int'];
     /**
-     * The actual line price, taking into account both item discounts _and_ prorated (proportially-distributed)
+     * The actual line price, taking into account both item discounts _and_ prorated (proportionally-distributed)
      * Order-level discounts. This value is the true economic value of the OrderLine, and is used in tax
      * and refund calculations.
      */
@@ -6865,6 +6865,17 @@ export type UpdateStockMutationVariables = Exact<{
 
 export type UpdateStockMutation = { updateProductVariants: Array<Maybe<VariantWithStockFragment>> };
 
+export type TransitionFulfillmentToStateMutationVariables = Exact<{
+    id: Scalars['ID'];
+    state: Scalars['String'];
+}>;
+
+export type TransitionFulfillmentToStateMutation = {
+    transitionFulfillmentToState:
+        | Pick<Fulfillment, 'id' | 'state' | 'nextStates' | 'createdAt'>
+        | Pick<FulfillmentStateTransitionError, 'errorCode' | 'message' | 'transitionError'>;
+};
+
 export type GetTagListQueryVariables = Exact<{
     options?: Maybe<TagListOptions>;
 }>;
@@ -9248,6 +9259,26 @@ export namespace UpdateStock {
     >;
 }
 
+export namespace TransitionFulfillmentToState {
+    export type Variables = TransitionFulfillmentToStateMutationVariables;
+    export type Mutation = TransitionFulfillmentToStateMutation;
+    export type TransitionFulfillmentToState = NonNullable<
+        TransitionFulfillmentToStateMutation['transitionFulfillmentToState']
+    >;
+    export type FulfillmentInlineFragment = DiscriminateUnion<
+        NonNullable<TransitionFulfillmentToStateMutation['transitionFulfillmentToState']>,
+        { __typename?: 'Fulfillment' }
+    >;
+    export type ErrorResultInlineFragment = DiscriminateUnion<
+        NonNullable<TransitionFulfillmentToStateMutation['transitionFulfillmentToState']>,
+        { __typename?: 'ErrorResult' }
+    >;
+    export type FulfillmentStateTransitionErrorInlineFragment = DiscriminateUnion<
+        NonNullable<TransitionFulfillmentToStateMutation['transitionFulfillmentToState']>,
+        { __typename?: 'FulfillmentStateTransitionError' }
+    >;
+}
+
 export namespace GetTagList {
     export type Variables = GetTagListQueryVariables;
     export type Query = GetTagListQuery;

+ 4 - 4
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -1837,7 +1837,7 @@ export type OrderItem = Node & {
     /** The price of a single unit including discounts and tax */
     discountedUnitPriceWithTax: Scalars['Int'];
     /**
-     * The actual unit price, taking into account both item discounts _and_ prorated (proportially-distributed)
+     * The actual unit price, taking into account both item discounts _and_ prorated (proportionally-distributed)
      * Order-level discounts. This value is the true economic value of the OrderItem, and is used in tax
      * and refund calculations.
      */
@@ -1885,7 +1885,7 @@ export type OrderLine = Node & {
     /** The price of a single unit including discounts and tax */
     discountedUnitPriceWithTax: Scalars['Int'];
     /**
-     * The actual unit price, taking into account both item discounts _and_ prorated (proportially-distributed)
+     * The actual unit price, taking into account both item discounts _and_ prorated (proportionally-distributed)
      * Order-level discounts. This value is the true economic value of the OrderItem, and is used in tax
      * and refund calculations.
      */
@@ -1897,14 +1897,14 @@ export type OrderLine = Node & {
     taxRate: Scalars['Float'];
     /** The total price of the line excluding tax and discounts. */
     linePrice: Scalars['Int'];
-    /** The total price of the line including tax bit excluding discounts. */
+    /** The total price of the line including tax but excluding discounts. */
     linePriceWithTax: Scalars['Int'];
     /** The price of the line including discounts, excluding tax */
     discountedLinePrice: Scalars['Int'];
     /** The price of the line including discounts and tax */
     discountedLinePriceWithTax: Scalars['Int'];
     /**
-     * The actual line price, taking into account both item discounts _and_ prorated (proportially-distributed)
+     * The actual line price, taking into account both item discounts _and_ prorated (proportionally-distributed)
      * Order-level discounts. This value is the true economic value of the OrderLine, and is used in tax
      * and refund calculations.
      */

+ 119 - 3
packages/core/e2e/stock-control.e2e-spec.ts

@@ -20,6 +20,8 @@ import {
     GlobalFlag,
     SettlePayment,
     StockMovementType,
+    TransitFulfillment,
+    TransitionFulfillmentToState,
     UpdateGlobalSettings,
     UpdateProductVariantInput,
     UpdateProductVariants,
@@ -69,9 +71,8 @@ describe('Stock control', () => {
         }),
     );
 
-    const orderGuard: ErrorResultGuard<
-        TestOrderFragmentFragment | UpdatedOrderFragment
-    > = createErrorResultGuard(input => !!input.lines);
+    const orderGuard: ErrorResultGuard<TestOrderFragmentFragment | UpdatedOrderFragment> =
+        createErrorResultGuard(input => !!input.lines);
 
     const fulfillmentGuard: ErrorResultGuard<FulfillmentFragment> = createErrorResultGuard(
         input => !!input.state,
@@ -412,6 +413,101 @@ describe('Stock control', () => {
             expect(variant3.stockMovements.items[5].type).toBe(StockMovementType.CANCELLATION);
             expect(variant3.stockMovements.items[6].type).toBe(StockMovementType.CANCELLATION);
         });
+
+        // https://github.com/vendure-ecommerce/vendure/issues/1198
+        it('creates Cancellations & adjusts stock when cancelling a Fulfillment', async () => {
+            async function getTrackedVariant() {
+                const result = await getProductWithStockMovement('T_2');
+                return result?.variants[1]!;
+            }
+
+            const trackedVariant1 = await getTrackedVariant();
+
+            expect(trackedVariant1.stockOnHand).toBe(5);
+
+            // Add items to order and check out
+            await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
+            await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
+                productVariantId: trackedVariant1.id,
+                quantity: 1,
+            });
+            await shopClient.query<SetShippingAddress.Mutation, SetShippingAddress.Variables>(
+                SET_SHIPPING_ADDRESS,
+                {
+                    input: {
+                        streetLine1: '1 Test Street',
+                        countryCode: 'GB',
+                    } as CreateAddressInput,
+                },
+            );
+            await shopClient.query<TransitionToState.Mutation, TransitionToState.Variables>(
+                TRANSITION_TO_STATE,
+                { state: 'ArrangingPayment' as OrderState },
+            );
+            const { addPaymentToOrder: order } = await shopClient.query<
+                AddPaymentToOrder.Mutation,
+                AddPaymentToOrder.Variables
+            >(ADD_PAYMENT, {
+                input: {
+                    method: testSuccessfulPaymentMethod.code,
+                    metadata: {},
+                } as PaymentInput,
+            });
+            orderGuard.assertSuccess(order);
+            expect(order).not.toBeNull();
+
+            const trackedVariant2 = await getTrackedVariant();
+            expect(trackedVariant2.stockOnHand).toBe(5);
+
+            const linesInput =
+                order?.lines
+                    .filter(l => l.productVariant.id === trackedVariant2.id)
+                    .map(l => ({ orderLineId: l.id, quantity: l.quantity })) ?? [];
+
+            const { addFulfillmentToOrder } = await adminClient.query<
+                CreateFulfillment.Mutation,
+                CreateFulfillment.Variables
+            >(CREATE_FULFILLMENT, {
+                input: {
+                    lines: linesInput,
+                    handler: {
+                        code: manualFulfillmentHandler.code,
+                        arguments: [
+                            { name: 'method', value: 'test method' },
+                            { name: 'trackingCode', value: 'ABC123' },
+                        ],
+                    },
+                },
+            });
+
+            const trackedVariant3 = await getTrackedVariant();
+
+            expect(trackedVariant3.stockOnHand).toBe(4);
+
+            const { transitionFulfillmentToState } = await adminClient.query<
+                TransitionFulfillmentToState.Mutation,
+                TransitionFulfillmentToState.Variables
+            >(TRANSITION_FULFILLMENT_TO_STATE, {
+                state: 'Cancelled',
+                id: (addFulfillmentToOrder as any).id,
+            });
+
+            const trackedVariant4 = await getTrackedVariant();
+
+            expect(trackedVariant4.stockOnHand).toBe(5);
+            expect(trackedVariant4.stockMovements.items).toEqual([
+                { id: 'T_4', quantity: 5, type: 'ADJUSTMENT' },
+                { id: 'T_7', quantity: 3, type: 'ALLOCATION' },
+                { id: 'T_9', quantity: 1, type: 'RELEASE' },
+                { id: 'T_11', quantity: -2, type: 'SALE' },
+                { id: 'T_15', quantity: 1, type: 'CANCELLATION' },
+                { id: 'T_16', quantity: 1, type: 'CANCELLATION' },
+                { id: 'T_21', quantity: 1, type: 'ALLOCATION' },
+                { id: 'T_22', quantity: -1, type: 'SALE' },
+                // This is the cancellation we are testing for
+                { id: 'T_23', quantity: 1, type: 'CANCELLATION' },
+            ]);
+        });
     });
 
     describe('saleable stock level', () => {
@@ -1061,3 +1157,23 @@ const UPDATE_STOCK_ON_HAND = gql`
     }
     ${VARIANT_WITH_STOCK_FRAGMENT}
 `;
+
+export const TRANSITION_FULFILLMENT_TO_STATE = gql`
+    mutation TransitionFulfillmentToState($id: ID!, $state: String!) {
+        transitionFulfillmentToState(id: $id, state: $state) {
+            ... on Fulfillment {
+                id
+                state
+                nextStates
+                createdAt
+            }
+            ... on ErrorResult {
+                errorCode
+                message
+            }
+            ... on FulfillmentStateTransitionError {
+                transitionError
+            }
+        }
+    }
+`;

+ 10 - 1
packages/core/src/service/helpers/fulfillment-state-machine/fulfillment-state-machine.ts

@@ -12,6 +12,7 @@ import { ConfigService } from '../../../config/config.service';
 import { Fulfillment } from '../../../entity/fulfillment/fulfillment.entity';
 import { Order } from '../../../entity/order/order.entity';
 import { HistoryService } from '../../services/history.service';
+import { StockMovementService } from '../../services/stock-movement.service';
 
 import {
     FulfillmentState,
@@ -24,7 +25,11 @@ export class FulfillmentStateMachine {
     readonly config: StateMachineConfig<FulfillmentState, FulfillmentTransitionData>;
     private readonly initialState: FulfillmentState = 'Created';
 
-    constructor(private configService: ConfigService, private historyService: HistoryService) {
+    constructor(
+        private configService: ConfigService,
+        private historyService: HistoryService,
+        private stockMovementService: StockMovementService,
+    ) {
         this.config = this.initConfig();
     }
 
@@ -93,6 +98,10 @@ export class FulfillmentStateMachine {
             }),
         );
         await Promise.all(historyEntryPromises);
+        if (toState === 'Cancelled') {
+            const { ctx, orders, fulfillment } = data;
+            await this.stockMovementService.createCancellationsForOrderItems(ctx, fulfillment.orderItems);
+        }
     }
 
     private initConfig(): StateMachineConfig<FulfillmentState, FulfillmentTransitionData> {

+ 4 - 4
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -2966,7 +2966,7 @@ export type OrderItem = Node & {
     /** The price of a single unit including discounts and tax */
     discountedUnitPriceWithTax: Scalars['Int'];
     /**
-     * The actual unit price, taking into account both item discounts _and_ prorated (proportially-distributed)
+     * The actual unit price, taking into account both item discounts _and_ prorated (proportionally-distributed)
      * Order-level discounts. This value is the true economic value of the OrderItem, and is used in tax
      * and refund calculations.
      */
@@ -3014,7 +3014,7 @@ export type OrderLine = Node & {
     /** The price of a single unit including discounts and tax */
     discountedUnitPriceWithTax: Scalars['Int'];
     /**
-     * The actual unit price, taking into account both item discounts _and_ prorated (proportially-distributed)
+     * The actual unit price, taking into account both item discounts _and_ prorated (proportionally-distributed)
      * Order-level discounts. This value is the true economic value of the OrderItem, and is used in tax
      * and refund calculations.
      */
@@ -3026,14 +3026,14 @@ export type OrderLine = Node & {
     taxRate: Scalars['Float'];
     /** The total price of the line excluding tax and discounts. */
     linePrice: Scalars['Int'];
-    /** The total price of the line including tax bit excluding discounts. */
+    /** The total price of the line including tax but excluding discounts. */
     linePriceWithTax: Scalars['Int'];
     /** The price of the line including discounts, excluding tax */
     discountedLinePrice: Scalars['Int'];
     /** The price of the line including discounts and tax */
     discountedLinePriceWithTax: Scalars['Int'];
     /**
-     * The actual line price, taking into account both item discounts _and_ prorated (proportially-distributed)
+     * The actual line price, taking into account both item discounts _and_ prorated (proportionally-distributed)
      * Order-level discounts. This value is the true economic value of the OrderLine, and is used in tax
      * and refund calculations.
      */

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
schema-admin.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
schema-shop.json


Vissa filer visades inte eftersom för många filer har ändrats