Browse Source

feat(core): Add `addItemsToOrder` mutation to add multiple items to cart (#3500)

Martijn 7 months ago
parent
commit
84a8cbe8f5

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

@@ -6669,6 +6669,9 @@ export type UpdateOrderInput = {
   id: Scalars['ID']['input'];
 };
 
+/** Union type of all possible errors that can occur when adding or removing items from an Order. */
+export type UpdateOrderItemErrorResult = InsufficientStockError | NegativeQuantityError | OrderInterceptorError | OrderLimitError | OrderModificationError;
+
 export type UpdateOrderItemsResult = InsufficientStockError | NegativeQuantityError | Order | OrderInterceptorError | OrderLimitError | OrderModificationError;
 
 export type UpdateOrderNoteInput = {

+ 7 - 0
packages/admin-ui/src/lib/core/src/common/introspection-result.ts

@@ -334,6 +334,13 @@
       "ChannelDefaultLanguageError",
       "GlobalSettings"
     ],
+    "UpdateOrderItemErrorResult": [
+      "InsufficientStockError",
+      "NegativeQuantityError",
+      "OrderInterceptorError",
+      "OrderLimitError",
+      "OrderModificationError"
+    ],
     "UpdateOrderItemsResult": [
       "InsufficientStockError",
       "NegativeQuantityError",

File diff suppressed because it is too large
+ 437 - 458
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts


File diff suppressed because it is too large
+ 658 - 674
packages/common/src/generated-shop-types.ts


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

@@ -6581,6 +6581,9 @@ export type UpdateOrderInput = {
   id: Scalars['ID']['input'];
 };
 
+/** Union type of all possible errors that can occur when adding or removing items from an Order. */
+export type UpdateOrderItemErrorResult = InsufficientStockError | NegativeQuantityError | OrderInterceptorError | OrderLimitError | OrderModificationError;
+
 export type UpdateOrderItemsResult = InsufficientStockError | NegativeQuantityError | Order | OrderInterceptorError | OrderLimitError | OrderModificationError;
 
 export type UpdateOrderNoteInput = {

File diff suppressed because it is too large
+ 437 - 458
packages/core/e2e/graphql/generated-e2e-admin-types.ts


File diff suppressed because it is too large
+ 629 - 645
packages/core/e2e/graphql/generated-e2e-shop-types.ts


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

@@ -129,6 +129,25 @@ export const ADD_ITEM_TO_ORDER = gql`
     }
     ${UPDATED_ORDER_FRAGMENT}
 `;
+
+export const ADD_MULTIPLE_ITEMS_TO_ORDER = gql`
+    mutation AddItemsToOrder($inputs: [AddItemInput!]!) {
+        addItemsToOrder(inputs: $inputs) {
+            order {
+                ...UpdatedOrder
+            }
+            errorResults {
+                ...on ErrorResult {
+                    errorCode
+                    message
+                }
+            }
+            
+        }
+    }
+    ${UPDATED_ORDER_FRAGMENT}
+`;
+
 export const SEARCH_PRODUCTS_SHOP = gql`
     query SearchProductsShop($input: SearchInput!) {
         search(input: $input) {

+ 54 - 0
packages/core/e2e/order.e2e-spec.ts

@@ -76,6 +76,7 @@ import {
 } from './graphql/shared-definitions';
 import {
     ADD_ITEM_TO_ORDER,
+    ADD_MULTIPLE_ITEMS_TO_ORDER,
     ADD_PAYMENT,
     APPLY_COUPON_CODE,
     GET_ACTIVE_CUSTOMER_WITH_ORDERS_PRODUCT_PRICE,
@@ -2805,6 +2806,59 @@ describe('Orders resolver', () => {
             expect(order2?.state).toBe('PartiallyShipped');
         });
     });
+
+    describe('multiple items to order', () => {
+        it('adds multiple items to a new active order', async () => {
+            await shopClient.asAnonymousUser();
+            const { addItemsToOrder } = await shopClient.query<
+                CodegenShop.AddItemsToOrderMutation,
+                CodegenShop.AddItemsToOrderMutationVariables
+            >(ADD_MULTIPLE_ITEMS_TO_ORDER, {
+                inputs: [
+                    {
+                        productVariantId: 'T_1',
+                        quantity: 5,
+                    },
+                    {
+                        productVariantId: 'T_2',
+                        quantity: 3,
+                    },
+                ],
+            });
+            expect(addItemsToOrder.order.lines.length).toBe(2);
+            expect(addItemsToOrder.order.lines[0].quantity).toBe(5);
+            expect(addItemsToOrder.order.lines[1].quantity).toBe(3);
+        });
+
+        it('adds successful items and returns error results for failed items', async () => {
+            await shopClient.asAnonymousUser();
+            const { addItemsToOrder } = await shopClient.query<
+                CodegenShop.AddItemsToOrderMutation,
+                CodegenShop.AddItemsToOrderMutationVariables
+            >(ADD_MULTIPLE_ITEMS_TO_ORDER, {
+                inputs: [
+                    {
+                        productVariantId: 'T_1',
+                        quantity: 1,
+                    },
+                    {
+                        productVariantId: 'T_2',
+                        quantity: 999999, // Exceeds limit
+                    }
+                ],
+            });
+            const t1 = addItemsToOrder.order.lines.find(l => l.productVariant.id === 'T_1')
+            // Should have added 1 of T_1
+            expect(t1?.quantity).toBe(1);
+            // Should not have added T_2
+            const t2 = addItemsToOrder.order.lines.find(l => l.productVariant.id === 'T_2')
+            expect(t2).toBeUndefined(); 
+            // Should have errors
+            expect(addItemsToOrder.errorResults.length).toBe(1);
+            expect(addItemsToOrder.errorResults[0].errorCode).toBe('ORDER_LIMIT_ERROR')
+            expect(addItemsToOrder.errorResults[0].message).toBe('ORDER_LIMIT_ERROR')
+        });
+    });
 });
 
 async function createTestOrder(

+ 70 - 0
packages/core/e2e/shop-order.e2e-spec.ts

@@ -610,6 +610,7 @@ describe('Shop orders', () => {
                 orderResultGuard.assertSuccess(removeOrderLine);
                 expect(removeOrderLine.lines.length).toBe(1);
             });
+
         });
 
         it('addItemToOrder errors when going beyond orderItemsLimit', async () => {
@@ -1406,6 +1407,44 @@ describe('Shop orders', () => {
             expect(addItemToOrder.lines[1].productVariant.id).toBe(variantWithoutFeaturedAsset?.id);
             expect(addItemToOrder.lines[1].featuredAsset?.id).toBe(product?.featuredAsset?.id);
         });
+
+
+        it('adds multiple items to order with different custom fields', async () => {
+            await shopClient.asAnonymousUser(); // New order
+            const { addItemsToOrder } = await shopClient.query<CodegenShop.AddItemsToOrderMutation>(
+                ADD_MULTIPLE_ITEMS_TO_ORDER_WITH_CUSTOM_FIELDS,
+                {
+                    inputs: [
+                        {
+                            productVariantId: 'T_1',
+                            quantity: 1,
+                            customFields: {
+                                    notes: 'Variant 1 note',
+                            },
+                        },
+                        {
+                            productVariantId: 'T_2',
+                            quantity: 2,
+                            customFields: {
+                                    notes: 'Variant 2 note',
+                            },
+                        },
+                        {
+                            productVariantId: 'T_3',
+                            quantity: 3,
+                            // no custom field
+                        },
+                    ],
+                },
+            );
+            const order = addItemsToOrder.order as any;
+            expect(order.lines.length).toBe(3);
+            expect(order.lines[0].customFields.notes).toBe('Variant 1 note');
+            expect(order.lines[1].quantity).toBe(2);
+            expect(order.lines[1].customFields.notes).toBe('Variant 2 note');
+            expect(order.lines[2].quantity).toBe(3);
+            expect(order.lines[2].customFields.notes).toBeNull();
+        });
     });
 
     describe('ordering as authenticated user', () => {
@@ -2822,6 +2861,37 @@ export const ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS = gql`
     ${UPDATED_ORDER_FRAGMENT}
 `;
 
+export const ADD_MULTIPLE_ITEMS_TO_ORDER_WITH_CUSTOM_FIELDS = gql`
+    mutation AddMultipleItemsToOrderWithCustomFields(
+        $inputs: [AddItemInput!]!
+    ) {
+        addItemsToOrder(
+            inputs: $inputs
+        ) {
+            order {
+                ...UpdatedOrder
+                lines {
+                    id
+                    quantity
+                    productVariant {
+                        id
+                    }
+                    customFields {
+                        notes
+                    }
+                }   
+            }
+            errorResults {
+                ...on ErrorResult {
+                    errorCode
+                    message
+                }
+            }
+        }
+    }
+    ${UPDATED_ORDER_FRAGMENT}
+`;
+
 const ADJUST_ORDER_LINE_WITH_CUSTOM_FIELDS = gql`
     mutation ($orderLineId: ID!, $quantity: Int!, $customFields: OrderLineCustomFieldsInput) {
         adjustOrderLine(orderLineId: $orderLineId, quantity: $quantity, customFields: $customFields) {

+ 25 - 4
packages/core/src/api/resolvers/shop/shop-order.resolver.ts

@@ -3,6 +3,7 @@ import {
     ActiveOrderResult,
     AddPaymentToOrderResult,
     ApplyCouponCodeResult,
+    MutationAddItemsToOrderArgs,
     MutationAddItemToOrderArgs,
     MutationAddPaymentToOrderArgs,
     MutationAdjustOrderLineArgs,
@@ -23,6 +24,7 @@ import {
     SetOrderShippingMethodResult,
     ShippingMethodQuote,
     TransitionOrderToStateResult,
+    UpdateMultipleOrderItemsResult,
     UpdateOrderItemsResult,
 } from '@vendure/common/lib/generated-shop-types';
 import { QueryCountriesArgs } from '@vendure/common/lib/generated-types';
@@ -30,10 +32,7 @@ import { unique } from '@vendure/common/lib/unique';
 
 import { ErrorResultUnion, isGraphQlErrorResult } from '../../../common/error/error-result';
 import { ForbiddenError } from '../../../common/error/errors';
-import {
-    AlreadyLoggedInError,
-    NoActiveOrderError,
-} from '../../../common/error/generated-graphql-shop-errors';
+import { NoActiveOrderError } from '../../../common/error/generated-graphql-shop-errors';
 import { Translated } from '../../../common/types/locale-types';
 import { idsAreEqual } from '../../../common/utils';
 import { ACTIVE_ORDER_INPUT_FIELD_NAME, ConfigService, LogLevel } from '../../../config';
@@ -353,6 +352,28 @@ export class ShopOrderResolver {
         );
     }
 
+    @Transaction()
+    @Mutation()
+    @Allow(Permission.UpdateOrder, Permission.Owner)
+    async addItemsToOrder(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MutationAddItemsToOrderArgs & ActiveOrderArgs,
+    ): Promise<{ 
+        order: Order; 
+        errorResults: UpdateMultipleOrderItemsResult['errorResults'] 
+    }> {
+        const order = await this.activeOrderService.getActiveOrder(
+            ctx,
+            args[ACTIVE_ORDER_INPUT_FIELD_NAME],
+            true,
+        );
+        const result = await this.orderService.addItemsToOrder(ctx, order.id, args.inputs);
+        return {
+            order: result.order,
+            errorResults: result.errorResults,
+        };
+    }
+
     @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder, Permission.Owner)

+ 10 - 0
packages/core/src/api/schema/common/common-error-results.graphql

@@ -105,3 +105,13 @@ type OrderInterceptorError implements ErrorResult {
     message: String!
     interceptorError: String!
 }
+
+"""
+Union type of all possible errors that can occur when adding or removing items from an Order.
+"""
+union UpdateOrderItemErrorResult =
+      OrderModificationError
+    | OrderLimitError
+    | NegativeQuantityError
+    | InsufficientStockError
+    | OrderInterceptorError

+ 1 - 0
packages/core/src/api/schema/common/common-types.graphql

@@ -275,6 +275,7 @@ union UpdateOrderItemsResult =
     | NegativeQuantityError
     | InsufficientStockError
     | OrderInterceptorError
+
 union RemoveOrderItemsResult = Order | OrderModificationError | OrderInterceptorError
 union SetOrderShippingMethodResult =
       Order

+ 16 - 0
packages/core/src/api/schema/shop-api/shop.api.graphql

@@ -70,6 +70,8 @@ type PublicShippingMethod {
 type Mutation {
     "Adds an item to the Order. If custom fields are defined on the OrderLine entity, a third argument 'customFields' will be available."
     addItemToOrder(productVariantId: ID!, quantity: Int!): UpdateOrderItemsResult!
+    "Adds mutliple items to the Order. Returns a list of errors for each item that failed to add. It will still add successful items."
+    addItemsToOrder(inputs: [AddItemInput!]!): UpdateMultipleOrderItemsResult!
     "Remove an OrderLine from the Order"
     removeOrderLine(orderLineId: ID!): RemoveOrderItemsResult!
     "Remove all OrderLine from the Order"
@@ -285,3 +287,17 @@ union NativeAuthenticationResult =
     | NativeAuthStrategyError
 union AuthenticationResult = CurrentUser | InvalidCredentialsError | NotVerifiedError
 union ActiveOrderResult = Order | NoActiveOrderError
+
+"""
+Returned when multiple items are added to an Order.
+The errorResults array contains the errors that occurred for each item, if any.
+"""
+type UpdateMultipleOrderItemsResult  {
+    order: Order!
+    errorResults: [UpdateOrderItemErrorResult!]!
+}
+
+input AddItemInput {
+    productVariantId: ID!
+    quantity: Int!
+}

File diff suppressed because it is too large
+ 437 - 458
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts


File diff suppressed because it is too large
+ 383 - 403
packages/payments-plugin/e2e/graphql/generated-admin-types.ts


File diff suppressed because it is too large
+ 633 - 649
packages/payments-plugin/e2e/graphql/generated-shop-types.ts


File diff suppressed because it is too large
+ 662 - 678
packages/payments-plugin/src/mollie/graphql/generated-shop-types.ts


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


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


+ 2 - 1
scripts/codegen/download-introspection-schema.ts

@@ -1,7 +1,7 @@
 /* eslint-disable no-console */
 import { INestApplication } from '@nestjs/common';
 import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
-import { bootstrap, VendureConfig } from '@vendure/core';
+import { bootstrap, DefaultLogger, LogLevel, VendureConfig } from '@vendure/core';
 import fs from 'fs';
 import { getIntrospectionQuery } from 'graphql';
 import http from 'http';
@@ -28,6 +28,7 @@ export const config: VendureConfig = {
         paymentMethodHandlers: [],
     },
     plugins: [AdminUiPlugin],
+    logger: new DefaultLogger({ level: LogLevel.Verbose }),
 };
 
 let appPromise: Promise<INestApplication>;

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