Преглед на файлове

fix(core): Fix splitting of shippingLines on multivendor orders

Relates to #2859
Michael Bromley преди 1 година
родител
ревизия
9112dd8130

+ 42 - 0
packages/core/e2e/graphql/admin-definitions.ts

@@ -17,3 +17,45 @@ export const SEARCH_PRODUCTS_ADMIN = gql`
         }
     }
 `;
+
+export const GET_ORDER_WITH_SELLER_ORDERS = gql`
+    query GetOrderWithSellerOrders($id: ID!) {
+        order(id: $id) {
+            id
+            code
+            state
+            sellerOrders {
+                id
+                aggregateOrderId
+                lines {
+                    id
+                    productVariant {
+                        id
+                        name
+                    }
+                }
+                shippingLines {
+                    id
+                    shippingMethod {
+                        id
+                        code
+                    }
+                }
+            }
+            lines {
+                id
+                productVariant {
+                    id
+                    name
+                }
+            }
+            shippingLines {
+                id
+                shippingMethod {
+                    id
+                    code
+                }
+            }
+        }
+    }
+`;

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

@@ -7997,6 +7997,26 @@ export type SearchProductsAdminQuery = {
     };
 };
 
+export type GetOrderWithSellerOrdersQueryVariables = Exact<{
+    id: Scalars['ID']['input'];
+}>;
+
+export type GetOrderWithSellerOrdersQuery = {
+    order?: {
+        id: string;
+        code: string;
+        state: string;
+        sellerOrders?: Array<{
+            id: string;
+            aggregateOrderId?: string | null;
+            lines: Array<{ id: string; productVariant: { id: string; name: string } }>;
+            shippingLines: Array<{ id: string; shippingMethod: { id: string; code: string } }>;
+        }> | null;
+        lines: Array<{ id: string; productVariant: { id: string; name: string } }>;
+        shippingLines: Array<{ id: string; shippingMethod: { id: string; code: string } }>;
+    } | null;
+};
+
 export type AdministratorFragment = {
     id: string;
     firstName: string;
@@ -22781,6 +22801,178 @@ export const SearchProductsAdminDocument = {
         },
     ],
 } as unknown as DocumentNode<SearchProductsAdminQuery, SearchProductsAdminQueryVariables>;
