Parcourir la source

feat(core): Implement `setOrderCustomer` mutation

Relates to #2505
Michael Bromley il y a 2 ans
Parent
commit
26e77d749c

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

@@ -1937,6 +1937,7 @@ export enum HistoryEntryType {
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
   ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
   ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
+  ORDER_CUSTOMER_UPDATED = 'ORDER_CUSTOMER_UPDATED',
   ORDER_FULFILLMENT = 'ORDER_FULFILLMENT',
   ORDER_FULFILLMENT = 'ORDER_FULFILLMENT',
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_MODIFIED = 'ORDER_MODIFIED',
   ORDER_MODIFIED = 'ORDER_MODIFIED',
@@ -2849,6 +2850,8 @@ export type Mutation = {
   setDraftOrderShippingMethod: SetOrderShippingMethodResult;
   setDraftOrderShippingMethod: SetOrderShippingMethodResult;
   setMainNavExpanded: Scalars['Boolean']['output'];
   setMainNavExpanded: Scalars['Boolean']['output'];
   setOrderCustomFields?: Maybe<Order>;
   setOrderCustomFields?: Maybe<Order>;
+  /** Allows a different Customer to be assigned to an Order. Added in v2.2.0. */
+  setOrderCustomer?: Maybe<Order>;
   setUiLanguage: LanguageCode;
   setUiLanguage: LanguageCode;
   setUiLocale?: Maybe<Scalars['String']['output']>;
   setUiLocale?: Maybe<Scalars['String']['output']>;
   setUiTheme: Scalars['String']['output'];
   setUiTheme: Scalars['String']['output'];
@@ -3576,6 +3579,11 @@ export type MutationSetOrderCustomFieldsArgs = {
 };
 };
 
 
 
 
+export type MutationSetOrderCustomerArgs = {
+  input: SetOrderCustomerInput;
+};
+
+
 export type MutationSetUiLanguageArgs = {
 export type MutationSetUiLanguageArgs = {
   languageCode: LanguageCode;
   languageCode: LanguageCode;
 };
 };
@@ -5709,6 +5717,12 @@ export type ServerConfig = {
 
 
 export type SetCustomerForDraftOrderResult = EmailAddressConflictError | Order;
 export type SetCustomerForDraftOrderResult = EmailAddressConflictError | Order;
 
 
+export type SetOrderCustomerInput = {
+  customerId: Scalars['ID']['input'];
+  note?: InputMaybe<Scalars['String']['input']>;
+  orderId: Scalars['ID']['input'];
+};
+
 export type SetOrderShippingMethodResult = IneligibleShippingMethodError | NoActiveOrderError | Order | OrderModificationError;
 export type SetOrderShippingMethodResult = IneligibleShippingMethodError | NoActiveOrderError | Order | OrderModificationError;
 
 
 /** Returned if the Payment settlement fails */
 /** Returned if the Payment settlement fails */

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

@@ -1866,6 +1866,7 @@ export enum HistoryEntryType {
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
   ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
   ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
+  ORDER_CUSTOMER_UPDATED = 'ORDER_CUSTOMER_UPDATED',
   ORDER_FULFILLMENT = 'ORDER_FULFILLMENT',
   ORDER_FULFILLMENT = 'ORDER_FULFILLMENT',
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_MODIFIED = 'ORDER_MODIFIED',
   ORDER_MODIFIED = 'ORDER_MODIFIED',
@@ -2747,6 +2748,8 @@ export type Mutation = {
   /** Sets the shipping method by id, which can be obtained with the `eligibleShippingMethodsForDraftOrder` query */
   /** Sets the shipping method by id, which can be obtained with the `eligibleShippingMethodsForDraftOrder` query */
   setDraftOrderShippingMethod: SetOrderShippingMethodResult;
   setDraftOrderShippingMethod: SetOrderShippingMethodResult;
   setOrderCustomFields?: Maybe<Order>;
   setOrderCustomFields?: Maybe<Order>;
+  /** Allows a different Customer to be assigned to an Order. Added in v2.2.0. */
+  setOrderCustomer?: Maybe<Order>;
   settlePayment: SettlePaymentResult;
   settlePayment: SettlePaymentResult;
   settleRefund: SettleRefundResult;
   settleRefund: SettleRefundResult;
   transitionFulfillmentToState: TransitionFulfillmentToStateResult;
   transitionFulfillmentToState: TransitionFulfillmentToStateResult;
@@ -3445,6 +3448,11 @@ export type MutationSetOrderCustomFieldsArgs = {
 };
 };
 
 
 
 
+export type MutationSetOrderCustomerArgs = {
+  input: SetOrderCustomerInput;
+};
+
+
 export type MutationSettlePaymentArgs = {
 export type MutationSettlePaymentArgs = {
   id: Scalars['ID']['input'];
   id: Scalars['ID']['input'];
 };
 };
@@ -5483,6 +5491,12 @@ export type ServerConfig = {
 
 
 export type SetCustomerForDraftOrderResult = EmailAddressConflictError | Order;
 export type SetCustomerForDraftOrderResult = EmailAddressConflictError | Order;
 
 
+export type SetOrderCustomerInput = {
+  customerId: Scalars['ID']['input'];
+  note?: InputMaybe<Scalars['String']['input']>;
+  orderId: Scalars['ID']['input'];
+};
+
 export type SetOrderShippingMethodResult = IneligibleShippingMethodError | NoActiveOrderError | Order | OrderModificationError;
 export type SetOrderShippingMethodResult = IneligibleShippingMethodError | NoActiveOrderError | Order | OrderModificationError;
 
 
 /** Returned if the Payment settlement fails */
 /** Returned if the Payment settlement fails */

+ 1 - 0
packages/common/src/generated-shop-types.ts

@@ -1181,6 +1181,7 @@ export enum HistoryEntryType {
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
   ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
   ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
+  ORDER_CUSTOMER_UPDATED = 'ORDER_CUSTOMER_UPDATED',
   ORDER_FULFILLMENT = 'ORDER_FULFILLMENT',
   ORDER_FULFILLMENT = 'ORDER_FULFILLMENT',
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_MODIFIED = 'ORDER_MODIFIED',
   ORDER_MODIFIED = 'ORDER_MODIFIED',

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

@@ -1925,6 +1925,7 @@ export enum HistoryEntryType {
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
   ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
   ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
+  ORDER_CUSTOMER_UPDATED = 'ORDER_CUSTOMER_UPDATED',
   ORDER_FULFILLMENT = 'ORDER_FULFILLMENT',
   ORDER_FULFILLMENT = 'ORDER_FULFILLMENT',
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_MODIFIED = 'ORDER_MODIFIED',
   ORDER_MODIFIED = 'ORDER_MODIFIED',
@@ -2829,6 +2830,8 @@ export type Mutation = {
   /** Sets the shipping method by id, which can be obtained with the `eligibleShippingMethodsForDraftOrder` query */
   /** Sets the shipping method by id, which can be obtained with the `eligibleShippingMethodsForDraftOrder` query */
   setDraftOrderShippingMethod: SetOrderShippingMethodResult;
   setDraftOrderShippingMethod: SetOrderShippingMethodResult;
   setOrderCustomFields?: Maybe<Order>;
   setOrderCustomFields?: Maybe<Order>;
+  /** Allows a different Customer to be assigned to an Order. Added in v2.2.0. */
+  setOrderCustomer?: Maybe<Order>;
   settlePayment: SettlePaymentResult;
   settlePayment: SettlePaymentResult;
   settleRefund: SettleRefundResult;
   settleRefund: SettleRefundResult;
   transitionFulfillmentToState: TransitionFulfillmentToStateResult;
   transitionFulfillmentToState: TransitionFulfillmentToStateResult;
@@ -3527,6 +3530,11 @@ export type MutationSetOrderCustomFieldsArgs = {
 };
 };
 
 
 
 
+export type MutationSetOrderCustomerArgs = {
+  input: SetOrderCustomerInput;
+};
+
+
 export type MutationSettlePaymentArgs = {
 export type MutationSettlePaymentArgs = {
   id: Scalars['ID']['input'];
   id: Scalars['ID']['input'];
 };
 };
@@ -5631,6 +5639,12 @@ export type ServerConfig = {
 
 
 export type SetCustomerForDraftOrderResult = EmailAddressConflictError | Order;
 export type SetCustomerForDraftOrderResult = EmailAddressConflictError | Order;
 
 
+export type SetOrderCustomerInput = {
+  customerId: Scalars['ID']['input'];
+  note?: InputMaybe<Scalars['String']['input']>;
+  orderId: Scalars['ID']['input'];
+};
+
 export type SetOrderShippingMethodResult = IneligibleShippingMethodError | NoActiveOrderError | Order | OrderModificationError;
 export type SetOrderShippingMethodResult = IneligibleShippingMethodError | NoActiveOrderError | Order | OrderModificationError;
 
 
 /** Returned if the Payment settlement fails */
 /** Returned if the Payment settlement fails */

Fichier diff supprimé car celui-ci est trop grand
+ 21 - 0
packages/core/e2e/graphql/generated-e2e-admin-types.ts


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

@@ -1137,6 +1137,7 @@ export enum HistoryEntryType {
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
   ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
   ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
+  ORDER_CUSTOMER_UPDATED = 'ORDER_CUSTOMER_UPDATED',
   ORDER_FULFILLMENT = 'ORDER_FULFILLMENT',
   ORDER_FULFILLMENT = 'ORDER_FULFILLMENT',
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_MODIFIED = 'ORDER_MODIFIED',
   ORDER_MODIFIED = 'ORDER_MODIFIED',

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

@@ -2379,6 +2379,100 @@ describe('Orders resolver', () => {
         });
         });
     });
     });
 
 
+    // https://github.com/vendure-ecommerce/vendure/issues/2505
+    describe('updating order customer', () => {
+        let orderId: string;
+        let customerId: string;
+
+        it('set up order', async () => {
+            const result = await createTestOrder(
+                adminClient,
+                shopClient,
+                customers[1].emailAddress,
+                password,
+            );
+            orderId = result.orderId;
+            customerId = customers[1].id;
+
+            await proceedToArrangingPayment(shopClient);
+            const order = await addPaymentToOrder(shopClient, singleStageRefundablePaymentMethod);
+            orderGuard.assertSuccess(order);
+            expect(order.customer?.id).toBe(customerId);
+        });
+
+        it(
+            'throws in invalid orderId',
+            assertThrowsWithMessage(async () => {
+                await adminClient.query<
+                    Codegen.SetOrderCustomerMutation,
+                    Codegen.SetOrderCustomerMutationVariables
+                >(SET_ORDER_CUSTOMER, {
+                    input: {
+                        orderId: 'T_9999',
+                        customerId: customers[2].id,
+                        note: 'Testing',
+                    },
+                });
+            }, 'No Order with the id "9999" could be found'),
+        );
+
+        it(
+            'throws in invalid orderId',
+            assertThrowsWithMessage(async () => {
+                await adminClient.query<
+                    Codegen.SetOrderCustomerMutation,
+                    Codegen.SetOrderCustomerMutationVariables
+                >(SET_ORDER_CUSTOMER, {
+                    input: {
+                        orderId,
+                        customerId: 'T_999',
+                        note: 'Testing',
+                    },
+                });
+            }, 'No Customer with the id "999" could be found'),
+        );
+
+        it('update order customer', async () => {
+            const newCustomerId = customers[2].id;
+            const { setOrderCustomer } = await adminClient.query<
+                Codegen.SetOrderCustomerMutation,
+                Codegen.SetOrderCustomerMutationVariables
+            >(SET_ORDER_CUSTOMER, {
+                input: {
+                    orderId,
+                    customerId: customers[2].id,
+                    note: 'Testing',
+                },
+            });
+
+            expect(setOrderCustomer?.customer?.id).toBe(newCustomerId);
+        });
+
+        it('adds a history entry for the customer update', async () => {
+            const { order } = await adminClient.query<
+                Codegen.GetOrderHistoryQuery,
+                Codegen.GetOrderHistoryQueryVariables
+            >(GET_ORDER_HISTORY, {
+                id: orderId,
+                options: {
+                    skip: 4,
+                },
+            });
+            expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
+                {
+                    data: {
+                        previousCustomerId: customerId,
+                        previousCustomerName: 'Trevor Donnelly',
+                        newCustomerId: customers[2].id,
+                        newCustomerName: `${customers[2].firstName} ${customers[2].lastName}`,
+                        note: 'Testing',
+                    },
+                    type: HistoryEntryType.ORDER_CUSTOMER_UPDATED,
+                },
+            ]);
+        });
+    });
+
     describe('issues', () => {
     describe('issues', () => {
         // https://github.com/vendure-ecommerce/vendure/issues/639
         // https://github.com/vendure-ecommerce/vendure/issues/639
         it('returns fulfillments for Order with no lines', async () => {
         it('returns fulfillments for Order with no lines', async () => {
@@ -2927,3 +3021,14 @@ const CANCEL_PAYMENT = gql`
     }
     }
     ${PAYMENT_FRAGMENT}
     ${PAYMENT_FRAGMENT}
 `;
 `;
+
+const SET_ORDER_CUSTOMER = gql`
+    mutation SetOrderCustomer($input: SetOrderCustomerInput!) {
+        setOrderCustomer(input: $input) {
+            id
+            customer {
+                id
+            }
+        }
+    }
+`;

+ 2 - 0
packages/core/src/api/middleware/id-codec-plugin.ts

@@ -50,6 +50,8 @@ export class IdCodecPlugin implements ApolloServerPlugin {
                     'refundId',
                     'refundId',
                     'groupId',
                     'groupId',
                     'modificationId',
                     'modificationId',
+                    'previousCustomerId',
+                    'newCustomerId',
                 ]);
                 ]);
             }
             }
             return isIdType ? this.idCodecService.encode(value) : value;
             return isIdType ? this.idCodecService.encode(value) : value;

+ 8 - 0
packages/core/src/api/resolvers/admin/order.resolver.ts

@@ -11,6 +11,7 @@ import {
     MutationDeleteOrderNoteArgs,
     MutationDeleteOrderNoteArgs,
     MutationModifyOrderArgs,
     MutationModifyOrderArgs,
     MutationRefundOrderArgs,
     MutationRefundOrderArgs,
+    MutationSetOrderCustomerArgs,
     MutationSetOrderCustomFieldsArgs,
     MutationSetOrderCustomFieldsArgs,
     MutationSettlePaymentArgs,
     MutationSettlePaymentArgs,
     MutationSettleRefundArgs,
     MutationSettleRefundArgs,
@@ -152,6 +153,13 @@ export class OrderResolver {
         return this.orderService.updateCustomFields(ctx, args.input.id, args.input.customFields);
         return this.orderService.updateCustomFields(ctx, args.input.id, args.input.customFields);
     }
     }
 
 
+    @Transaction()
+    @Mutation()
+    @Allow(Permission.UpdateOrder)
+    async setOrderCustomer(@Ctx() ctx: RequestContext, @Args() { input }: MutationSetOrderCustomerArgs) {
+        return this.orderService.updateOrderCustomer(ctx, input);
+    }
+
     @Transaction()
     @Transaction()
     @Mutation()
     @Mutation()
     @Allow(Permission.UpdateOrder)
     @Allow(Permission.UpdateOrder)

+ 10 - 0
packages/core/src/api/schema/admin-api/order.api.graphql

@@ -20,6 +20,10 @@ type Mutation {
     transitionPaymentToState(id: ID!, state: String!): TransitionPaymentToStateResult!
     transitionPaymentToState(id: ID!, state: String!): TransitionPaymentToStateResult!
     setOrderCustomFields(input: UpdateOrderInput!): Order
     setOrderCustomFields(input: UpdateOrderInput!): Order
     """
     """
+    Allows a different Customer to be assigned to an Order. Added in v2.2.0.
+    """
+    setOrderCustomer(input: SetOrderCustomerInput!): Order
+    """
     Allows an Order to be modified after it has been completed by the Customer. The Order must first
     Allows an Order to be modified after it has been completed by the Customer. The Order must first
     be in the `Modifying` state.
     be in the `Modifying` state.
     """
     """
@@ -80,6 +84,12 @@ input OrderSortParameter {
 # generated by generateListOptions function
 # generated by generateListOptions function
 input OrderListOptions
 input OrderListOptions
 
 
+input SetOrderCustomerInput {
+    orderId: ID!
+    customerId: ID!
+    note: String
+}
+
 # Populated with any custom fields at run-time
 # Populated with any custom fields at run-time
 input UpdateOrderInput {
 input UpdateOrderInput {
     id: ID!
     id: ID!

+ 1 - 0
packages/core/src/api/schema/common/history-entry.type.graphql

@@ -31,6 +31,7 @@ enum HistoryEntryType {
     ORDER_COUPON_APPLIED
     ORDER_COUPON_APPLIED
     ORDER_COUPON_REMOVED
     ORDER_COUPON_REMOVED
     ORDER_MODIFIED
     ORDER_MODIFIED
+    ORDER_CUSTOMER_UPDATED
 }
 }
 
 
 type HistoryEntryList implements PaginatedList {
 type HistoryEntryList implements PaginatedList {

+ 1 - 0
packages/core/src/i18n/messages/en.json

@@ -50,6 +50,7 @@
     "promotion-channels-can-only-be-changed-from-default-channel": "Promotions channels may only be changed from the Default Channel",
     "promotion-channels-can-only-be-changed-from-default-channel": "Promotions channels may only be changed from the Default Channel",
     "stockonhand-cannot-be-negative": "stockOnHand cannot be a negative value",
     "stockonhand-cannot-be-negative": "stockOnHand cannot be a negative value",
     "superadmin-must-have-superadmin-role": "Cannot remove the SuperAdmin role from the sole SuperAdmin",
     "superadmin-must-have-superadmin-role": "Cannot remove the SuperAdmin role from the sole SuperAdmin",
+    "target-customer-not-assigned-to-order-channels": "The target Customer is not assigned to the same Channels as the Order. Missing channels IDs: { missingChannelIds }",
     "unauthorized": "The credentials did not match. Please check and try again"
     "unauthorized": "The credentials did not match. Please check and try again"
   },
   },
   "errorResult": {
   "errorResult": {

+ 7 - 0
packages/core/src/service/services/history.service.ts

@@ -109,6 +109,13 @@ export interface OrderHistoryEntryData {
     [HistoryEntryType.ORDER_MODIFIED]: {
     [HistoryEntryType.ORDER_MODIFIED]: {
         modificationId: ID;
         modificationId: ID;
     };
     };
+    [HistoryEntryType.ORDER_CUSTOMER_UPDATED]: {
+        previousCustomerId?: ID;
+        previousCustomerName?: ID;
+        newCustomerId: ID;
+        newCustomerName: ID;
+        note?: string;
+    };
 }
 }
 
 
 export interface CreateCustomerHistoryEntryArgs<T extends keyof CustomerHistoryEntryData> {
 export interface CreateCustomerHistoryEntryArgs<T extends keyof CustomerHistoryEntryData> {

+ 62 - 7
packages/core/src/service/services/order.service.ts

@@ -30,6 +30,7 @@ import {
     OrderType,
     OrderType,
     RefundOrderInput,
     RefundOrderInput,
     RefundOrderResult,
     RefundOrderResult,
+    SetOrderCustomerInput,
     SettlePaymentResult,
     SettlePaymentResult,
     SettleRefundInput,
     SettleRefundInput,
     ShippingMethodQuote,
     ShippingMethodQuote,
@@ -80,10 +81,10 @@ import { Channel } from '../../entity/channel/channel.entity';
 import { Customer } from '../../entity/customer/customer.entity';
 import { Customer } from '../../entity/customer/customer.entity';
 import { Fulfillment } from '../../entity/fulfillment/fulfillment.entity';
 import { Fulfillment } from '../../entity/fulfillment/fulfillment.entity';
 import { HistoryEntry } from '../../entity/history-entry/history-entry.entity';
 import { HistoryEntry } from '../../entity/history-entry/history-entry.entity';
-import { Order } from '../../entity/order/order.entity';
-import { OrderLine } from '../../entity/order-line/order-line.entity';
 import { FulfillmentLine } from '../../entity/order-line-reference/fulfillment-line.entity';
 import { FulfillmentLine } from '../../entity/order-line-reference/fulfillment-line.entity';
+import { OrderLine } from '../../entity/order-line/order-line.entity';
 import { OrderModification } from '../../entity/order-modification/order-modification.entity';
 import { OrderModification } from '../../entity/order-modification/order-modification.entity';
+import { Order } from '../../entity/order/order.entity';
 import { Payment } from '../../entity/payment/payment.entity';
 import { Payment } from '../../entity/payment/payment.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { Promotion } from '../../entity/promotion/promotion.entity';
 import { Promotion } from '../../entity/promotion/promotion.entity';
@@ -483,6 +484,53 @@ export class OrderService {
         return updatedOrder;
         return updatedOrder;
     }
     }
 
 
+    /**
+     * @description
+     * Updates the Customer which is assigned to a given Order. The target Customer must be assigned to the same
+     * Channels as the Order, otherwise an error will be thrown.
+     *
+     * @since 2.2.0
+     */
+    async updateOrderCustomer(ctx: RequestContext, { customerId, orderId, note }: SetOrderCustomerInput) {
+        const order = await this.getOrderOrThrow(ctx, orderId);
+        const currentCustomer = order.customer;
+        if (currentCustomer?.id === customerId) {
+            // No change in customer, so just return the order as-is
+            return order;
+        }
+        const targetCustomer = await this.customerService.findOne(ctx, customerId, ['channels']);
+        if (!targetCustomer) {
+            throw new EntityNotFoundError('Customer', customerId);
+        }
+
+        // ensure the customer is assigned to the same channels as the order
+        const channelIds = order.channels.map(c => c.id);
+        const customerChannelIds = targetCustomer.channels.map(c => c.id);
+        const missingChannelIds = channelIds.filter(id => !customerChannelIds.includes(id));
+        if (missingChannelIds.length) {
+            throw new UserInputError(`error.target-customer-not-assigned-to-order-channels`, {
+                channelIds: missingChannelIds.join(', '),
+            });
+        }
+
+        const updatedOrder = await this.addCustomerToOrder(ctx, order.id, targetCustomer);
+        this.eventBus.publish(new OrderEvent(ctx, updatedOrder, 'updated'));
+        await this.historyService.createHistoryEntryForOrder({
+            ctx,
+            orderId,
+            type: HistoryEntryType.ORDER_CUSTOMER_UPDATED,
+            data: {
+                previousCustomerId: currentCustomer?.id,
+                previousCustomerName:
+                    currentCustomer && `${currentCustomer.firstName} ${currentCustomer.lastName}`,
+                newCustomerId: targetCustomer.id,
+                newCustomerName: `${targetCustomer.firstName} ${targetCustomer.lastName}`,
+                note,
+            },
+        });
+        return updatedOrder;
+    }
+
     /**
     /**
      * @description
      * @description
      * Adds an item to the Order, either creating a new OrderLine or
      * Adds an item to the Order, either creating a new OrderLine or
@@ -1443,14 +1491,21 @@ export class OrderService {
      * @description
      * @description
      * Associates a Customer with the Order.
      * Associates a Customer with the Order.
      */
      */
-    async addCustomerToOrder(ctx: RequestContext, orderId: ID, customer: Customer): Promise<Order> {
-        const order = await this.getOrderOrThrow(ctx, orderId);
+    async addCustomerToOrder(
+        ctx: RequestContext,
+        orderIdOrOrder: ID | Order,
+        customer: Customer,
+    ): Promise<Order> {
+        const order =
+            orderIdOrOrder instanceof Order
+                ? orderIdOrOrder
+                : await this.getOrderOrThrow(ctx, orderIdOrOrder);
         order.customer = customer;
         order.customer = customer;
         await this.connection.getRepository(ctx, Order).save(order, { reload: false });
         await this.connection.getRepository(ctx, Order).save(order, { reload: false });
+        let updatedOrder = order;
         // Check that any applied couponCodes are still valid now that
         // Check that any applied couponCodes are still valid now that
         // we know the Customer.
         // we know the Customer.
-        let updatedOrder = order;
-        if (order.couponCodes) {
+        if (order.active && order.couponCodes) {
             for (const couponCode of order.couponCodes.slice()) {
             for (const couponCode of order.couponCodes.slice()) {
                 const validationResult = await this.promotionService.validateCouponCode(
                 const validationResult = await this.promotionService.validateCouponCode(
                     ctx,
                     ctx,
@@ -1458,7 +1513,7 @@ export class OrderService {
                     customer.id,
                     customer.id,
                 );
                 );
                 if (isGraphQlErrorResult(validationResult)) {
                 if (isGraphQlErrorResult(validationResult)) {
-                    updatedOrder = await this.removeCouponCode(ctx, orderId, couponCode);
+                    updatedOrder = await this.removeCouponCode(ctx, order.id, couponCode);
                 }
                 }
             }
             }
         }
         }

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

@@ -1866,6 +1866,7 @@ export enum HistoryEntryType {
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
   ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
   ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
+  ORDER_CUSTOMER_UPDATED = 'ORDER_CUSTOMER_UPDATED',
   ORDER_FULFILLMENT = 'ORDER_FULFILLMENT',
   ORDER_FULFILLMENT = 'ORDER_FULFILLMENT',
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_MODIFIED = 'ORDER_MODIFIED',
   ORDER_MODIFIED = 'ORDER_MODIFIED',
@@ -2747,6 +2748,8 @@ export type Mutation = {
   /** Sets the shipping method by id, which can be obtained with the `eligibleShippingMethodsForDraftOrder` query */
   /** Sets the shipping method by id, which can be obtained with the `eligibleShippingMethodsForDraftOrder` query */
   setDraftOrderShippingMethod: SetOrderShippingMethodResult;
   setDraftOrderShippingMethod: SetOrderShippingMethodResult;
   setOrderCustomFields?: Maybe<Order>;
   setOrderCustomFields?: Maybe<Order>;
+  /** Allows a different Customer to be assigned to an Order. Added in v2.2.0. */
+  setOrderCustomer?: Maybe<Order>;
   settlePayment: SettlePaymentResult;
   settlePayment: SettlePaymentResult;
   settleRefund: SettleRefundResult;
   settleRefund: SettleRefundResult;
   transitionFulfillmentToState: TransitionFulfillmentToStateResult;
   transitionFulfillmentToState: TransitionFulfillmentToStateResult;
@@ -3445,6 +3448,11 @@ export type MutationSetOrderCustomFieldsArgs = {
 };
 };
 
 
 
 
+export type MutationSetOrderCustomerArgs = {
+  input: SetOrderCustomerInput;
+};
+
+
 export type MutationSettlePaymentArgs = {
 export type MutationSettlePaymentArgs = {
   id: Scalars['ID']['input'];
   id: Scalars['ID']['input'];
 };
 };
@@ -5483,6 +5491,12 @@ export type ServerConfig = {
 
 
 export type SetCustomerForDraftOrderResult = EmailAddressConflictError | Order;
 export type SetCustomerForDraftOrderResult = EmailAddressConflictError | Order;
 
 
+export type SetOrderCustomerInput = {
+  customerId: Scalars['ID']['input'];
+  note?: InputMaybe<Scalars['String']['input']>;
+  orderId: Scalars['ID']['input'];
+};
+
 export type SetOrderShippingMethodResult = IneligibleShippingMethodError | NoActiveOrderError | Order | OrderModificationError;
 export type SetOrderShippingMethodResult = IneligibleShippingMethodError | NoActiveOrderError | Order | OrderModificationError;
 
 
 /** Returned if the Payment settlement fails */
 /** Returned if the Payment settlement fails */

+ 14 - 0
packages/payments-plugin/e2e/graphql/generated-admin-types.ts

@@ -1866,6 +1866,7 @@ export enum HistoryEntryType {
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
   ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
   ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
+  ORDER_CUSTOMER_UPDATED = 'ORDER_CUSTOMER_UPDATED',
   ORDER_FULFILLMENT = 'ORDER_FULFILLMENT',
   ORDER_FULFILLMENT = 'ORDER_FULFILLMENT',
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_MODIFIED = 'ORDER_MODIFIED',
   ORDER_MODIFIED = 'ORDER_MODIFIED',
@@ -2747,6 +2748,8 @@ export type Mutation = {
   /** Sets the shipping method by id, which can be obtained with the `eligibleShippingMethodsForDraftOrder` query */
   /** Sets the shipping method by id, which can be obtained with the `eligibleShippingMethodsForDraftOrder` query */
   setDraftOrderShippingMethod: SetOrderShippingMethodResult;
   setDraftOrderShippingMethod: SetOrderShippingMethodResult;
   setOrderCustomFields?: Maybe<Order>;
   setOrderCustomFields?: Maybe<Order>;
+  /** Allows a different Customer to be assigned to an Order. Added in v2.2.0. */
+  setOrderCustomer?: Maybe<Order>;
   settlePayment: SettlePaymentResult;
   settlePayment: SettlePaymentResult;
   settleRefund: SettleRefundResult;
   settleRefund: SettleRefundResult;
   transitionFulfillmentToState: TransitionFulfillmentToStateResult;
   transitionFulfillmentToState: TransitionFulfillmentToStateResult;
@@ -3445,6 +3448,11 @@ export type MutationSetOrderCustomFieldsArgs = {
 };
 };
 
 
 
 
+export type MutationSetOrderCustomerArgs = {
+  input: SetOrderCustomerInput;
+};
+
+
 export type MutationSettlePaymentArgs = {
 export type MutationSettlePaymentArgs = {
   id: Scalars['ID']['input'];
   id: Scalars['ID']['input'];
 };
 };
@@ -5483,6 +5491,12 @@ export type ServerConfig = {
 
 
 export type SetCustomerForDraftOrderResult = EmailAddressConflictError | Order;
 export type SetCustomerForDraftOrderResult = EmailAddressConflictError | Order;
 
 
+export type SetOrderCustomerInput = {
+  customerId: Scalars['ID']['input'];
+  note?: InputMaybe<Scalars['String']['input']>;
+  orderId: Scalars['ID']['input'];
+};
+
 export type SetOrderShippingMethodResult = IneligibleShippingMethodError | NoActiveOrderError | Order | OrderModificationError;
 export type SetOrderShippingMethodResult = IneligibleShippingMethodError | NoActiveOrderError | Order | OrderModificationError;
 
 
 /** Returned if the Payment settlement fails */
 /** Returned if the Payment settlement fails */

+ 1 - 0
packages/payments-plugin/e2e/graphql/generated-shop-types.ts

@@ -1137,6 +1137,7 @@ export enum HistoryEntryType {
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
   ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
   ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
+  ORDER_CUSTOMER_UPDATED = 'ORDER_CUSTOMER_UPDATED',
   ORDER_FULFILLMENT = 'ORDER_FULFILLMENT',
   ORDER_FULFILLMENT = 'ORDER_FULFILLMENT',
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_MODIFIED = 'ORDER_MODIFIED',
   ORDER_MODIFIED = 'ORDER_MODIFIED',

+ 1 - 0
packages/payments-plugin/src/mollie/graphql/generated-shop-types.ts

@@ -1185,6 +1185,7 @@ export enum HistoryEntryType {
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_CANCELLATION = 'ORDER_CANCELLATION',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
   ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
   ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
+  ORDER_CUSTOMER_UPDATED = 'ORDER_CUSTOMER_UPDATED',
   ORDER_FULFILLMENT = 'ORDER_FULFILLMENT',
   ORDER_FULFILLMENT = 'ORDER_FULFILLMENT',
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_MODIFIED = 'ORDER_MODIFIED',
   ORDER_MODIFIED = 'ORDER_MODIFIED',

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
schema-admin.json


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
schema-shop.json


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

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

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff