Browse Source

fix(core): Remove inapplicable shipping methods when updating an order

Relates to #2548, relates to #2540
Michael Bromley 2 năm trước cách đây
mục cha
commit
f04b033c7d

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 16 - 16
packages/core/e2e/graphql/generated-e2e-shop-types.ts


+ 1 - 0
packages/core/e2e/graphql/shop-definitions.ts

@@ -44,6 +44,7 @@ export const TEST_ORDER_FRAGMENT = gql`
             }
         }
         shippingLines {
+            priceWithTax
             shippingMethod {
                 id
                 code

+ 263 - 0
packages/core/e2e/order-multiple-shipping.e2e-spec.ts

@@ -0,0 +1,263 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+import { summate } from '@vendure/common/lib/shared-utils';
+import {
+    mergeConfig,
+    RequestContext,
+    ShippingLineAssignmentStrategy,
+    ShippingLine,
+    Order,
+    OrderLine,
+    manualFulfillmentHandler,
+    defaultShippingCalculator,
+    defaultShippingEligibilityChecker,
+    OrderService,
+    RequestContextService,
+} from '@vendure/core';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
+import path from 'path';
+import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+
+import { initialData } from '../../../e2e-common/e2e-initial-data';
+import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
+import { hydratingShippingEligibilityChecker } from './fixtures/test-shipping-eligibility-checkers';
+import {
+    CreateAddressInput,
+    CreateShippingMethodDocument,
+    LanguageCode,
+} from './graphql/generated-e2e-admin-types';
+
+import * as Codegen from './graphql/generated-e2e-admin-types';
+import * as CodegenShop from './graphql/generated-e2e-shop-types';
+import { GET_COUNTRY_LIST, UPDATE_COUNTRY } from './graphql/shared-definitions';
+import {
+    ADD_ITEM_TO_ORDER,
+    GET_ACTIVE_ORDER,
+    GET_AVAILABLE_COUNTRIES,
+    GET_ELIGIBLE_SHIPPING_METHODS,
+    REMOVE_ITEM_FROM_ORDER,
+    SET_SHIPPING_ADDRESS,
+    SET_SHIPPING_METHOD,
+} from './graphql/shop-definitions';
+
+declare module '@vendure/core/dist/entity/custom-entity-fields' {
+    interface CustomShippingMethodFields {
+        minPrice: number;
+        maxPrice: number;
+    }
+}
+
+class CustomShippingLineAssignmentStrategy implements ShippingLineAssignmentStrategy {
+    assignShippingLineToOrderLines(
+        ctx: RequestContext,
+        shippingLine: ShippingLine,
+        order: Order,
+    ): OrderLine[] | Promise<OrderLine[]> {
+        const { minPrice, maxPrice } = shippingLine.shippingMethod.customFields;
+        return order.lines.filter(l => l.linePriceWithTax >= minPrice && l.linePriceWithTax <= maxPrice);
+    }
+}
+
+describe('Shop orders', () => {
+    const { server, adminClient, shopClient } = createTestEnvironment(
+        mergeConfig(testConfig(), {
+            customFields: {
+                ShippingMethod: [
+                    { name: 'minPrice', type: 'int' },
+                    { name: 'maxPrice', type: 'int' },
+                ],
+            },
+            shippingOptions: {
+                shippingLineAssignmentStrategy: new CustomShippingLineAssignmentStrategy(),
+            },
+        }),
+    );
+
+    type OrderSuccessResult =
+        | CodegenShop.UpdatedOrderFragment
+        | CodegenShop.TestOrderFragmentFragment
+        | CodegenShop.TestOrderWithPaymentsFragment
+        | CodegenShop.ActiveOrderCustomerFragment;
+    const orderResultGuard: ErrorResultGuard<OrderSuccessResult> = createErrorResultGuard(
+        input => !!input.lines,
+    );
+
+    let lessThan100MethodId: string;
+    let greaterThan100MethodId: string;
+    let orderService: OrderService;
+
+    beforeAll(async () => {
+        await server.init({
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 3,
+        });
+        await adminClient.asSuperAdmin();
+        orderService = server.app.get(OrderService);
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    it('setup shipping methods', async () => {
+        const result1 = await adminClient.query(CreateShippingMethodDocument, {
+            input: {
+                code: 'less-than-100',
+                translations: [{ languageCode: LanguageCode.en, name: 'Less than 100', description: '' }],
+                fulfillmentHandler: manualFulfillmentHandler.code,
+                checker: {
+                    code: defaultShippingEligibilityChecker.code,
+                    arguments: [{ name: 'orderMinimum', value: '0' }],
+                },
+                calculator: {
+                    code: defaultShippingCalculator.code,
+                    arguments: [
+                        { name: 'rate', value: '1000' },
+                        { name: 'taxRate', value: '0' },
+                        { name: 'includesTax', value: 'auto' },
+                    ],
+                },
+                customFields: {
+                    minPrice: 0,
+                    maxPrice: 100_00,
+                },
+            },
+        });
+
+        const result2 = await adminClient.query(CreateShippingMethodDocument, {
+            input: {
+                code: 'greater-than-100',
+                translations: [{ languageCode: LanguageCode.en, name: 'Greater than 200', description: '' }],
+                fulfillmentHandler: manualFulfillmentHandler.code,
+                checker: {
+                    code: defaultShippingEligibilityChecker.code,
+                    arguments: [{ name: 'orderMinimum', value: '0' }],
+                },
+                calculator: {
+                    code: defaultShippingCalculator.code,
+                    arguments: [
+                        { name: 'rate', value: '2000' },
+                        { name: 'taxRate', value: '0' },
+                        { name: 'includesTax', value: 'auto' },
+                    ],
+                },
+                customFields: {
+                    minPrice: 100_00,
+                    maxPrice: 500000_00,
+                },
+            },
+        });
+
+        expect(result1.createShippingMethod.id).toBe('T_3');
+        expect(result2.createShippingMethod.id).toBe('T_4');
+        lessThan100MethodId = result1.createShippingMethod.id;
+        greaterThan100MethodId = result2.createShippingMethod.id;
+    });
+
+    it('assigns shipping methods to correct order lines', async () => {
+        await shopClient.query<
+            CodegenShop.AddItemToOrderMutation,
+            CodegenShop.AddItemToOrderMutationVariables
+        >(ADD_ITEM_TO_ORDER, {
+            productVariantId: 'T_1',
+            quantity: 1,
+        });
+        await shopClient.query<
+            CodegenShop.AddItemToOrderMutation,
+            CodegenShop.AddItemToOrderMutationVariables
+        >(ADD_ITEM_TO_ORDER, {
+            productVariantId: 'T_11',
+            quantity: 1,
+        });
+
+        await shopClient.query<
+            CodegenShop.SetShippingAddressMutation,
+            CodegenShop.SetShippingAddressMutationVariables
+        >(SET_SHIPPING_ADDRESS, {
+            input: {
+                streetLine1: '12 the street',
+                postalCode: '123456',
+                countryCode: 'US',
+            },
+        });
+
+        const { eligibleShippingMethods } = await shopClient.query<CodegenShop.GetShippingMethodsQuery>(
+            GET_ELIGIBLE_SHIPPING_METHODS,
+        );
+
+        expect(eligibleShippingMethods.map(m => m.id).includes(lessThan100MethodId)).toBe(true);
+        expect(eligibleShippingMethods.map(m => m.id).includes(greaterThan100MethodId)).toBe(true);
+
+        const { setOrderShippingMethod } = await shopClient.query<
+            CodegenShop.SetShippingMethodMutation,
+            CodegenShop.SetShippingMethodMutationVariables
+        >(SET_SHIPPING_METHOD, {
+            id: [lessThan100MethodId, greaterThan100MethodId],
+        });
+
+        orderResultGuard.assertSuccess(setOrderShippingMethod);
+
+        const order = await getInternalOrder(setOrderShippingMethod.id);
+        expect(order?.lines[0].shippingLine?.shippingMethod.code).toBe('greater-than-100');
+        expect(order?.lines[1].shippingLine?.shippingMethod.code).toBe('less-than-100');
+
+        expect(order?.shippingLines.length).toBe(2);
+    });
+
+    it('removes shipping methods that are no longer applicable', async () => {
+        const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
+
+        const { removeOrderLine } = await shopClient.query<
+            CodegenShop.RemoveItemFromOrderMutation,
+            CodegenShop.RemoveItemFromOrderMutationVariables
+        >(REMOVE_ITEM_FROM_ORDER, {
+            orderLineId: activeOrder!.lines[0].id,
+        });
+        orderResultGuard.assertSuccess(removeOrderLine);
+
+        const order = await getInternalOrder(activeOrder!.id);
+        expect(order?.lines.length).toBe(1);
+        expect(order?.shippingLines.length).toBe(1);
+        expect(order?.shippingLines[0].shippingMethod.code).toBe('less-than-100');
+
+        const { activeOrder: activeOrder2 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
+            GET_ACTIVE_ORDER,
+        );
+
+        expect(activeOrder2?.shippingWithTax).toBe(summate(activeOrder2!.shippingLines, 'priceWithTax'));
+    });
+
+    it('removes remaining shipping method when removing all items', async () => {
+        const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
+
+        const { removeOrderLine } = await shopClient.query<
+            CodegenShop.RemoveItemFromOrderMutation,
+            CodegenShop.RemoveItemFromOrderMutationVariables
+        >(REMOVE_ITEM_FROM_ORDER, {
+            orderLineId: activeOrder!.lines[0].id,
+        });
+        orderResultGuard.assertSuccess(removeOrderLine);
+
+        const order = await getInternalOrder(activeOrder!.id);
+        expect(order?.lines.length).toBe(0);
+
+        const { activeOrder: activeOrder2 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
+            GET_ACTIVE_ORDER,
+        );
+
+        expect(activeOrder2?.shippingWithTax).toBe(0);
+    });
+
+    async function getInternalOrder(externalOrderId: string): Promise<Order> {
+        const ctx = await server.app.get(RequestContextService).create({ apiType: 'admin' });
+        const internalOrderId = +externalOrderId.replace('T_', '');
+        const order = await orderService.findOne(ctx, internalOrderId, [
+            'lines',
+            'lines.shippingLine',
+            'lines.shippingLine.shippingMethod',
+            'shippingLines',
+            'shippingLines.shippingMethod',
+        ]);
+        return order!;
+    }
+});

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

@@ -2315,7 +2315,7 @@ describe('Shop orders', () => {
         // https://github.com/vendure-ecommerce/vendure/issues/2548
         it('hydrating Order in the ShippingEligibilityChecker does not break order modification', async () => {
             // First we'll create a ShippingMethod that uses the hydrating checker
-            const { createShippingMethod } = await adminClient.query(CreateShippingMethodDocument, {
+            await adminClient.query(CreateShippingMethodDocument, {
                 input: {
                     code: 'hydrating-checker',
                     translations: [

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

@@ -306,6 +306,13 @@ export class OrderCalculator {
     }
 
     private async applyShipping(ctx: RequestContext, order: Order) {
+        // First we need to remove any ShippingLines which are no longer applicable
+        // to the Order, i.e. there is no OrderLine which is assigned to the ShippingLine's
+        // ShippingMethod.
+        const orderLineShippingLineIds = order.lines.map(line => line.shippingLineId);
+        order.shippingLines = order.shippingLines.filter(shippingLine =>
+            orderLineShippingLineIds.includes(shippingLine.id),
+        );
         for (const shippingLine of order.shippingLines) {
             const currentShippingMethod =
                 shippingLine?.shippingMethodId &&

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác