Pārlūkot izejas kodu

feat(core): Implement GuestCheckoutStrategy

Fixes #911, Fixes #762
Michael Bromley 2 gadi atpakaļ
vecāks
revīzija
7e0f1d15b3
27 mainītis faili ar 1043 papildinājumiem un 559 dzēšanām
  1. 12 3
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  2. 1 0
      packages/admin-ui/src/lib/core/src/common/introspection-result.ts
  3. 8 2
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  4. 10 3
      packages/common/src/generated-shop-types.ts
  5. 9 2
      packages/common/src/generated-types.ts
  6. 508 526
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  7. 10 2
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  8. 3 0
      packages/core/e2e/graphql/shop-definitions.ts
  9. 257 0
      packages/core/e2e/guest-checkout-strategy.e2e-spec.ts
  10. 1 1
      packages/core/e2e/order-taxes.e2e-spec.ts
  11. 5 7
      packages/core/src/api/resolvers/shop/shop-order.resolver.ts
  12. 7 0
      packages/core/src/api/schema/common/common-error-results.graphql
  13. 1 1
      packages/core/src/api/schema/shop-api/shop.api.graphql
  14. 14 1
      packages/core/src/common/error/generated-graphql-admin-errors.ts
  15. 14 1
      packages/core/src/common/error/generated-graphql-shop-errors.ts
  16. 2 0
      packages/core/src/config/config.module.ts
  17. 2 0
      packages/core/src/config/default-config.ts
  18. 2 0
      packages/core/src/config/index.ts
  19. 95 0
      packages/core/src/config/order/default-guest-checkout-strategy.ts
  20. 38 0
      packages/core/src/config/order/guest-checkout-strategy.ts
  21. 9 0
      packages/core/src/config/vendure-config.ts
  22. 8 2
      packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts
  23. 8 2
      packages/payments-plugin/e2e/graphql/generated-admin-types.ts
  24. 9 3
      packages/payments-plugin/e2e/graphql/generated-shop-types.ts
  25. 10 3
      packages/payments-plugin/src/mollie/graphql/generated-shop-types.ts
  26. 0 0
      schema-admin.json
  27. 0 0
      schema-shop.json

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

