Sfoglia il codice sorgente

fix(core): Fix draft orders not getting correctly placed

Fixes #2105
Michael Bromley 2 anni fa
parent
commit
4d01ab53b3

+ 81 - 4
packages/core/e2e/draft-order.e2e-spec.ts

@@ -1,10 +1,18 @@
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
 import { LanguageCode } from '@vendure/common/lib/generated-types';
-import { DefaultLogger, mergeConfig, orderPercentageDiscount } from '@vendure/core';
+import {
+    DefaultLogger,
+    DefaultOrderPlacedStrategy,
+    mergeConfig,
+    Order,
+    orderPercentageDiscount,
+    OrderState,
+    RequestContext,
+} from '@vendure/core';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
 import path from 'path';
-import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
@@ -12,15 +20,36 @@ import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-conf
 import { singleStageRefundablePaymentMethod } from './fixtures/test-payment-methods';
 import { ORDER_WITH_LINES_FRAGMENT } from './graphql/fragments';
 import * as Codegen from './graphql/generated-e2e-admin-types';
-import { CanceledOrderFragment, OrderWithLinesFragment } from './graphql/generated-e2e-admin-types';
+import {
+    AddManualPaymentDocument,
+    AdminTransitionDocument,
+    CanceledOrderFragment,
+    GetOrderDocument,
+    GetOrderPlacedAtDocument,
+    OrderWithLinesFragment,
+} from './graphql/generated-e2e-admin-types';
 import {
     GetActiveCustomerOrdersQuery,
     TestOrderFragmentFragment,
+    TransitionToStateDocument,
     UpdatedOrderFragment,
 } from './graphql/generated-e2e-shop-types';
 import { CREATE_PROMOTION, GET_CUSTOMER_LIST } from './graphql/shared-definitions';
 import { GET_ACTIVE_CUSTOMER_ORDERS } from './graphql/shop-definitions';
 
+class TestOrderPlacedStrategy extends DefaultOrderPlacedStrategy {
+    static spy = vi.fn();
+    shouldSetAsPlaced(
+        ctx: RequestContext,
+        fromState: OrderState,
+        toState: OrderState,
+        order: Order,
+    ): boolean {
+        TestOrderPlacedStrategy.spy(order);
+        return super.shouldSetAsPlaced(ctx, fromState, toState, order);
+    }
+}
+
 describe('Draft Orders resolver', () => {
     const { server, adminClient, shopClient } = createTestEnvironment(
         mergeConfig(testConfig(), {
@@ -28,6 +57,9 @@ describe('Draft Orders resolver', () => {
             paymentOptions: {
                 paymentMethodHandlers: [singleStageRefundablePaymentMethod],
             },
+            orderOptions: {
+                orderPlacedStrategy: new TestOrderPlacedStrategy(),
+            },
             dbConnectionOptions: {
                 logging: true,
             },
@@ -39,7 +71,7 @@ describe('Draft Orders resolver', () => {
 
     const orderGuard: ErrorResultGuard<
         TestOrderFragmentFragment | CanceledOrderFragment | UpdatedOrderFragment
-    > = createErrorResultGuard(input => !!input.lines);
+    > = createErrorResultGuard(input => !!input.lines || !!input.state);
 
     beforeAll(async () => {
         await server.init({
@@ -332,6 +364,39 @@ describe('Draft Orders resolver', () => {
         expect(setDraftOrderShippingMethod.shippingLines.length).toBe(1);
         expect(setDraftOrderShippingMethod.shippingLines[0].shippingMethod.id).toBe('T_2');
     });
+
+    // https://github.com/vendure-ecommerce/vendure/issues/2105
+    it('sets order as placed when payment is settled', async () => {
+        TestOrderPlacedStrategy.spy.mockClear();
+        expect(TestOrderPlacedStrategy.spy.mock.calls.length).toBe(0);
+
+        const { transitionOrderToState } = await adminClient.query(AdminTransitionDocument, {
+            id: draftOrder.id,
+            state: 'ArrangingPayment',
+        });
+
+        orderGuard.assertSuccess(transitionOrderToState);
+        expect(transitionOrderToState.state).toBe('ArrangingPayment');
+
+        const { addManualPaymentToOrder } = await adminClient.query(AddManualPaymentDocument, {
+            input: {
+                orderId: draftOrder.id,
+                metadata: {},
+                method: singleStageRefundablePaymentMethod.code,
+                transactionId: '12345',
+            },
+        });
+
+        orderGuard.assertSuccess(addManualPaymentToOrder);
+        expect(addManualPaymentToOrder.state).toBe('PaymentSettled');
+
+        const { order } = await adminClient.query(GetOrderPlacedAtDocument, {
+            id: draftOrder.id,
+        });
+        expect(order?.orderPlacedAt).not.toBeNull();
+        expect(TestOrderPlacedStrategy.spy.mock.calls.length).toBe(1);
+        expect(TestOrderPlacedStrategy.spy.mock.calls[0][0].code).toBe(draftOrder.code);
+    });
 });
 
 export const CREATE_DRAFT_ORDER = gql`
@@ -470,3 +535,15 @@ export const SET_DRAFT_ORDER_SHIPPING_METHOD = gql`
     }
     ${ORDER_WITH_LINES_FRAGMENT}
 `;
+
+export const GET_ORDER_PLACED_AT = gql`
+    query GetOrderPlacedAt($id: ID!) {
+        order(id: $id) {
+            id
+            createdAt
+            updatedAt
+            state
+            orderPlacedAt
+        }
+    }
+`;

+ 54 - 0
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -7599,6 +7599,14 @@ export type SetDraftOrderShippingMethodMutation = {
         | { errorCode: ErrorCode; message: string };
 };
 
+export type GetOrderPlacedAtQueryVariables = Exact<{
+    id: Scalars['ID'];
+}>;
+
+export type GetOrderPlacedAtQuery = {
+    order?: { id: string; createdAt: any; updatedAt: any; state: string; orderPlacedAt?: any | null } | null;
+};
+
 export type IdTest1QueryVariables = Exact<{ [key: string]: never }>;
 
 export type IdTest1Query = { products: { items: Array<{ id: string }> } };
@@ -21066,6 +21074,52 @@ export const SetDraftOrderShippingMethodDocument = {
     SetDraftOrderShippingMethodMutation,
     SetDraftOrderShippingMethodMutationVariables
 >;
+export const GetOrderPlacedAtDocument = {
+    kind: 'Document',
+    definitions: [
+        {
+            kind: 'OperationDefinition',
+            operation: 'query',
+            name: { kind: 'Name', value: 'GetOrderPlacedAt' },
+            variableDefinitions: [
+                {
+                    kind: 'VariableDefinition',
+                    variable: { kind: 'Variable', name: { kind: 'Name', value: 'id' } },
+                    type: {
+                        kind: 'NonNullType',
+                        type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
+                    },
+                },
+            ],
+            selectionSet: {
+                kind: 'SelectionSet',
+                selections: [
+                    {
+                        kind: 'Field',
+                        name: { kind: 'Name', value: 'order' },
+                        arguments: [
+                            {
+                                kind: 'Argument',
+                                name: { kind: 'Name', value: 'id' },
+                                value: { kind: 'Variable', name: { kind: 'Name', value: 'id' } },
+                            },
+                        ],
+                        selectionSet: {
+                            kind: 'SelectionSet',
+                            selections: [
+                                { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'createdAt' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'updatedAt' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'state' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'orderPlacedAt' } },
+                            ],
+                        },
+                    },
+                ],
+            },
+        },
+    ],
+} as unknown as DocumentNode<GetOrderPlacedAtQuery, GetOrderPlacedAtQueryVariables>;
 export const IdTest1Document = {
     kind: 'Document',
     definitions: [

+ 5 - 0
packages/core/src/config/order/default-order-process.ts

@@ -435,6 +435,11 @@ export function configureDefaultOrderProcess(options: DefaultOrderProcessOptions
             if (toState === 'Cancelled') {
                 order.active = false;
             }
+            if (fromState === 'Draft' && toState === 'ArrangingPayment') {
+                // Once we exit the Draft state, we can consider the order active,
+                // which will allow us to run the OrderPlacedStrategy at the correct point.
+                order.active = true;
+            }
             await historyService.createHistoryEntryForOrder({
                 orderId: order.id,
                 type: HistoryEntryType.ORDER_STATE_TRANSITION,