+export const GetOrderWithSellerOrdersDocument = {
+    kind: 'Document',
+    definitions: [
+        {
+            kind: 'OperationDefinition',
+            operation: 'query',
+            name: { kind: 'Name', value: 'GetOrderWithSellerOrders' },
+            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: 'code' } },
+                                { kind: 'Field', name: { kind: 'Name', value: 'state' } },
+                                {
+                                    kind: 'Field',
+                                    name: { kind: 'Name', value: 'sellerOrders' },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                            {
+                                                kind: 'Field',
+                                                name: { kind: 'Name', value: 'aggregateOrderId' },
+                                            },
+                                            {
+                                                kind: 'Field',
+                                                name: { kind: 'Name', value: 'lines' },
+                                                selectionSet: {
+                                                    kind: 'SelectionSet',
+                                                    selections: [
+                                                        {
+                                                            kind: 'Field',
+                                                            name: { kind: 'Name', value: 'id' },
+                                                        },
+                                                        {
+                                                            kind: 'Field',
+                                                            name: { kind: 'Name', value: 'productVariant' },
+                                                            selectionSet: {
+                                                                kind: 'SelectionSet',
+                                                                selections: [
+                                                                    {
+                                                                        kind: 'Field',
+                                                                        name: { kind: 'Name', value: 'id' },
+                                                                    },
+                                                                    {
+                                                                        kind: 'Field',
+                                                                        name: { kind: 'Name', value: 'name' },
+                                                                    },
+                                                                ],
+                                                            },
+                                                        },
+                                                    ],
+                                                },
+                                            },
+                                            {
+                                                kind: 'Field',
+                                                name: { kind: 'Name', value: 'shippingLines' },
+                                                selectionSet: {
+                                                    kind: 'SelectionSet',
+                                                    selections: [
+                                                        {
+                                                            kind: 'Field',
+                                                            name: { kind: 'Name', value: 'id' },
+                                                        },
+                                                        {
+                                                            kind: 'Field',
+                                                            name: { kind: 'Name', value: 'shippingMethod' },
+                                                            selectionSet: {
+                                                                kind: 'SelectionSet',
+                                                                selections: [
+                                                                    {
+                                                                        kind: 'Field',
+                                                                        name: { kind: 'Name', value: 'id' },
+                                                                    },
+                                                                    {
+                                                                        kind: 'Field',
+                                                                        name: { kind: 'Name', value: 'code' },
+                                                                    },
+                                                                ],
+                                                            },
+                                                        },
+                                                    ],
+                                                },
+                                            },
+                                        ],
+                                    },
+                                },
+                                {
+                                    kind: 'Field',
+                                    name: { kind: 'Name', value: 'lines' },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                            {
+                                                kind: 'Field',
+                                                name: { kind: 'Name', value: 'productVariant' },
+                                                selectionSet: {
+                                                    kind: 'SelectionSet',
+                                                    selections: [
+                                                        {
+                                                            kind: 'Field',
+                                                            name: { kind: 'Name', value: 'id' },
+                                                        },
+                                                        {
+                                                            kind: 'Field',
+                                                            name: { kind: 'Name', value: 'name' },
+                                                        },
+                                                    ],
+                                                },
+                                            },
+                                        ],
+                                    },
+                                },
+                                {
+                                    kind: 'Field',
+                                    name: { kind: 'Name', value: 'shippingLines' },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            { kind: 'Field', name: { kind: 'Name', value: 'id' } },
+                                            {
+                                                kind: 'Field',
+                                                name: { kind: 'Name', value: 'shippingMethod' },
+                                                selectionSet: {
+                                                    kind: 'SelectionSet',
+                                                    selections: [
+                                                        {
+                                                            kind: 'Field',
+                                                            name: { kind: 'Name', value: 'id' },
+                                                        },
+                                                        {
+                                                            kind: 'Field',
+                                                            name: { kind: 'Name', value: 'code' },
+                                                        },
+                                                    ],
+                                                },
+                                            },
+                                        ],
+                                    },
+                                },
+                            ],
+                        },
+                    },
+                ],
+            },
+        },
+    ],
+} as unknown as DocumentNode<GetOrderWithSellerOrdersQuery, GetOrderWithSellerOrdersQueryVariables>;
 export const CreateAdministratorDocument = {
     kind: 'Document',
     definitions: [

+ 249 - 0
packages/core/e2e/order-multi-vendor.e2e-spec.ts

@@ -0,0 +1,249 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+import { mergeConfig, OrderService } from '@vendure/core';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
+import { multivendorPaymentMethodHandler } from 'dev-server/example-plugins/multivendor-plugin/config/mv-payment-handler';
+import { CONNECTED_PAYMENT_METHOD_CODE } from 'dev-server/example-plugins/multivendor-plugin/constants';
+import { MultivendorPlugin } from 'dev-server/example-plugins/multivendor-plugin/multivendor.plugin';
+import gql from 'graphql-tag';
+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 {
+    AssignProductsToChannelDocument,
+    GetOrderWithSellerOrdersDocument,
+} from './graphql/generated-e2e-admin-types';
+import * as CodegenShop from './graphql/generated-e2e-shop-types';
+import { SetShippingMethodDocument } from './graphql/generated-e2e-shop-types';
+import {
+    ADD_ITEM_TO_ORDER,
+    ADD_PAYMENT,
+    GET_ELIGIBLE_SHIPPING_METHODS,
+    SET_SHIPPING_ADDRESS,
+    TRANSITION_TO_STATE,
+} from './graphql/shop-definitions';
+
+declare module '@vendure/core/dist/entity/custom-entity-fields' {
+    interface CustomShippingMethodFields {
+        minPrice: number;
+        maxPrice: number;
+    }
+}
+
+describe('Multi-vendor orders', () => {
+    const { server, adminClient, shopClient } = createTestEnvironment(
+        mergeConfig(testConfig(), {
+            plugins: [
+                MultivendorPlugin.init({
+                    platformFeePercent: 10,
+                    platformFeeSKU: 'FEE',
+                }),
+            ],
+        }),
+    );
+
+    let bobsPartsChannel: { id: string; token: string; variantIds: string[] };
+    let alicesWaresChannel: { id: string; token: string; variantIds: string[] };
+    let orderId: string;
+
+    type OrderSuccessResult =
+        | CodegenShop.UpdatedOrderFragment
+        | CodegenShop.TestOrderFragmentFragment
+        | CodegenShop.TestOrderWithPaymentsFragment
+        | CodegenShop.ActiveOrderCustomerFragment;
+    const orderResultGuard: ErrorResultGuard<OrderSuccessResult> = createErrorResultGuard(
+        input => !!input.lines,
+    );
+
+    beforeAll(async () => {
+        await server.init({
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 3,
+        });
+        await adminClient.asSuperAdmin();
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    it('setup sellers', async () => {
+        const result1 = await shopClient.query(REGISTER_SELLER, {
+            input: {
+                shopName: "Bob's Parts",
+                seller: {
+                    firstName: 'Bob',
+                    lastName: 'Dobalina',
+                    emailAddress: 'bob@bobs-parts.com',
+                    password: 'test',
+                },
+            },
+        });
+        bobsPartsChannel = result1.registerNewSeller;
+        expect(bobsPartsChannel.token).toBe('bobs-parts-token');
+
+        const result2 = await shopClient.query(REGISTER_SELLER, {
+            input: {
+                shopName: "Alice's Wares",
+                seller: {
+                    firstName: 'Alice',
+                    lastName: 'Smith',
+                    emailAddress: 'alice@alices-wares.com',
+                    password: 'test',
+                },
+            },
+        });
+        alicesWaresChannel = result2.registerNewSeller;
+        expect(alicesWaresChannel.token).toBe('alices-wares-token');
+    });
+
+    it('assign products to sellers', async () => {
+        const { assignProductsToChannel } = await adminClient.query(AssignProductsToChannelDocument, {
+            input: {
+                channelId: bobsPartsChannel.id,
+                productIds: ['T_1'],
+                priceFactor: 1,
+            },
+        });
+
+        expect(assignProductsToChannel[0].channels.map(c => c.code)).toEqual([
+            '__default_channel__',
+            'bobs-parts',
+        ]);
+        bobsPartsChannel.variantIds = assignProductsToChannel[0].variants.map(v => v.id);
+
+        expect(bobsPartsChannel.variantIds).toEqual(['T_1', 'T_2', 'T_3', 'T_4']);
+
+        const { assignProductsToChannel: result2 } = await adminClient.query(
+            AssignProductsToChannelDocument,
+            {
+                input: {
+                    channelId: alicesWaresChannel.id,
+                    productIds: ['T_11'],
+                    priceFactor: 1,
+                },
+            },
+        );
+        expect(result2[0].channels.map(c => c.code)).toEqual(['__default_channel__', 'alices-wares']);
+        alicesWaresChannel.variantIds = result2[0].variants.map(v => v.id);
+
+        expect(alicesWaresChannel.variantIds).toEqual(['T_22']);
+    });
+
+    it('adds items and sets shipping methods', async () => {
+        await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
+        await shopClient.query<
+            CodegenShop.AddItemToOrderMutation,
+            CodegenShop.AddItemToOrderMutationVariables
+        >(ADD_ITEM_TO_ORDER, {
+            productVariantId: bobsPartsChannel.variantIds[0],
+            quantity: 1,
+        });
+        await shopClient.query<
+            CodegenShop.AddItemToOrderMutation,
+            CodegenShop.AddItemToOrderMutationVariables
+        >(ADD_ITEM_TO_ORDER, {
+            productVariantId: alicesWaresChannel.variantIds[0],
+            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.code).sort()).toEqual([
+            'alices-wares-shipping',
+            'bobs-parts-shipping',
+            'express-shipping',
+            'standard-shipping',
+        ]);
+
+        const { setOrderShippingMethod } = await shopClient.query(SetShippingMethodDocument, {
+            id: [
+                eligibleShippingMethods.find(m => m.code === 'bobs-parts-shipping')!.id,
+                eligibleShippingMethods.find(m => m.code === 'alices-wares-shipping')!.id,
+            ],
+        });
+
+        orderResultGuard.assertSuccess(setOrderShippingMethod);
+        expect(setOrderShippingMethod.shippingLines.map(l => l.shippingMethod.code).sort()).toEqual([
+            'alices-wares-shipping',
+            'bobs-parts-shipping',
+        ]);
+    });
+
+    it('completing checkout splits order', async () => {
+        const { transitionOrderToState } = await shopClient.query<
+            CodegenShop.TransitionToStateMutation,
+            CodegenShop.TransitionToStateMutationVariables
+        >(TRANSITION_TO_STATE, { state: 'ArrangingPayment' });
+
+        orderResultGuard.assertSuccess(transitionOrderToState);
+
+        const { addPaymentToOrder } = await shopClient.query<
+            CodegenShop.AddPaymentToOrderMutation,
+            CodegenShop.AddPaymentToOrderMutationVariables
+        >(ADD_PAYMENT, {
+            input: {
+                method: CONNECTED_PAYMENT_METHOD_CODE,
+                metadata: {},
+            },
+        });
+        orderResultGuard.assertSuccess(addPaymentToOrder);
+
+        expect(addPaymentToOrder.state).toBe('PaymentSettled');
+
+        const { order } = await adminClient.query(GetOrderWithSellerOrdersDocument, {
+            id: addPaymentToOrder.id,
+        });
+        orderId = order!.id;
+
+        expect(order?.sellerOrders?.length).toBe(2);
+    });
+
+    it('order lines get split', async () => {
+        const { order } = await adminClient.query(GetOrderWithSellerOrdersDocument, {
+            id: orderId,
+        });
+
+        expect(order?.sellerOrders?.[0].lines.map(l => l.productVariant.name)).toEqual([
+            'Laptop 13 inch 8GB',
+        ]);
+        expect(order?.sellerOrders?.[1].lines.map(l => l.productVariant.name)).toEqual(['Road Bike']);
+    });
+
+    it('shippingLines get split', async () => {
+        const { order } = await adminClient.query(GetOrderWithSellerOrdersDocument, {
+            id: orderId,
+        });
+
+        expect(order?.sellerOrders?.[0]?.shippingLines.length).toBe(1);
+        expect(order?.sellerOrders?.[1]?.shippingLines.length).toBe(1);
+        expect(order?.sellerOrders?.[0]?.shippingLines[0].shippingMethod.code).toBe('bobs-parts-shipping');
+        expect(order?.sellerOrders?.[1]?.shippingLines[0].shippingMethod.code).toBe('alices-wares-shipping');
+    });
+});
+
+export const REGISTER_SELLER = gql`
+    mutation RegisterSeller($input: RegisterSellerInput!) {
+        registerNewSeller(input: $input) {
+            id
+            code
+            token
+        }
+    }
+`;

+ 12 - 22
packages/core/e2e/order-multiple-shipping.e2e-spec.ts

@@ -1,17 +1,17 @@
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
 import { summate } from '@vendure/common/lib/shared-utils';
 import {
+    defaultShippingCalculator,
+    defaultShippingEligibilityChecker,
+    manualFulfillmentHandler,
     mergeConfig,
-    RequestContext,
-    ShippingLineAssignmentStrategy,
-    ShippingLine,
     Order,
     OrderLine,
-    manualFulfillmentHandler,
-    defaultShippingCalculator,
-    defaultShippingEligibilityChecker,
     OrderService,
+    RequestContext,
     RequestContextService,
+    ShippingLine,
+    ShippingLineAssignmentStrategy,
 } from '@vendure/core';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import path from 'path';
@@ -19,20 +19,12 @@ 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 { CreateShippingMethodDocument, LanguageCode } 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,
@@ -57,7 +49,7 @@ class CustomShippingLineAssignmentStrategy implements ShippingLineAssignmentStra
     }
 }
 
-describe('Shop orders', () => {
+describe('Multiple shipping orders', () => {
     const { server, adminClient, shopClient } = createTestEnvironment(
         mergeConfig(testConfig(), {
             customFields: {
@@ -220,9 +212,8 @@ describe('Shop orders', () => {
         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,
-        );
+        const { activeOrder: activeOrder2 } =
+            await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
 
         expect(activeOrder2?.shippingWithTax).toBe(summate(activeOrder2!.shippingLines, 'priceWithTax'));
     });
@@ -241,9 +232,8 @@ describe('Shop orders', () => {
         const order = await getInternalOrder(activeOrder!.id);
         expect(order?.lines.length).toBe(0);
 
-        const { activeOrder: activeOrder2 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
-            GET_ACTIVE_ORDER,
-        );
+        const { activeOrder: activeOrder2 } =
+            await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
 
         expect(activeOrder2?.shippingWithTax).toBe(0);
     });

+ 4 - 3
packages/core/src/service/helpers/order-splitter/order-splitter.ts

@@ -40,11 +40,12 @@ export class OrderSplitter {
             const shippingLines: ShippingLine[] = [];
             for (const shippingLine of partialOrder.shippingLines) {
                 const newShippingLine = await this.duplicateShippingLine(ctx, shippingLine);
-                lines.map((line) => {
-                    if(shippingLine.id === line.shippingLineId) {
+                for (const line of lines) {
+                    if (shippingLine.id === line.shippingLineId) {
                         line.shippingLineId = newShippingLine.id;
+                        await this.connection.getRepository(ctx, OrderLine).save(line);
                     }
-                })
+                }
                 shippingLines.push(newShippingLine);
             }
             const sellerOrder = await this.connection.getRepository(ctx, Order).save(

+ 1 - 0
scripts/codegen/generate-graphql-types.ts

@@ -31,6 +31,7 @@ const specFileToIgnore = [
     'relations-decorator.e2e-spec',
     'active-order-strategy.e2e-spec',
     'error-handler-strategy.e2e-spec',
+    'order-multi-vendor.e2e-spec',
 ];
 const E2E_ADMIN_QUERY_FILES = path.join(
     __dirname,