@@ -1477,6 +1477,7 @@ export enum ErrorCode {
   EMPTY_ORDER_LINE_SELECTION_ERROR = 'EMPTY_ORDER_LINE_SELECTION_ERROR',
   FACET_IN_USE_ERROR = 'FACET_IN_USE_ERROR',
   FULFILLMENT_STATE_TRANSITION_ERROR = 'FULFILLMENT_STATE_TRANSITION_ERROR',
+  GUEST_CHECKOUT_ERROR = 'GUEST_CHECKOUT_ERROR',
   INELIGIBLE_SHIPPING_METHOD_ERROR = 'INELIGIBLE_SHIPPING_METHOD_ERROR',
   INSUFFICIENT_STOCK_ERROR = 'INSUFFICIENT_STOCK_ERROR',
   INSUFFICIENT_STOCK_ON_HAND_ERROR = 'INSUFFICIENT_STOCK_ON_HAND_ERROR',
@@ -1751,6 +1752,14 @@ export type GlobalSettings = {
   updatedAt: Scalars['DateTime'];
 };
 
+/** Returned when attempting to set the Customer on a guest checkout when the configured GuestCheckoutStrategy does not allow it. */
+export type GuestCheckoutError = ErrorResult & {
+  __typename?: 'GuestCheckoutError';
+  errorCode: ErrorCode;
+  errorDetail: Scalars['String'];
+  message: Scalars['String'];
+};
+
 export type HistoryEntry = Node & {
   __typename?: 'HistoryEntry';
   administrator?: Maybe<Administrator>;
@@ -5105,8 +5114,6 @@ export type SearchInput = {
   collectionId?: InputMaybe<Scalars['ID']>;
   collectionSlug?: InputMaybe<Scalars['String']>;
   facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
-  facetValueIds?: InputMaybe<Array<Scalars['ID']>>;
-  facetValueOperator?: InputMaybe<LogicalOperator>;
   groupByProduct?: InputMaybe<Scalars['Boolean']>;
   skip?: InputMaybe<Scalars['Int']>;
   sort?: InputMaybe<SearchResultSortParameter>;
@@ -7599,6 +7606,8 @@ type ErrorResult_FacetInUseError_Fragment = { __typename?: 'FacetInUseError', er
 
 type ErrorResult_FulfillmentStateTransitionError_Fragment = { __typename?: 'FulfillmentStateTransitionError', errorCode: ErrorCode, message: string };
 
+type ErrorResult_GuestCheckoutError_Fragment = { __typename?: 'GuestCheckoutError', errorCode: ErrorCode, message: string };
+
 type ErrorResult_IneligibleShippingMethodError_Fragment = { __typename?: 'IneligibleShippingMethodError', errorCode: ErrorCode, message: string };
 
 type ErrorResult_InsufficientStockError_Fragment = { __typename?: 'InsufficientStockError', errorCode: ErrorCode, message: string };
@@ -7657,7 +7666,7 @@ type ErrorResult_RefundStateTransitionError_Fragment = { __typename?: 'RefundSta
 
 type ErrorResult_SettlePaymentError_Fragment = { __typename?: 'SettlePaymentError', errorCode: ErrorCode, message: string };
 
-export type ErrorResultFragment = ErrorResult_AlreadyRefundedError_Fragment | ErrorResult_CancelActiveOrderError_Fragment | ErrorResult_CancelPaymentError_Fragment | ErrorResult_ChannelDefaultLanguageError_Fragment | ErrorResult_CouponCodeExpiredError_Fragment | ErrorResult_CouponCodeInvalidError_Fragment | ErrorResult_CouponCodeLimitError_Fragment | ErrorResult_CreateFulfillmentError_Fragment | ErrorResult_EmailAddressConflictError_Fragment | ErrorResult_EmptyOrderLineSelectionError_Fragment | ErrorResult_FacetInUseError_Fragment | ErrorResult_FulfillmentStateTransitionError_Fragment | ErrorResult_IneligibleShippingMethodError_Fragment | ErrorResult_InsufficientStockError_Fragment | ErrorResult_InsufficientStockOnHandError_Fragment | ErrorResult_InvalidCredentialsError_Fragment | ErrorResult_InvalidFulfillmentHandlerError_Fragment | ErrorResult_ItemsAlreadyFulfilledError_Fragment | ErrorResult_LanguageNotAvailableError_Fragment | ErrorResult_ManualPaymentStateError_Fragment | ErrorResult_MimeTypeError_Fragment | ErrorResult_MissingConditionsError_Fragment | ErrorResult_MultipleOrderError_Fragment | ErrorResult_NativeAuthStrategyError_Fragment | ErrorResult_NegativeQuantityError_Fragment | ErrorResult_NoActiveOrderError_Fragment | ErrorResult_NoChangesSpecifiedError_Fragment | ErrorResult_NothingToRefundError_Fragment | ErrorResult_OrderLimitError_Fragment | ErrorResult_OrderModificationError_Fragment | ErrorResult_OrderModificationStateError_Fragment | ErrorResult_OrderStateTransitionError_Fragment | ErrorResult_PaymentMethodMissingError_Fragment | ErrorResult_PaymentOrderMismatchError_Fragment | ErrorResult_PaymentStateTransitionError_Fragment | ErrorResult_ProductOptionInUseError_Fragment | ErrorResult_QuantityTooGreatError_Fragment | ErrorResult_RefundOrderStateError_Fragment | ErrorResult_RefundPaymentIdMissingError_Fragment | ErrorResult_RefundStateTransitionError_Fragment | ErrorResult_SettlePaymentError_Fragment;
+export type ErrorResultFragment = ErrorResult_AlreadyRefundedError_Fragment | ErrorResult_CancelActiveOrderError_Fragment | ErrorResult_CancelPaymentError_Fragment | ErrorResult_ChannelDefaultLanguageError_Fragment | ErrorResult_CouponCodeExpiredError_Fragment | ErrorResult_CouponCodeInvalidError_Fragment | ErrorResult_CouponCodeLimitError_Fragment | ErrorResult_CreateFulfillmentError_Fragment | ErrorResult_EmailAddressConflictError_Fragment | ErrorResult_EmptyOrderLineSelectionError_Fragment | ErrorResult_FacetInUseError_Fragment | ErrorResult_FulfillmentStateTransitionError_Fragment | ErrorResult_GuestCheckoutError_Fragment | ErrorResult_IneligibleShippingMethodError_Fragment | ErrorResult_InsufficientStockError_Fragment | ErrorResult_InsufficientStockOnHandError_Fragment | ErrorResult_InvalidCredentialsError_Fragment | ErrorResult_InvalidFulfillmentHandlerError_Fragment | ErrorResult_ItemsAlreadyFulfilledError_Fragment | ErrorResult_LanguageNotAvailableError_Fragment | ErrorResult_ManualPaymentStateError_Fragment | ErrorResult_MimeTypeError_Fragment | ErrorResult_MissingConditionsError_Fragment | ErrorResult_MultipleOrderError_Fragment | ErrorResult_NativeAuthStrategyError_Fragment | ErrorResult_NegativeQuantityError_Fragment | ErrorResult_NoActiveOrderError_Fragment | ErrorResult_NoChangesSpecifiedError_Fragment | ErrorResult_NothingToRefundError_Fragment | ErrorResult_OrderLimitError_Fragment | ErrorResult_OrderModificationError_Fragment | ErrorResult_OrderModificationStateError_Fragment | ErrorResult_OrderStateTransitionError_Fragment | ErrorResult_PaymentMethodMissingError_Fragment | ErrorResult_PaymentOrderMismatchError_Fragment | ErrorResult_PaymentStateTransitionError_Fragment | ErrorResult_ProductOptionInUseError_Fragment | ErrorResult_QuantityTooGreatError_Fragment | ErrorResult_RefundOrderStateError_Fragment | ErrorResult_RefundPaymentIdMissingError_Fragment | ErrorResult_RefundStateTransitionError_Fragment | ErrorResult_SettlePaymentError_Fragment;
 
 export type ShippingMethodFragment = { __typename?: 'ShippingMethod', id: string, createdAt: any, updatedAt: any, code: string, name: string, description: string, fulfillmentHandlerCode: string, checker: { __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }, calculator: { __typename?: 'ConfigurableOperation', code: string, args: Array<{ __typename?: 'ConfigArg', name: string, value: string }> }, translations: Array<{ __typename?: 'ShippingMethodTranslation', id: string, languageCode: LanguageCode, name: string, description: string }> };
 

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

@@ -94,6 +94,7 @@
       "EmptyOrderLineSelectionError",
       "FacetInUseError",
       "FulfillmentStateTransitionError",
+      "GuestCheckoutError",
       "IneligibleShippingMethodError",
       "InsufficientStockError",
       "InsufficientStockOnHandError",

+ 8 - 2
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -1422,6 +1422,7 @@ export enum ErrorCode {
   EMPTY_ORDER_LINE_SELECTION_ERROR = 'EMPTY_ORDER_LINE_SELECTION_ERROR',
   FACET_IN_USE_ERROR = 'FACET_IN_USE_ERROR',
   FULFILLMENT_STATE_TRANSITION_ERROR = 'FULFILLMENT_STATE_TRANSITION_ERROR',
+  GUEST_CHECKOUT_ERROR = 'GUEST_CHECKOUT_ERROR',
   INELIGIBLE_SHIPPING_METHOD_ERROR = 'INELIGIBLE_SHIPPING_METHOD_ERROR',
   INSUFFICIENT_STOCK_ERROR = 'INSUFFICIENT_STOCK_ERROR',
   INSUFFICIENT_STOCK_ON_HAND_ERROR = 'INSUFFICIENT_STOCK_ON_HAND_ERROR',
@@ -1683,6 +1684,13 @@ export type GlobalSettings = {
   updatedAt: Scalars['DateTime'];
 };
 
+/** Returned when attempting to set the Customer on a guest checkout when the configured GuestCheckoutStrategy does not allow it. */
+export type GuestCheckoutError = ErrorResult & {
+  errorCode: ErrorCode;
+  errorDetail: Scalars['String'];
+  message: Scalars['String'];
+};
+
 export type HistoryEntry = Node & {
   administrator?: Maybe<Administrator>;
   createdAt: Scalars['DateTime'];
@@ -4899,8 +4907,6 @@ export type SearchInput = {
   collectionId?: InputMaybe<Scalars['ID']>;
   collectionSlug?: InputMaybe<Scalars['String']>;
   facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
-  facetValueIds?: InputMaybe<Array<Scalars['ID']>>;
-  facetValueOperator?: InputMaybe<LogicalOperator>;
   groupByProduct?: InputMaybe<Scalars['Boolean']>;
   skip?: InputMaybe<Scalars['Int']>;
   sort?: InputMaybe<SearchResultSortParameter>;

+ 10 - 3
packages/common/src/generated-shop-types.ts

@@ -870,6 +870,7 @@ export enum ErrorCode {
   COUPON_CODE_INVALID_ERROR = 'COUPON_CODE_INVALID_ERROR',
   COUPON_CODE_LIMIT_ERROR = 'COUPON_CODE_LIMIT_ERROR',
   EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR',
+  GUEST_CHECKOUT_ERROR = 'GUEST_CHECKOUT_ERROR',
   IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR = 'IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR',
   IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR = 'IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR',
   INELIGIBLE_PAYMENT_METHOD_ERROR = 'INELIGIBLE_PAYMENT_METHOD_ERROR',
@@ -1049,6 +1050,14 @@ export enum GlobalFlag {
   TRUE = 'TRUE'
 }
 
+/** Returned when attempting to set the Customer on a guest checkout when the configured GuestCheckoutStrategy does not allow it. */
+export type GuestCheckoutError = ErrorResult & {
+  __typename?: 'GuestCheckoutError';
+  errorCode: ErrorCode;
+  errorDetail: Scalars['String'];
+  message: Scalars['String'];
+};
+
 export type HistoryEntry = Node & {
   __typename?: 'HistoryEntry';
   createdAt: Scalars['DateTime'];
@@ -2907,8 +2916,6 @@ export type SearchInput = {
   collectionId?: InputMaybe<Scalars['ID']>;
   collectionSlug?: InputMaybe<Scalars['String']>;
   facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
-  facetValueIds?: InputMaybe<Array<Scalars['ID']>>;
-  facetValueOperator?: InputMaybe<LogicalOperator>;
   groupByProduct?: InputMaybe<Scalars['Boolean']>;
   skip?: InputMaybe<Scalars['Int']>;
   sort?: InputMaybe<SearchResultSortParameter>;
@@ -2975,7 +2982,7 @@ export type Seller = Node & {
   updatedAt: Scalars['DateTime'];
 };
 
-export type SetCustomerForOrderResult = AlreadyLoggedInError | EmailAddressConflictError | NoActiveOrderError | Order;
+export type SetCustomerForOrderResult = AlreadyLoggedInError | EmailAddressConflictError | GuestCheckoutError | NoActiveOrderError | Order;
 
 export type SetOrderShippingMethodResult = IneligibleShippingMethodError | NoActiveOrderError | Order | OrderModificationError;
 

+ 9 - 2
packages/common/src/generated-types.ts

@@ -1466,6 +1466,7 @@ export enum ErrorCode {
   EMPTY_ORDER_LINE_SELECTION_ERROR = 'EMPTY_ORDER_LINE_SELECTION_ERROR',
   FACET_IN_USE_ERROR = 'FACET_IN_USE_ERROR',
   FULFILLMENT_STATE_TRANSITION_ERROR = 'FULFILLMENT_STATE_TRANSITION_ERROR',
+  GUEST_CHECKOUT_ERROR = 'GUEST_CHECKOUT_ERROR',
   INELIGIBLE_SHIPPING_METHOD_ERROR = 'INELIGIBLE_SHIPPING_METHOD_ERROR',
   INSUFFICIENT_STOCK_ERROR = 'INSUFFICIENT_STOCK_ERROR',
   INSUFFICIENT_STOCK_ON_HAND_ERROR = 'INSUFFICIENT_STOCK_ON_HAND_ERROR',
@@ -1740,6 +1741,14 @@ export type GlobalSettings = {
   updatedAt: Scalars['DateTime'];
 };
 
+/** Returned when attempting to set the Customer on a guest checkout when the configured GuestCheckoutStrategy does not allow it. */
+export type GuestCheckoutError = ErrorResult & {
+  __typename?: 'GuestCheckoutError';
+  errorCode: ErrorCode;
+  errorDetail: Scalars['String'];
+  message: Scalars['String'];
+};
+
 export type HistoryEntry = Node & {
   __typename?: 'HistoryEntry';
   administrator?: Maybe<Administrator>;
@@ -5034,8 +5043,6 @@ export type SearchInput = {
   collectionId?: InputMaybe<Scalars['ID']>;
   collectionSlug?: InputMaybe<Scalars['String']>;
   facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
-  facetValueIds?: InputMaybe<Array<Scalars['ID']>>;
-  facetValueOperator?: InputMaybe<LogicalOperator>;
   groupByProduct?: InputMaybe<Scalars['Boolean']>;
   skip?: InputMaybe<Scalars['Int']>;
   sort?: InputMaybe<SearchResultSortParameter>;

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 508 - 526
packages/core/e2e/graphql/generated-e2e-admin-types.ts


+ 10 - 2
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -854,6 +854,7 @@ export enum ErrorCode {
     COUPON_CODE_INVALID_ERROR = 'COUPON_CODE_INVALID_ERROR',
     COUPON_CODE_LIMIT_ERROR = 'COUPON_CODE_LIMIT_ERROR',
     EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR',
+    GUEST_CHECKOUT_ERROR = 'GUEST_CHECKOUT_ERROR',
     IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR = 'IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR',
     IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR = 'IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR',
     INELIGIBLE_PAYMENT_METHOD_ERROR = 'INELIGIBLE_PAYMENT_METHOD_ERROR',
@@ -1024,6 +1025,13 @@ export enum GlobalFlag {
     TRUE = 'TRUE',
 }
 
+/** Returned when attempting to set the Customer on a guest checkout when the configured GuestCheckoutStrategy does not allow it. */
+export type GuestCheckoutError = ErrorResult & {
+    errorCode: ErrorCode;
+    errorDetail: Scalars['String'];
+    message: Scalars['String'];
+};
+
 export type HistoryEntry = Node & {
     createdAt: Scalars['DateTime'];
     data: Scalars['JSON'];
@@ -2805,8 +2813,6 @@ export type SearchInput = {
     collectionId?: InputMaybe<Scalars['ID']>;
     collectionSlug?: InputMaybe<Scalars['String']>;
     facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
-    facetValueIds?: InputMaybe<Array<Scalars['ID']>>;
-    facetValueOperator?: InputMaybe<LogicalOperator>;
     groupByProduct?: InputMaybe<Scalars['Boolean']>;
     skip?: InputMaybe<Scalars['Int']>;
     sort?: InputMaybe<SearchResultSortParameter>;
@@ -2871,6 +2877,7 @@ export type Seller = Node & {
 export type SetCustomerForOrderResult =
     | AlreadyLoggedInError
     | EmailAddressConflictError
+    | GuestCheckoutError
     | NoActiveOrderError
     | Order;
 
@@ -3719,6 +3726,7 @@ export type SetCustomerForOrderMutation = {
         | { errorCode: ErrorCode; message: string }
         | { errorCode: ErrorCode; message: string }
         | { errorCode: ErrorCode; message: string }
+        | { errorCode: ErrorCode; message: string }
         | {
               id: string;
               customer?: { id: string; emailAddress: string; firstName: string; lastName: string } | null;

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

@@ -430,6 +430,9 @@ export const SET_CUSTOMER = gql`
                 errorCode
                 message
             }
+            ... on GuestCheckoutError {
+                errorDetail
+            }
         }
     }
     ${ACTIVE_ORDER_CUSTOMER}

+ 257 - 0
packages/core/e2e/guest-checkout-strategy.e2e-spec.ts

@@ -0,0 +1,257 @@
+import { CreateCustomerInput, SetCustomerForOrderResult } from '@vendure/common/lib/generated-shop-types';
+import {
+    GuestCheckoutStrategy,
+    Order,
+    RequestContext,
+    ErrorResultUnion,
+    Customer,
+    CustomerService,
+    GuestCheckoutError,
+    Injector,
+    TransactionalConnection,
+    ChannelService,
+} from '@vendure/core';
+import {
+    createErrorResultGuard,
+    createTestEnvironment,
+    ErrorResultGuard,
+    SimpleGraphQLClient,
+} from '@vendure/testing';
+import path from 'path';
+import { IsNull } from 'typeorm';
+import { it, afterAll, beforeAll, describe, expect } from 'vitest';
+
+import { initialData } from '../../../e2e-common/e2e-initial-data';
+import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
+import { AlreadyLoggedInError } from '../src/common/error/generated-graphql-shop-errors';
+import { CustomerEvent } from '../src/index';
+
+import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
+import * as Codegen from './graphql/generated-e2e-admin-types';
+import * as CodegenShop from './graphql/generated-e2e-shop-types';
+import { GET_CUSTOMER_LIST, GET_PRODUCTS_WITH_VARIANT_PRICES } from './graphql/shared-definitions';
+import { ADD_ITEM_TO_ORDER, SET_CUSTOMER } from './graphql/shop-definitions';
+
+class TestGuestCheckoutStrategy implements GuestCheckoutStrategy {
+    static allowGuestCheckout = true;
+    static allowGuestCheckoutForRegisteredCustomers = true;
+    static createNewCustomerOnEmailAddressConflict = false;
+
+    private customerService: CustomerService;
+    private connection: TransactionalConnection;
+    private channelService: ChannelService;
+
+    init(injector: Injector) {
+        this.customerService = injector.get(CustomerService);
+        this.connection = injector.get(TransactionalConnection);
+        this.channelService = injector.get(ChannelService);
+    }
+
+    async setCustomerForOrder(
+        ctx: RequestContext,
+        order: Order,
+        input: CreateCustomerInput,
+    ): Promise<ErrorResultUnion<SetCustomerForOrderResult, Customer>> {
+        if (TestGuestCheckoutStrategy.allowGuestCheckout === false) {
+            return new GuestCheckoutError({ errorDetail: 'Guest orders are disabled' });
+        }
+        if (ctx.activeUserId) {
+            return new AlreadyLoggedInError();
+        }
+        if (TestGuestCheckoutStrategy.createNewCustomerOnEmailAddressConflict === true) {
+            const existing = await this.connection.getRepository(ctx, Customer).findOne({
+                relations: ['channels'],
+                where: {
+                    emailAddress: input.emailAddress,
+                    deletedAt: IsNull(),
+                },
+            });
+            if (existing) {
+                const newCustomer = await this.connection
+                    .getRepository(ctx, Customer)
+                    .save(new Customer(input));
+                await this.channelService.assignToCurrentChannel(newCustomer, ctx);
+                return newCustomer;
+            }
+        }
+        const errorOnExistingUser = !TestGuestCheckoutStrategy.allowGuestCheckoutForRegisteredCustomers;
+        const customer = await this.customerService.createOrUpdate(ctx, input, errorOnExistingUser);
+        return customer;
+    }
+}
+
+describe('Order taxes', () => {
+    const { server, adminClient, shopClient } = createTestEnvironment({
+        ...testConfig(),
+        orderOptions: {
+            guestCheckoutStrategy: new TestGuestCheckoutStrategy(),
+        },
+        paymentOptions: {
+            paymentMethodHandlers: [testSuccessfulPaymentMethod],
+        },
+    });
+    let customers: Codegen.GetCustomerListQuery['customers']['items'];
+
+    const orderResultGuard: ErrorResultGuard<CodegenShop.ActiveOrderCustomerFragment> =
+        createErrorResultGuard(input => !!input.lines);
+
+    beforeAll(async () => {
+        await server.init({
+            initialData: {
+                ...initialData,
+                paymentMethods: [
+                    {
+                        name: testSuccessfulPaymentMethod.code,
+                        handler: { code: testSuccessfulPaymentMethod.code, arguments: [] },
+                    },
+                ],
+            },
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 2,
+        });
+        await adminClient.asSuperAdmin();
+        const result = await adminClient.query<Codegen.GetCustomerListQuery>(GET_CUSTOMER_LIST);
+        customers = result.customers.items;
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    it('with guest checkout disabled', async () => {
+        TestGuestCheckoutStrategy.allowGuestCheckout = false;
+
+        await shopClient.asAnonymousUser();
+        await addItemToOrder(shopClient);
+
+        const { setCustomerForOrder } = await shopClient.query<
+            CodegenShop.SetCustomerForOrderMutation,
+            CodegenShop.SetCustomerForOrderMutationVariables
+        >(SET_CUSTOMER, {
+            input: {
+                emailAddress: 'guest@test.com',
+                firstName: 'Guest',
+                lastName: 'User',
+            },
+        });
+
+        orderResultGuard.assertErrorResult(setCustomerForOrder);
+        expect(setCustomerForOrder.errorCode).toBe('GUEST_CHECKOUT_ERROR');
+        expect((setCustomerForOrder as any).errorDetail).toBe('Guest orders are disabled');
+    });
+
+    it('with guest checkout enabled', async () => {
+        TestGuestCheckoutStrategy.allowGuestCheckout = true;
+
+        await shopClient.asAnonymousUser();
+        await addItemToOrder(shopClient);
+
+        const { setCustomerForOrder } = await shopClient.query<
+            CodegenShop.SetCustomerForOrderMutation,
+            CodegenShop.SetCustomerForOrderMutationVariables
+        >(SET_CUSTOMER, {
+            input: {
+                emailAddress: 'guest@test.com',
+                firstName: 'Guest',
+                lastName: 'User',
+            },
+        });
+
+        orderResultGuard.assertSuccess(setCustomerForOrder);
+        expect(setCustomerForOrder.customer?.emailAddress).toBe('guest@test.com');
+    });
+
+    it('with guest checkout for registered customers disabled', async () => {
+        TestGuestCheckoutStrategy.allowGuestCheckoutForRegisteredCustomers = false;
+
+        await shopClient.asAnonymousUser();
+        await addItemToOrder(shopClient);
+
+        const { setCustomerForOrder } = await shopClient.query<
+            CodegenShop.SetCustomerForOrderMutation,
+            CodegenShop.SetCustomerForOrderMutationVariables
+        >(SET_CUSTOMER, {
+            input: {
+                emailAddress: customers[0].emailAddress,
+                firstName: customers[0].firstName,
+                lastName: customers[0].lastName,
+            },
+        });
+
+        orderResultGuard.assertErrorResult(setCustomerForOrder);
+        expect(setCustomerForOrder.errorCode).toBe('EMAIL_ADDRESS_CONFLICT_ERROR');
+    });
+
+    it('with guest checkout for registered customers enabled', async () => {
+        TestGuestCheckoutStrategy.allowGuestCheckoutForRegisteredCustomers = true;
+
+        await shopClient.asAnonymousUser();
+        await addItemToOrder(shopClient);
+
+        const { setCustomerForOrder } = await shopClient.query<
+            CodegenShop.SetCustomerForOrderMutation,
+            CodegenShop.SetCustomerForOrderMutationVariables
+        >(SET_CUSTOMER, {
+            input: {
+                emailAddress: customers[0].emailAddress,
+                firstName: customers[0].firstName,
+                lastName: customers[0].lastName,
+            },
+        });
+
+        orderResultGuard.assertSuccess(setCustomerForOrder);
+        expect(setCustomerForOrder.customer?.emailAddress).toBe(customers[0].emailAddress);
+        expect(setCustomerForOrder.customer?.id).toBe(customers[0].id);
+    });
+
+    it('create new customer on email address conflict', async () => {
+        TestGuestCheckoutStrategy.createNewCustomerOnEmailAddressConflict = true;
+
+        await shopClient.asAnonymousUser();
+        await addItemToOrder(shopClient);
+
+        const { setCustomerForOrder } = await shopClient.query<
+            CodegenShop.SetCustomerForOrderMutation,
+            CodegenShop.SetCustomerForOrderMutationVariables
+        >(SET_CUSTOMER, {
+            input: {
+                emailAddress: customers[0].emailAddress,
+                firstName: customers[0].firstName,
+                lastName: customers[0].lastName,
+            },
+        });
+
+        orderResultGuard.assertSuccess(setCustomerForOrder);
+        expect(setCustomerForOrder.customer?.emailAddress).toBe(customers[0].emailAddress);
+        expect(setCustomerForOrder.customer?.id).not.toBe(customers[0].id);
+    });
+
+    it('when already logged in', async () => {
+        await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
+        await addItemToOrder(shopClient);
+
+        const { setCustomerForOrder } = await shopClient.query<
+            CodegenShop.SetCustomerForOrderMutation,
+            CodegenShop.SetCustomerForOrderMutationVariables
+        >(SET_CUSTOMER, {
+            input: {
+                emailAddress: customers[0].emailAddress,
+                firstName: customers[0].firstName,
+                lastName: customers[0].lastName,
+            },
+        });
+
+        orderResultGuard.assertErrorResult(setCustomerForOrder);
+        expect(setCustomerForOrder.errorCode).toBe('ALREADY_LOGGED_IN_ERROR');
+    });
+});
+
+async function addItemToOrder(shopClient: SimpleGraphQLClient) {
+    await shopClient.query<CodegenShop.AddItemToOrderMutation, CodegenShop.AddItemToOrderMutationVariables>(
+        ADD_ITEM_TO_ORDER,
+        {
+            productVariantId: 'T_1',
+            quantity: 1,
+        },
+    );
+}

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

@@ -62,7 +62,7 @@ class TestTaxZoneStrategy implements TaxZoneStrategy {
         return zoneForCountryCode ?? channel.defaultTaxZone;
     }
 
-    private getZoneForCountryCode(ctx: RequestContext, countryCode?: string): Promise<Zone | undefined> {
+    private getZoneForCountryCode(ctx: RequestContext, countryCode?: string): Promise<Zone | null> {
         return this.connection
             .getRepository(ctx, Zone)
             .createQueryBuilder('zone')

+ 5 - 7
packages/core/src/api/resolvers/shop/shop-order.resolver.ts

@@ -429,19 +429,17 @@ export class ShopOrderResolver {
         @Args() args: MutationSetCustomerForOrderArgs & ActiveOrderArgs,
     ): Promise<ErrorResultUnion<SetCustomerForOrderResult, Order>> {
         if (ctx.authorizedAsOwnerOnly) {
-            if (ctx.activeUserId) {
-                return new AlreadyLoggedInError();
-            }
             const sessionOrder = await this.activeOrderService.getActiveOrder(
                 ctx,
                 args[ACTIVE_ORDER_INPUT_FIELD_NAME],
             );
             if (sessionOrder) {
-                const customer = await this.customerService.createOrUpdate(ctx, args.input, true);
-                if (isGraphQlErrorResult(customer)) {
-                    return customer;
+                const { guestCheckoutStrategy } = this.configService.orderOptions;
+                const result = await guestCheckoutStrategy.setCustomerForOrder(ctx, sessionOrder, args.input);
+                if (isGraphQlErrorResult(result)) {
+                    return result;
                 }
-                return this.orderService.addCustomerToOrder(ctx, sessionOrder.id, customer);
+                return this.orderService.addCustomerToOrder(ctx, sessionOrder.id, result);
             }
         }
         return new NoActiveOrderError();

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

@@ -26,6 +26,13 @@ type EmailAddressConflictError implements ErrorResult {
     message: String!
 }
 
+"Returned when attempting to set the Customer on a guest checkout when the configured GuestCheckoutStrategy does not allow it."
+type GuestCheckoutError implements ErrorResult {
+    errorCode: ErrorCode!
+    message: String!
+    errorDetail: String!
+}
+
 "Returned when the maximum order size limit has been reached."
 type OrderLimitError implements ErrorResult {
     errorCode: ErrorCode!

+ 1 - 1
packages/core/src/api/schema/shop-api/shop.api.graphql

@@ -204,7 +204,7 @@ union AddPaymentToOrderResult =
     | OrderStateTransitionError
     | NoActiveOrderError
 union TransitionOrderToStateResult = Order | OrderStateTransitionError
-union SetCustomerForOrderResult = Order | AlreadyLoggedInError | EmailAddressConflictError | NoActiveOrderError
+union SetCustomerForOrderResult = Order | AlreadyLoggedInError | EmailAddressConflictError | NoActiveOrderError | GuestCheckoutError
 union RegisterCustomerAccountResult = Success | MissingPasswordError | PasswordValidationError | NativeAuthStrategyError
 union RefreshCustomerVerificationResult = Success | NativeAuthStrategyError
 union VerifyCustomerAccountResult =

+ 14 - 1
packages/core/src/common/error/generated-graphql-admin-errors.ts

@@ -183,6 +183,19 @@ export class FulfillmentStateTransitionError extends ErrorResult {
   }
 }
 
+export class GuestCheckoutError extends ErrorResult {
+  readonly __typename = 'GuestCheckoutError';
+  readonly errorCode = 'GUEST_CHECKOUT_ERROR' as any;
+  readonly message = 'GUEST_CHECKOUT_ERROR';
+  readonly errorDetail: Scalars['String'];
+  constructor(
+    input: { errorDetail: Scalars['String'] }
+  ) {
+    super();
+    this.errorDetail = input.errorDetail
+  }
+}
+
 export class IneligibleShippingMethodError extends ErrorResult {
   readonly __typename = 'IneligibleShippingMethodError';
   readonly errorCode = 'INELIGIBLE_SHIPPING_METHOD_ERROR' as any;
@@ -549,7 +562,7 @@ export class SettlePaymentError extends ErrorResult {
 }
 
 
-const errorTypeNames = new Set<string>(['AlreadyRefundedError', 'CancelActiveOrderError', 'CancelPaymentError', 'ChannelDefaultLanguageError', 'CouponCodeExpiredError', 'CouponCodeInvalidError', 'CouponCodeLimitError', 'CreateFulfillmentError', 'EmailAddressConflictError', 'EmptyOrderLineSelectionError', 'FacetInUseError', 'FulfillmentStateTransitionError', 'IneligibleShippingMethodError', 'InsufficientStockError', 'InsufficientStockOnHandError', 'InvalidCredentialsError', 'InvalidFulfillmentHandlerError', 'ItemsAlreadyFulfilledError', 'LanguageNotAvailableError', 'ManualPaymentStateError', 'MimeTypeError', 'MissingConditionsError', 'MultipleOrderError', 'NativeAuthStrategyError', 'NegativeQuantityError', 'NoActiveOrderError', 'NoChangesSpecifiedError', 'NothingToRefundError', 'OrderLimitError', 'OrderModificationError', 'OrderModificationStateError', 'OrderStateTransitionError', 'PaymentMethodMissingError', 'PaymentOrderMismatchError', 'PaymentStateTransitionError', 'ProductOptionInUseError', 'QuantityTooGreatError', 'RefundOrderStateError', 'RefundPaymentIdMissingError', 'RefundStateTransitionError', 'SettlePaymentError']);
+const errorTypeNames = new Set<string>(['AlreadyRefundedError', 'CancelActiveOrderError', 'CancelPaymentError', 'ChannelDefaultLanguageError', 'CouponCodeExpiredError', 'CouponCodeInvalidError', 'CouponCodeLimitError', 'CreateFulfillmentError', 'EmailAddressConflictError', 'EmptyOrderLineSelectionError', 'FacetInUseError', 'FulfillmentStateTransitionError', 'GuestCheckoutError', 'IneligibleShippingMethodError', 'InsufficientStockError', 'InsufficientStockOnHandError', 'InvalidCredentialsError', 'InvalidFulfillmentHandlerError', 'ItemsAlreadyFulfilledError', 'LanguageNotAvailableError', 'ManualPaymentStateError', 'MimeTypeError', 'MissingConditionsError', 'MultipleOrderError', 'NativeAuthStrategyError', 'NegativeQuantityError', 'NoActiveOrderError', 'NoChangesSpecifiedError', 'NothingToRefundError', 'OrderLimitError', 'OrderModificationError', 'OrderModificationStateError', 'OrderStateTransitionError', 'PaymentMethodMissingError', 'PaymentOrderMismatchError', 'PaymentStateTransitionError', 'ProductOptionInUseError', 'QuantityTooGreatError', 'RefundOrderStateError', 'RefundPaymentIdMissingError', 'RefundStateTransitionError', 'SettlePaymentError']);
 function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').ErrorResult {
   return input instanceof ErrorResult || errorTypeNames.has(input.__typename);
 }

+ 14 - 1
packages/core/src/common/error/generated-graphql-shop-errors.ts

@@ -82,6 +82,19 @@ export class EmailAddressConflictError extends ErrorResult {
   }
 }
 
+export class GuestCheckoutError extends ErrorResult {
+  readonly __typename = 'GuestCheckoutError';
+  readonly errorCode = 'GUEST_CHECKOUT_ERROR' as any;
+  readonly message = 'GUEST_CHECKOUT_ERROR';
+  readonly errorDetail: Scalars['String'];
+  constructor(
+    input: { errorDetail: Scalars['String'] }
+  ) {
+    super();
+    this.errorDetail = input.errorDetail
+  }
+}
+
 export class IdentifierChangeTokenExpiredError extends ErrorResult {
   readonly __typename = 'IdentifierChangeTokenExpiredError';
   readonly errorCode = 'IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR' as any;
@@ -358,7 +371,7 @@ export class VerificationTokenInvalidError extends ErrorResult {
 }
 
 
-const errorTypeNames = new Set<string>(['AlreadyLoggedInError', 'CouponCodeExpiredError', 'CouponCodeInvalidError', 'CouponCodeLimitError', 'EmailAddressConflictError', 'IdentifierChangeTokenExpiredError', 'IdentifierChangeTokenInvalidError', 'IneligiblePaymentMethodError', 'IneligibleShippingMethodError', 'InsufficientStockError', 'InvalidCredentialsError', 'MissingPasswordError', 'NativeAuthStrategyError', 'NegativeQuantityError', 'NoActiveOrderError', 'NotVerifiedError', 'OrderLimitError', 'OrderModificationError', 'OrderPaymentStateError', 'OrderStateTransitionError', 'PasswordAlreadySetError', 'PasswordResetTokenExpiredError', 'PasswordResetTokenInvalidError', 'PasswordValidationError', 'PaymentDeclinedError', 'PaymentFailedError', 'VerificationTokenExpiredError', 'VerificationTokenInvalidError']);
+const errorTypeNames = new Set<string>(['AlreadyLoggedInError', 'CouponCodeExpiredError', 'CouponCodeInvalidError', 'CouponCodeLimitError', 'EmailAddressConflictError', 'GuestCheckoutError', 'IdentifierChangeTokenExpiredError', 'IdentifierChangeTokenInvalidError', 'IneligiblePaymentMethodError', 'IneligibleShippingMethodError', 'InsufficientStockError', 'InvalidCredentialsError', 'MissingPasswordError', 'NativeAuthStrategyError', 'NegativeQuantityError', 'NoActiveOrderError', 'NotVerifiedError', 'OrderLimitError', 'OrderModificationError', 'OrderPaymentStateError', 'OrderStateTransitionError', 'PasswordAlreadySetError', 'PasswordResetTokenExpiredError', 'PasswordResetTokenInvalidError', 'PasswordValidationError', 'PaymentDeclinedError', 'PaymentFailedError', 'VerificationTokenExpiredError', 'VerificationTokenInvalidError']);
 function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').ErrorResult {
   return input instanceof ErrorResult || errorTypeNames.has(input.__typename);
 }

+ 2 - 0
packages/core/src/config/config.module.ts

@@ -93,6 +93,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             activeOrderStrategy,
             changedPriceHandlingStrategy,
             orderSellerStrategy,
+            guestCheckoutStrategy,
         } = this.configService.orderOptions;
         const {
             customFulfillmentProcess,
@@ -140,6 +141,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             shippingLineAssignmentStrategy,
             stockLocationStrategy,
             productVariantPriceSelectionStrategy,
+            guestCheckoutStrategy,
         ];
     }
 

+ 2 - 0
packages/core/src/config/default-config.ts

@@ -28,6 +28,7 @@ import { manualFulfillmentHandler } from './fulfillment/manual-fulfillment-handl
 import { DefaultLogger } from './logger/default-logger';
 import { DefaultActiveOrderStrategy } from './order/default-active-order-strategy';
 import { DefaultChangedPriceHandlingStrategy } from './order/default-changed-price-handling-strategy';
+import { DefaultGuestCheckoutStrategy } from './order/default-guest-checkout-strategy';
 import { DefaultOrderItemPriceCalculationStrategy } from './order/default-order-item-price-calculation-strategy';
 import { DefaultOrderPlacedStrategy } from './order/default-order-placed-strategy';
 import { defaultOrderProcess } from './order/default-order-process';
@@ -154,6 +155,7 @@ export const defaultConfig: RuntimeVendureConfig = {
         orderPlacedStrategy: new DefaultOrderPlacedStrategy(),
         activeOrderStrategy: new DefaultActiveOrderStrategy(),
         orderSellerStrategy: new DefaultOrderSellerStrategy(),
+        guestCheckoutStrategy: new DefaultGuestCheckoutStrategy(),
     },
     paymentOptions: {
         paymentMethodEligibilityCheckers: [],

+ 2 - 0
packages/core/src/config/index.ts

@@ -48,6 +48,8 @@ export * from './order/default-order-placed-strategy';
 export * from './order/default-order-process';
 export * from './order/default-order-seller-strategy';
 export * from './order/default-stock-allocation-strategy';
+export * from './order/default-guest-checkout-strategy';
+export * from './order/guest-checkout-strategy';
 export * from './order/merge-orders-strategy';
 export * from './order/order-code-strategy';
 export * from './order/order-item-price-calculation-strategy';

+ 95 - 0
packages/core/src/config/order/default-guest-checkout-strategy.ts

@@ -0,0 +1,95 @@
+import { CreateCustomerInput, SetCustomerForOrderResult } from '@vendure/common/lib/generated-shop-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import { AlreadyLoggedInError, GuestCheckoutError } from '../../common/error/generated-graphql-shop-errors';
+import { ErrorResultUnion, Injector } from '../../common/index';
+import { InjectableStrategy } from '../../common/types/injectable-strategy';
+import { Customer, Order } from '../../entity/index';
+import { CustomerService } from '../../service/services/customer.service';
+
+import { GuestCheckoutStrategy } from './guest-checkout-strategy';
+
+/**
+ * @description
+ * Options available for the {@link DefaultGuestCheckoutStrategy}.
+ *
+ * @docsCategory orders
+ * @docsPage DefaultGuestCheckoutStrategy
+ * @since 2.0.0
+ */
+export interface DefaultGuestCheckoutStrategyOptions {
+    /**
+     * @description
+     * Whether to allow guest checkouts.
+     *
+     * @default true
+     */
+    allowGuestCheckouts?: boolean;
+    /**
+     * @description
+     * Whether to allow guest checkouts for customers who already have an account.
+     * Note that when this is enabled, the details provided in the `CreateCustomerInput`
+     * will overwrite the existing customer details of the registered customer.
+     *
+     * @default false
+     */
+    allowGuestCheckoutForRegisteredCustomers?: boolean;
+}
+
+/**
+ * @description
+ * The default implementation of the {@link GuestCheckoutStrategy}. This strategy allows
+ * guest checkouts by default, but can be configured to disallow them.
+ *
+ * @example
+ * ```TypeScript
+ * import { DefaultGuestCheckoutStrategy, VendureConfig } from '\@vendure/core';
+ *
+ * export const config: VendureConfig = {
+ *   orderOptions: {
+ *     guestCheckoutStrategy: new DefaultGuestCheckoutStrategy({
+ *       allowGuestCheckouts: false,
+ *       allowGuestCheckoutForRegisteredCustomers: false,
+ *     }),
+ *   },
+ *   // ...
+ * };
+ * ```
+ *
+ * @docsCategory orders
+ * @docsPage DefaultGuestCheckoutStrategy
+ * @docsWeight 0
+ * @since 2.0.0
+ */
+export class DefaultGuestCheckoutStrategy implements GuestCheckoutStrategy {
+    private customerService: CustomerService;
+    private readonly options: Required<DefaultGuestCheckoutStrategyOptions> = {
+        allowGuestCheckouts: true,
+        allowGuestCheckoutForRegisteredCustomers: false,
+    };
+    init(injector: Injector) {
+        this.customerService = injector.get(CustomerService);
+    }
+
+    constructor(options?: DefaultGuestCheckoutStrategyOptions) {
+        this.options = {
+            ...this.options,
+            ...(options ?? {}),
+        };
+    }
+    async setCustomerForOrder(
+        ctx: RequestContext,
+        order: Order,
+        input: CreateCustomerInput,
+    ): Promise<ErrorResultUnion<SetCustomerForOrderResult, Customer>> {
+        if (!this.options.allowGuestCheckouts) {
+            return new GuestCheckoutError({ errorDetail: 'Guest checkouts are disabled' });
+        }
+        if (ctx.activeUserId) {
+            return new AlreadyLoggedInError();
+        }
+        const errorOnExistingUser = !this.options.allowGuestCheckoutForRegisteredCustomers;
+        const customer = await this.customerService.createOrUpdate(ctx, input, errorOnExistingUser);
+        return customer;
+    }
+}

+ 38 - 0
packages/core/src/config/order/guest-checkout-strategy.ts

@@ -0,0 +1,38 @@
+import { CreateCustomerInput, SetCustomerForOrderResult } from '@vendure/common/lib/generated-shop-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import { ErrorResultUnion } from '../../common/index';
+import { InjectableStrategy } from '../../common/types/injectable-strategy';
+import { Customer, Order } from '../../entity/index';
+
+/**
+ * @description
+ * A strategy that determines how to deal with guest checkouts - i.e. when a customer
+ * checks out without being logged in. For example, a strategy could be used to implement
+ * business rules such as:
+ *
+ * - No guest checkouts allowed
+ * - No guest checkouts allowed for customers who already have an account
+ * - No guest checkouts allowed for customers who have previously placed an order
+ * - Allow guest checkouts, but create a new Customer entity if the email address
+ *   is already in use
+ * - Allow guest checkouts, but update the existing Customer entity if the email address
+ *   is already in use
+ *
+ * @docsCategory orders
+ * @since 2.0.0
+ */
+export interface GuestCheckoutStrategy extends InjectableStrategy {
+    /**
+     * @description
+     * This method is called when the `setCustomerForOrder` mutation is executed.
+     * It should return either a Customer object or an ErrorResult.
+     */
+    setCustomerForOrder(
+        ctx: RequestContext,
+        order: Order,
+        input: CreateCustomerInput,
+    ):
+        | ErrorResultUnion<SetCustomerForOrderResult, Customer>
+        | Promise<ErrorResultUnion<SetCustomerForOrderResult, Customer>>;
+}

+ 9 - 0
packages/core/src/config/vendure-config.ts

@@ -31,6 +31,7 @@ import { JobQueueStrategy } from './job-queue/job-queue-strategy';
 import { VendureLogger } from './logger/vendure-logger';
 import { ActiveOrderStrategy } from './order/active-order-strategy';
 import { ChangedPriceHandlingStrategy } from './order/changed-price-handling-strategy';
+import { GuestCheckoutStrategy } from './order/guest-checkout-strategy';
 import { OrderByCodeAccessStrategy } from './order/order-by-code-access-strategy';
 import { OrderCodeStrategy } from './order/order-code-strategy';
 import { OrderItemPriceCalculationStrategy } from './order/order-item-price-calculation-strategy';
@@ -583,6 +584,14 @@ export interface OrderOptions {
      * @default DefaultOrderSellerStrategy
      */
     orderSellerStrategy?: OrderSellerStrategy;
+    /**
+     * @description
+     * Defines how we deal with guest checkouts.
+     *
+     * @since 2.0.0
+     * @default DefaultGuestCheckoutStrategy
+     */
+    guestCheckoutStrategy?: GuestCheckoutStrategy;
 }
 
 /**

+ 8 - 2
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -1422,6 +1422,7 @@ export enum ErrorCode {
   EMPTY_ORDER_LINE_SELECTION_ERROR = 'EMPTY_ORDER_LINE_SELECTION_ERROR',
   FACET_IN_USE_ERROR = 'FACET_IN_USE_ERROR',
   FULFILLMENT_STATE_TRANSITION_ERROR = 'FULFILLMENT_STATE_TRANSITION_ERROR',
+  GUEST_CHECKOUT_ERROR = 'GUEST_CHECKOUT_ERROR',
   INELIGIBLE_SHIPPING_METHOD_ERROR = 'INELIGIBLE_SHIPPING_METHOD_ERROR',
   INSUFFICIENT_STOCK_ERROR = 'INSUFFICIENT_STOCK_ERROR',
   INSUFFICIENT_STOCK_ON_HAND_ERROR = 'INSUFFICIENT_STOCK_ON_HAND_ERROR',
@@ -1683,6 +1684,13 @@ export type GlobalSettings = {
   updatedAt: Scalars['DateTime'];
 };
 
+/** Returned when attempting to set the Customer on a guest checkout when the configured GuestCheckoutStrategy does not allow it. */
+export type GuestCheckoutError = ErrorResult & {
+  errorCode: ErrorCode;
+  errorDetail: Scalars['String'];
+  message: Scalars['String'];
+};
+
 export type HistoryEntry = Node & {
   administrator?: Maybe<Administrator>;
   createdAt: Scalars['DateTime'];
@@ -4899,8 +4907,6 @@ export type SearchInput = {
   collectionId?: InputMaybe<Scalars['ID']>;
   collectionSlug?: InputMaybe<Scalars['String']>;
   facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
-  facetValueIds?: InputMaybe<Array<Scalars['ID']>>;
-  facetValueOperator?: InputMaybe<LogicalOperator>;
   groupByProduct?: InputMaybe<Scalars['Boolean']>;
   skip?: InputMaybe<Scalars['Int']>;
   sort?: InputMaybe<SearchResultSortParameter>;

+ 8 - 2
packages/payments-plugin/e2e/graphql/generated-admin-types.ts

@@ -1422,6 +1422,7 @@ export enum ErrorCode {
   EMPTY_ORDER_LINE_SELECTION_ERROR = 'EMPTY_ORDER_LINE_SELECTION_ERROR',
   FACET_IN_USE_ERROR = 'FACET_IN_USE_ERROR',
   FULFILLMENT_STATE_TRANSITION_ERROR = 'FULFILLMENT_STATE_TRANSITION_ERROR',
+  GUEST_CHECKOUT_ERROR = 'GUEST_CHECKOUT_ERROR',
   INELIGIBLE_SHIPPING_METHOD_ERROR = 'INELIGIBLE_SHIPPING_METHOD_ERROR',
   INSUFFICIENT_STOCK_ERROR = 'INSUFFICIENT_STOCK_ERROR',
   INSUFFICIENT_STOCK_ON_HAND_ERROR = 'INSUFFICIENT_STOCK_ON_HAND_ERROR',
@@ -1683,6 +1684,13 @@ export type GlobalSettings = {
   updatedAt: Scalars['DateTime'];
 };
 
+/** Returned when attempting to set the Customer on a guest checkout when the configured GuestCheckoutStrategy does not allow it. */
+export type GuestCheckoutError = ErrorResult & {
+  errorCode: ErrorCode;
+  errorDetail: Scalars['String'];
+  message: Scalars['String'];
+};
+
 export type HistoryEntry = Node & {
   administrator?: Maybe<Administrator>;
   createdAt: Scalars['DateTime'];
@@ -4899,8 +4907,6 @@ export type SearchInput = {
   collectionId?: InputMaybe<Scalars['ID']>;
   collectionSlug?: InputMaybe<Scalars['String']>;
   facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
-  facetValueIds?: InputMaybe<Array<Scalars['ID']>>;
-  facetValueOperator?: InputMaybe<LogicalOperator>;
   groupByProduct?: InputMaybe<Scalars['Boolean']>;
   skip?: InputMaybe<Scalars['Int']>;
   sort?: InputMaybe<SearchResultSortParameter>;

+ 9 - 3
packages/payments-plugin/e2e/graphql/generated-shop-types.ts

@@ -837,6 +837,7 @@ export enum ErrorCode {
   COUPON_CODE_INVALID_ERROR = 'COUPON_CODE_INVALID_ERROR',
   COUPON_CODE_LIMIT_ERROR = 'COUPON_CODE_LIMIT_ERROR',
   EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR',
+  GUEST_CHECKOUT_ERROR = 'GUEST_CHECKOUT_ERROR',
   IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR = 'IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR',
   IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR = 'IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR',
   INELIGIBLE_PAYMENT_METHOD_ERROR = 'INELIGIBLE_PAYMENT_METHOD_ERROR',
@@ -1007,6 +1008,13 @@ export enum GlobalFlag {
   TRUE = 'TRUE'
 }
 
+/** Returned when attempting to set the Customer on a guest checkout when the configured GuestCheckoutStrategy does not allow it. */
+export type GuestCheckoutError = ErrorResult & {
+  errorCode: ErrorCode;
+  errorDetail: Scalars['String'];
+  message: Scalars['String'];
+};
+
 export type HistoryEntry = Node & {
   createdAt: Scalars['DateTime'];
   data: Scalars['JSON'];
@@ -2807,8 +2815,6 @@ export type SearchInput = {
   collectionId?: InputMaybe<Scalars['ID']>;
   collectionSlug?: InputMaybe<Scalars['String']>;
   facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
-  facetValueIds?: InputMaybe<Array<Scalars['ID']>>;
-  facetValueOperator?: InputMaybe<LogicalOperator>;
   groupByProduct?: InputMaybe<Scalars['Boolean']>;
   skip?: InputMaybe<Scalars['Int']>;
   sort?: InputMaybe<SearchResultSortParameter>;
@@ -2870,7 +2876,7 @@ export type Seller = Node & {
   updatedAt: Scalars['DateTime'];
 };
 
-export type SetCustomerForOrderResult = AlreadyLoggedInError | EmailAddressConflictError | NoActiveOrderError | Order;
+export type SetCustomerForOrderResult = AlreadyLoggedInError | EmailAddressConflictError | GuestCheckoutError | NoActiveOrderError | Order;
 
 export type SetOrderShippingMethodResult = IneligibleShippingMethodError | NoActiveOrderError | Order | OrderModificationError;
 

+ 10 - 3
packages/payments-plugin/src/mollie/graphql/generated-shop-types.ts

@@ -874,6 +874,7 @@ export enum ErrorCode {
   COUPON_CODE_INVALID_ERROR = 'COUPON_CODE_INVALID_ERROR',
   COUPON_CODE_LIMIT_ERROR = 'COUPON_CODE_LIMIT_ERROR',
   EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR',
+  GUEST_CHECKOUT_ERROR = 'GUEST_CHECKOUT_ERROR',
   IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR = 'IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR',
   IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR = 'IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR',
   INELIGIBLE_PAYMENT_METHOD_ERROR = 'INELIGIBLE_PAYMENT_METHOD_ERROR',
@@ -1053,6 +1054,14 @@ export enum GlobalFlag {
   TRUE = 'TRUE'
 }
 
+/** Returned when attempting to set the Customer on a guest checkout when the configured GuestCheckoutStrategy does not allow it. */
+export type GuestCheckoutError = ErrorResult & {
+  __typename?: 'GuestCheckoutError';
+  errorCode: ErrorCode;
+  errorDetail: Scalars['String'];
+  message: Scalars['String'];
+};
+
 export type HistoryEntry = Node & {
   __typename?: 'HistoryEntry';
   createdAt: Scalars['DateTime'];
@@ -2968,8 +2977,6 @@ export type SearchInput = {
   collectionId?: InputMaybe<Scalars['ID']>;
   collectionSlug?: InputMaybe<Scalars['String']>;
   facetValueFilters?: InputMaybe<Array<FacetValueFilterInput>>;
-  facetValueIds?: InputMaybe<Array<Scalars['ID']>>;
-  facetValueOperator?: InputMaybe<LogicalOperator>;
   groupByProduct?: InputMaybe<Scalars['Boolean']>;
   skip?: InputMaybe<Scalars['Int']>;
   sort?: InputMaybe<SearchResultSortParameter>;
@@ -3036,7 +3043,7 @@ export type Seller = Node & {
   updatedAt: Scalars['DateTime'];
 };
 
-export type SetCustomerForOrderResult = AlreadyLoggedInError | EmailAddressConflictError | NoActiveOrderError | Order;
+export type SetCustomerForOrderResult = AlreadyLoggedInError | EmailAddressConflictError | GuestCheckoutError | NoActiveOrderError | Order;
 
 export type SetOrderShippingMethodResult = IneligibleShippingMethodError | NoActiveOrderError | Order | OrderModificationError;
 

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
schema-admin.json


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
schema-shop.json


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels