Browse Source

feat(core): Implement eligibility checking for PaymentMethods

Relates to #469
Michael Bromley 5 years ago
parent
commit
690514aa4d
29 changed files with 509 additions and 92 deletions
  1. 14 9
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  2. 22 11
      packages/common/src/generated-shop-types.ts
  3. 15 10
      packages/common/src/generated-types.ts
  4. 62 9
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  5. 25 10
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  6. 3 0
      packages/core/e2e/graphql/shop-definitions.ts
  7. 152 9
      packages/core/e2e/payment-method.e2e-spec.ts
  8. 3 0
      packages/core/src/api/common/configurable-operation-codec.ts
  9. 13 0
      packages/core/src/api/resolvers/admin/payment-method.resolver.ts
  10. 3 0
      packages/core/src/api/schema/admin-api/payment-method.api.graphql
  11. 1 0
      packages/core/src/api/schema/admin-api/payment-method.type.graphql
  12. 10 0
      packages/core/src/api/schema/common/common-types.graphql
  13. 0 9
      packages/core/src/api/schema/common/order.type.graphql
  14. 8 1
      packages/core/src/api/schema/shop-api/shop-error-results.graphql
  15. 1 0
      packages/core/src/api/schema/shop-api/shop.api.graphql
  16. 12 1
      packages/core/src/common/error/generated-graphql-shop-errors.ts
  17. 2 1
      packages/core/src/config/config.module.ts
  18. 1 0
      packages/core/src/config/default-config.ts
  19. 1 1
      packages/core/src/config/index.ts
  20. 79 0
      packages/core/src/config/payment-method/payment-method-eligibility-checker.ts
  21. 3 3
      packages/core/src/config/shipping-method/shipping-eligibility-checker.ts
  22. 9 1
      packages/core/src/config/vendure-config.ts
  23. 2 0
      packages/core/src/entity/payment-method/payment-method.entity.ts
  24. 4 0
      packages/core/src/service/helpers/config-arg/config-arg.service.ts
  25. 4 0
      packages/core/src/service/services/order.service.ts
  26. 46 8
      packages/core/src/service/services/payment-method.service.ts
  27. 14 9
      packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts
  28. 0 0
      schema-admin.json
  29. 0 0
      schema-shop.json

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

@@ -49,6 +49,7 @@ export type Query = {
     orders: OrderList;
     paymentMethods: PaymentMethodList;
     paymentMethod?: Maybe<PaymentMethod>;
+    paymentMethodEligibilityCheckers: Array<ConfigurableOperationDefinition>;
     paymentMethodHandlers: Array<ConfigurableOperationDefinition>;
     productOptionGroups: Array<ProductOptionGroup>;
     productOptionGroup?: Maybe<ProductOptionGroup>;
@@ -1735,6 +1736,7 @@ export type CreatePaymentMethodInput = {
     code: Scalars['String'];
     description?: Maybe<Scalars['String']>;
     enabled: Scalars['Boolean'];
+    checker?: Maybe<ConfigurableOperationInput>;
     handler: ConfigurableOperationInput;
 };
 
@@ -1744,6 +1746,7 @@ export type UpdatePaymentMethodInput = {
     code?: Maybe<Scalars['String']>;
     description?: Maybe<Scalars['String']>;
     enabled?: Maybe<Scalars['Boolean']>;
+    checker?: Maybe<ConfigurableOperationInput>;
     handler?: Maybe<ConfigurableOperationInput>;
 };
 
@@ -1755,6 +1758,7 @@ export type PaymentMethod = Node & {
     code: Scalars['String'];
     description: Scalars['String'];
     enabled: Scalars['Boolean'];
+    checker?: Maybe<ConfigurableOperation>;
     handler: ConfigurableOperation;
 };
 
@@ -2679,6 +2683,16 @@ export type Success = {
     success: Scalars['Boolean'];
 };
 
+export type ShippingMethodQuote = {
+    id: Scalars['ID'];
+    price: Scalars['Int'];
+    priceWithTax: Scalars['Int'];
+    name: Scalars['String'];
+    description: Scalars['String'];
+    /** Any optional metadata returned by the ShippingCalculator in the ShippingCalculationResult */
+    metadata?: Maybe<Scalars['JSON']>;
+};
+
 export type Country = Node & {
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
@@ -3587,15 +3601,6 @@ export type OrderList = PaginatedList & {
     totalItems: Scalars['Int'];
 };
 
-export type ShippingMethodQuote = {
-    id: Scalars['ID'];
-    price: Scalars['Int'];
-    priceWithTax: Scalars['Int'];
-    name: Scalars['String'];
-    description: Scalars['String'];
-    metadata?: Maybe<Scalars['JSON']>;
-};
-
 export type ShippingLine = {
     shippingMethod: ShippingMethod;
     price: Scalars['Int'];

+ 22 - 11
packages/common/src/generated-shop-types.ts

@@ -530,6 +530,7 @@ export enum ErrorCode {
     ORDER_MODIFICATION_ERROR = 'ORDER_MODIFICATION_ERROR',
     INELIGIBLE_SHIPPING_METHOD_ERROR = 'INELIGIBLE_SHIPPING_METHOD_ERROR',
     ORDER_PAYMENT_STATE_ERROR = 'ORDER_PAYMENT_STATE_ERROR',
+    INELIGIBLE_PAYMENT_METHOD_ERROR = 'INELIGIBLE_PAYMENT_METHOD_ERROR',
     PAYMENT_FAILED_ERROR = 'PAYMENT_FAILED_ERROR',
     PAYMENT_DECLINED_ERROR = 'PAYMENT_DECLINED_ERROR',
     COUPON_CODE_INVALID_ERROR = 'COUPON_CODE_INVALID_ERROR',
@@ -788,6 +789,17 @@ export type Success = {
     success: Scalars['Boolean'];
 };
 
+export type ShippingMethodQuote = {
+    __typename?: 'ShippingMethodQuote';
+    id: Scalars['ID'];
+    price: Scalars['Int'];
+    priceWithTax: Scalars['Int'];
+    name: Scalars['String'];
+    description: Scalars['String'];
+    /** Any optional metadata returned by the ShippingCalculator in the ShippingCalculationResult */
+    metadata?: Maybe<Scalars['JSON']>;
+};
+
 export type Country = Node & {
     __typename?: 'Country';
     id: Scalars['ID'];
@@ -1826,16 +1838,6 @@ export type OrderList = PaginatedList & {
     totalItems: Scalars['Int'];
 };
 
-export type ShippingMethodQuote = {
-    __typename?: 'ShippingMethodQuote';
-    id: Scalars['ID'];
-    price: Scalars['Int'];
-    priceWithTax: Scalars['Int'];
-    name: Scalars['String'];
-    description: Scalars['String'];
-    metadata?: Maybe<Scalars['JSON']>;
-};
-
 export type ShippingLine = {
     __typename?: 'ShippingLine';
     shippingMethod: ShippingMethod;
@@ -2336,7 +2338,7 @@ export type OrderModificationError = ErrorResult & {
     message: Scalars['String'];
 };
 
-/** Returned when attempting to set a ShippingMethod for which the order is not eligible */
+/** Returned when attempting to set a ShippingMethod for which the Order is not eligible */
 export type IneligibleShippingMethodError = ErrorResult & {
     __typename?: 'IneligibleShippingMethodError';
     errorCode: ErrorCode;
@@ -2350,6 +2352,14 @@ export type OrderPaymentStateError = ErrorResult & {
     message: Scalars['String'];
 };
 
+/** Returned when attempting to add a Payment using a PaymentMethod for which the Order is not eligible. */
+export type IneligiblePaymentMethodError = ErrorResult & {
+    __typename?: 'IneligiblePaymentMethodError';
+    errorCode: ErrorCode;
+    message: Scalars['String'];
+    eligibilityCheckerMessage?: Maybe<Scalars['String']>;
+};
+
 /** Returned when a Payment fails due to an error. */
 export type PaymentFailedError = ErrorResult & {
     __typename?: 'PaymentFailedError';
@@ -2545,6 +2555,7 @@ export type ApplyCouponCodeResult =
 export type AddPaymentToOrderResult =
     | Order
     | OrderPaymentStateError
+    | IneligiblePaymentMethodError
     | PaymentFailedError
     | PaymentDeclinedError
     | OrderStateTransitionError

+ 15 - 10
packages/common/src/generated-types.ts

@@ -50,6 +50,7 @@ export type Query = {
   orders: OrderList;
   paymentMethods: PaymentMethodList;
   paymentMethod?: Maybe<PaymentMethod>;
+  paymentMethodEligibilityCheckers: Array<ConfigurableOperationDefinition>;
   paymentMethodHandlers: Array<ConfigurableOperationDefinition>;
   productOptionGroups: Array<ProductOptionGroup>;
   productOptionGroup?: Maybe<ProductOptionGroup>;
@@ -1893,6 +1894,7 @@ export type CreatePaymentMethodInput = {
   code: Scalars['String'];
   description?: Maybe<Scalars['String']>;
   enabled: Scalars['Boolean'];
+  checker?: Maybe<ConfigurableOperationInput>;
   handler: ConfigurableOperationInput;
 };
 
@@ -1902,6 +1904,7 @@ export type UpdatePaymentMethodInput = {
   code?: Maybe<Scalars['String']>;
   description?: Maybe<Scalars['String']>;
   enabled?: Maybe<Scalars['Boolean']>;
+  checker?: Maybe<ConfigurableOperationInput>;
   handler?: Maybe<ConfigurableOperationInput>;
 };
 
@@ -1914,6 +1917,7 @@ export type PaymentMethod = Node & {
   code: Scalars['String'];
   description: Scalars['String'];
   enabled: Scalars['Boolean'];
+  checker?: Maybe<ConfigurableOperation>;
   handler: ConfigurableOperation;
 };
 
@@ -2875,6 +2879,17 @@ export type Success = {
   success: Scalars['Boolean'];
 };
 
+export type ShippingMethodQuote = {
+  __typename?: 'ShippingMethodQuote';
+  id: Scalars['ID'];
+  price: Scalars['Int'];
+  priceWithTax: Scalars['Int'];
+  name: Scalars['String'];
+  description: Scalars['String'];
+  /** Any optional metadata returned by the ShippingCalculator in the ShippingCalculationResult */
+  metadata?: Maybe<Scalars['JSON']>;
+};
+
 export type Country = Node & {
   __typename?: 'Country';
   id: Scalars['ID'];
@@ -3799,16 +3814,6 @@ export type OrderList = PaginatedList & {
   totalItems: Scalars['Int'];
 };
 
-export type ShippingMethodQuote = {
-  __typename?: 'ShippingMethodQuote';
-  id: Scalars['ID'];
-  price: Scalars['Int'];
-  priceWithTax: Scalars['Int'];
-  name: Scalars['String'];
-  description: Scalars['String'];
-  metadata?: Maybe<Scalars['JSON']>;
-};
-
 export type ShippingLine = {
   __typename?: 'ShippingLine';
   shippingMethod: ShippingMethod;

+ 62 - 9
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -49,6 +49,7 @@ export type Query = {
     orders: OrderList;
     paymentMethods: PaymentMethodList;
     paymentMethod?: Maybe<PaymentMethod>;
+    paymentMethodEligibilityCheckers: Array<ConfigurableOperationDefinition>;
     paymentMethodHandlers: Array<ConfigurableOperationDefinition>;
     productOptionGroups: Array<ProductOptionGroup>;
     productOptionGroup?: Maybe<ProductOptionGroup>;
@@ -1735,6 +1736,7 @@ export type CreatePaymentMethodInput = {
     code: Scalars['String'];
     description?: Maybe<Scalars['String']>;
     enabled: Scalars['Boolean'];
+    checker?: Maybe<ConfigurableOperationInput>;
     handler: ConfigurableOperationInput;
 };
 
@@ -1744,6 +1746,7 @@ export type UpdatePaymentMethodInput = {
     code?: Maybe<Scalars['String']>;
     description?: Maybe<Scalars['String']>;
     enabled?: Maybe<Scalars['Boolean']>;
+    checker?: Maybe<ConfigurableOperationInput>;
     handler?: Maybe<ConfigurableOperationInput>;
 };
 
@@ -1755,6 +1758,7 @@ export type PaymentMethod = Node & {
     code: Scalars['String'];
     description: Scalars['String'];
     enabled: Scalars['Boolean'];
+    checker?: Maybe<ConfigurableOperation>;
     handler: ConfigurableOperation;
 };
 
@@ -2679,6 +2683,16 @@ export type Success = {
     success: Scalars['Boolean'];
 };
 
+export type ShippingMethodQuote = {
+    id: Scalars['ID'];
+    price: Scalars['Int'];
+    priceWithTax: Scalars['Int'];
+    name: Scalars['String'];
+    description: Scalars['String'];
+    /** Any optional metadata returned by the ShippingCalculator in the ShippingCalculationResult */
+    metadata?: Maybe<Scalars['JSON']>;
+};
+
 export type Country = Node & {
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
@@ -3587,15 +3601,6 @@ export type OrderList = PaginatedList & {
     totalItems: Scalars['Int'];
 };
 
-export type ShippingMethodQuote = {
-    id: Scalars['ID'];
-    price: Scalars['Int'];
-    priceWithTax: Scalars['Int'];
-    name: Scalars['String'];
-    description: Scalars['String'];
-    metadata?: Maybe<Scalars['JSON']>;
-};
-
 export type ShippingLine = {
     shippingMethod: ShippingMethod;
     price: Scalars['Int'];
@@ -6039,6 +6044,26 @@ export type UpdatePaymentMethodMutationVariables = Exact<{
 
 export type UpdatePaymentMethodMutation = { updatePaymentMethod: PaymentMethodFragment };
 
+export type GetPaymentMethodHandlersQueryVariables = Exact<{ [key: string]: never }>;
+
+export type GetPaymentMethodHandlersQuery = {
+    paymentMethodHandlers: Array<
+        Pick<ConfigurableOperationDefinition, 'code'> & {
+            args: Array<Pick<ConfigArgDefinition, 'name' | 'type'>>;
+        }
+    >;
+};
+
+export type GetPaymentMethodCheckersQueryVariables = Exact<{ [key: string]: never }>;
+
+export type GetPaymentMethodCheckersQuery = {
+    paymentMethodEligibilityCheckers: Array<
+        Pick<ConfigurableOperationDefinition, 'code'> & {
+            args: Array<Pick<ConfigArgDefinition, 'name' | 'type'>>;
+        }
+    >;
+};
+
 export type UpdateProductOptionGroupMutationVariables = Exact<{
     input: UpdateProductOptionGroupInput;
 }>;
@@ -8142,6 +8167,34 @@ export namespace UpdatePaymentMethod {
     export type UpdatePaymentMethod = NonNullable<UpdatePaymentMethodMutation['updatePaymentMethod']>;
 }
 
+export namespace GetPaymentMethodHandlers {
+    export type Variables = GetPaymentMethodHandlersQueryVariables;
+    export type Query = GetPaymentMethodHandlersQuery;
+    export type PaymentMethodHandlers = NonNullable<
+        NonNullable<GetPaymentMethodHandlersQuery['paymentMethodHandlers']>[number]
+    >;
+    export type Args = NonNullable<
+        NonNullable<
+            NonNullable<NonNullable<GetPaymentMethodHandlersQuery['paymentMethodHandlers']>[number]>['args']
+        >[number]
+    >;
+}
+
+export namespace GetPaymentMethodCheckers {
+    export type Variables = GetPaymentMethodCheckersQueryVariables;
+    export type Query = GetPaymentMethodCheckersQuery;
+    export type PaymentMethodEligibilityCheckers = NonNullable<
+        NonNullable<GetPaymentMethodCheckersQuery['paymentMethodEligibilityCheckers']>[number]
+    >;
+    export type Args = NonNullable<
+        NonNullable<
+            NonNullable<
+                NonNullable<GetPaymentMethodCheckersQuery['paymentMethodEligibilityCheckers']>[number]
+            >['args']
+        >[number]
+    >;
+}
+
 export namespace UpdateProductOptionGroup {
     export type Variables = UpdateProductOptionGroupMutationVariables;
     export type Mutation = UpdateProductOptionGroupMutation;

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

@@ -516,6 +516,7 @@ export enum ErrorCode {
     ORDER_MODIFICATION_ERROR = 'ORDER_MODIFICATION_ERROR',
     INELIGIBLE_SHIPPING_METHOD_ERROR = 'INELIGIBLE_SHIPPING_METHOD_ERROR',
     ORDER_PAYMENT_STATE_ERROR = 'ORDER_PAYMENT_STATE_ERROR',
+    INELIGIBLE_PAYMENT_METHOD_ERROR = 'INELIGIBLE_PAYMENT_METHOD_ERROR',
     PAYMENT_FAILED_ERROR = 'PAYMENT_FAILED_ERROR',
     PAYMENT_DECLINED_ERROR = 'PAYMENT_DECLINED_ERROR',
     COUPON_CODE_INVALID_ERROR = 'COUPON_CODE_INVALID_ERROR',
@@ -759,6 +760,16 @@ export type Success = {
     success: Scalars['Boolean'];
 };
 
+export type ShippingMethodQuote = {
+    id: Scalars['ID'];
+    price: Scalars['Int'];
+    priceWithTax: Scalars['Int'];
+    name: Scalars['String'];
+    description: Scalars['String'];
+    /** Any optional metadata returned by the ShippingCalculator in the ShippingCalculationResult */
+    metadata?: Maybe<Scalars['JSON']>;
+};
+
 export type Country = Node & {
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
@@ -1771,15 +1782,6 @@ export type OrderList = PaginatedList & {
     totalItems: Scalars['Int'];
 };
 
-export type ShippingMethodQuote = {
-    id: Scalars['ID'];
-    price: Scalars['Int'];
-    priceWithTax: Scalars['Int'];
-    name: Scalars['String'];
-    description: Scalars['String'];
-    metadata?: Maybe<Scalars['JSON']>;
-};
-
 export type ShippingLine = {
     shippingMethod: ShippingMethod;
     price: Scalars['Int'];
@@ -2241,7 +2243,7 @@ export type OrderModificationError = ErrorResult & {
     message: Scalars['String'];
 };
 
-/** Returned when attempting to set a ShippingMethod for which the order is not eligible */
+/** Returned when attempting to set a ShippingMethod for which the Order is not eligible */
 export type IneligibleShippingMethodError = ErrorResult & {
     errorCode: ErrorCode;
     message: Scalars['String'];
@@ -2253,6 +2255,13 @@ export type OrderPaymentStateError = ErrorResult & {
     message: Scalars['String'];
 };
 
+/** Returned when attempting to add a Payment using a PaymentMethod for which the Order is not eligible. */
+export type IneligiblePaymentMethodError = ErrorResult & {
+    errorCode: ErrorCode;
+    message: Scalars['String'];
+    eligibilityCheckerMessage?: Maybe<Scalars['String']>;
+};
+
 /** Returned when a Payment fails due to an error. */
 export type PaymentFailedError = ErrorResult & {
     errorCode: ErrorCode;
@@ -2432,6 +2441,7 @@ export type ApplyCouponCodeResult =
 export type AddPaymentToOrderResult =
     | Order
     | OrderPaymentStateError
+    | IneligiblePaymentMethodError
     | PaymentFailedError
     | PaymentDeclinedError
     | OrderStateTransitionError
@@ -3071,6 +3081,7 @@ export type AddPaymentToOrderMutation = {
     addPaymentToOrder:
         | TestOrderWithPaymentsFragment
         | Pick<OrderPaymentStateError, 'errorCode' | 'message'>
+        | Pick<IneligiblePaymentMethodError, 'errorCode' | 'message' | 'eligibilityCheckerMessage'>
         | Pick<PaymentFailedError, 'errorCode' | 'message' | 'paymentErrorMessage'>
         | Pick<PaymentDeclinedError, 'errorCode' | 'message' | 'paymentErrorMessage'>
         | Pick<OrderStateTransitionError, 'errorCode' | 'message' | 'transitionError'>
@@ -3573,6 +3584,10 @@ export namespace AddPaymentToOrder {
         NonNullable<AddPaymentToOrderMutation['addPaymentToOrder']>,
         { __typename?: 'OrderStateTransitionError' }
     >;
+    export type IneligiblePaymentMethodErrorInlineFragment = DiscriminateUnion<
+        NonNullable<AddPaymentToOrderMutation['addPaymentToOrder']>,
+        { __typename?: 'IneligiblePaymentMethodError' }
+    >;
 }
 
 export namespace GetActiveOrderPayments {

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

@@ -568,6 +568,9 @@ export const ADD_PAYMENT = gql`
             ... on OrderStateTransitionError {
                 transitionError
             }
+            ... on IneligiblePaymentMethodError {
+                eligibilityCheckerMessage
+            }
         }
     }
     ${TEST_ORDER_WITH_PAYMENTS_FRAGMENT}

+ 152 - 9
packages/core/e2e/payment-method.e2e-spec.ts

@@ -1,17 +1,61 @@
-import { dummyPaymentHandler } from '@vendure/core';
-import { createTestEnvironment } from '@vendure/testing';
+import {
+    DefaultLogger,
+    dummyPaymentHandler,
+    LanguageCode,
+    PaymentMethodEligibilityChecker,
+} from '@vendure/core';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
 import path from 'path';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 
-import { CreatePaymentMethod, UpdatePaymentMethod } from './graphql/generated-e2e-admin-types';
+import {
+    CreatePaymentMethod,
+    GetPaymentMethodCheckers,
+    GetPaymentMethodHandlers,
+    UpdatePaymentMethod,
+} from './graphql/generated-e2e-admin-types';
+import {
+    AddItemToOrder,
+    AddPaymentToOrder,
+    ErrorCode,
+    TestOrderWithPaymentsFragment,
+} from './graphql/generated-e2e-shop-types';
+import { ADD_ITEM_TO_ORDER, ADD_PAYMENT } from './graphql/shop-definitions';
+import { proceedToArrangingPayment } from './utils/test-order-utils';
+
+const checkerSpy = jest.fn();
+
+const minPriceChecker = new PaymentMethodEligibilityChecker({
+    code: 'min-price-checker',
+    description: [{ languageCode: LanguageCode.en, value: 'Min price checker' }],
+    args: {
+        minPrice: {
+            type: 'int',
+        },
+    },
+    check(ctx, order, args) {
+        checkerSpy();
+        if (order.totalWithTax >= args.minPrice) {
+            return true;
+        } else {
+            return `Order total too low`;
+        }
+    },
+});
 
 describe('PaymentMethod resolver', () => {
+    const orderGuard: ErrorResultGuard<TestOrderWithPaymentsFragment> = createErrorResultGuard(
+        input => !!input.lines,
+    );
+
     const { server, adminClient, shopClient } = createTestEnvironment({
         ...testConfig,
+        logger: new DefaultLogger(),
         paymentOptions: {
+            paymentMethodEligibilityCheckers: [minPriceChecker],
             paymentMethodHandlers: [dummyPaymentHandler],
         },
     });
@@ -35,8 +79,8 @@ describe('PaymentMethod resolver', () => {
             CreatePaymentMethod.Variables
         >(CREATE_PAYMENT_METHOD, {
             input: {
-                code: 'test-method',
-                name: 'Test Method',
+                code: 'no-checks',
+                name: 'No Checker',
                 description: 'This is a test payment method',
                 enabled: true,
                 handler: {
@@ -48,8 +92,8 @@ describe('PaymentMethod resolver', () => {
 
         expect(createPaymentMethod).toEqual({
             id: 'T_1',
-            name: 'Test Method',
-            code: 'test-method',
+            name: 'No Checker',
+            code: 'no-checks',
             description: 'This is a test payment method',
             enabled: true,
             handler: {
@@ -82,8 +126,8 @@ describe('PaymentMethod resolver', () => {
 
         expect(updatePaymentMethod).toEqual({
             id: 'T_1',
-            name: 'Test Method',
-            code: 'test-method',
+            name: 'No Checker',
+            code: 'no-checks',
             description: 'modified',
             enabled: false,
             handler: {
@@ -97,6 +141,81 @@ describe('PaymentMethod resolver', () => {
             },
         });
     });
+
+    it('paymentMethodEligibilityCheckers', async () => {
+        const { paymentMethodEligibilityCheckers } = await adminClient.query<GetPaymentMethodCheckers.Query>(
+            GET_PAYMENT_METHOD_CHECKERS,
+        );
+        expect(paymentMethodEligibilityCheckers).toEqual([
+            {
+                code: minPriceChecker.code,
+                args: [{ name: 'minPrice', type: 'int' }],
+            },
+        ]);
+    });
+
+    it('paymentMethodHandlers', async () => {
+        const { paymentMethodHandlers } = await adminClient.query<GetPaymentMethodHandlers.Query>(
+            GET_PAYMENT_METHOD_HANDLERS,
+        );
+        expect(paymentMethodHandlers).toEqual([
+            {
+                code: dummyPaymentHandler.code,
+                args: [{ name: 'automaticSettle', type: 'boolean' }],
+            },
+        ]);
+    });
+
+    describe('eligibility checks', () => {
+        beforeAll(async () => {
+            const { createPaymentMethod } = await adminClient.query<
+                CreatePaymentMethod.Mutation,
+                CreatePaymentMethod.Variables
+            >(CREATE_PAYMENT_METHOD, {
+                input: {
+                    code: 'price-check',
+                    name: 'With Min Price Checker',
+                    description: 'Order total must be more than 2k',
+                    enabled: true,
+                    checker: {
+                        code: minPriceChecker.code,
+                        arguments: [{ name: 'minPrice', value: '200000' }],
+                    },
+                    handler: {
+                        code: dummyPaymentHandler.code,
+                        arguments: [{ name: 'automaticSettle', value: 'true' }],
+                    },
+                },
+            });
+
+            await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
+            await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
+                productVariantId: 'T_1',
+                quantity: 1,
+            });
+
+            await proceedToArrangingPayment(shopClient);
+        });
+
+        it('addPaymentToOrder does not allow ineligible method', async () => {
+            checkerSpy.mockClear();
+            const { addPaymentToOrder } = await shopClient.query<
+                AddPaymentToOrder.Mutation,
+                AddPaymentToOrder.Variables
+            >(ADD_PAYMENT, {
+                input: {
+                    method: 'price-check',
+                    metadata: {},
+                },
+            });
+
+            orderGuard.assertErrorResult(addPaymentToOrder);
+
+            expect(addPaymentToOrder.errorCode).toBe(ErrorCode.INELIGIBLE_PAYMENT_METHOD_ERROR);
+            expect(addPaymentToOrder.eligibilityCheckerMessage).toBe('Order total too low');
+            expect(checkerSpy).toHaveBeenCalledTimes(1);
+        });
+    });
 });
 
 export const PAYMENT_METHOD_FRAGMENT = gql`
@@ -133,3 +252,27 @@ export const UPDATE_PAYMENT_METHOD = gql`
     }
     ${PAYMENT_METHOD_FRAGMENT}
 `;
+
+export const GET_PAYMENT_METHOD_HANDLERS = gql`
+    query GetPaymentMethodHandlers {
+        paymentMethodHandlers {
+            code
+            args {
+                name
+                type
+            }
+        }
+    }
+`;
+
+export const GET_PAYMENT_METHOD_CHECKERS = gql`
+    query GetPaymentMethodCheckers {
+        paymentMethodEligibilityCheckers {
+            code
+            args {
+                name
+                type
+            }
+        }
+    }
+`;

+ 3 - 0
packages/core/src/api/common/configurable-operation-codec.ts

@@ -13,6 +13,7 @@ import {
 } from '../../config';
 import { CollectionFilter } from '../../config/catalog/collection-filter';
 import { ConfigService } from '../../config/config.service';
+import { PaymentMethodEligibilityChecker } from '../../config/payment-method/payment-method-eligibility-checker';
 import { PaymentMethodHandler } from '../../config/payment-method/payment-method-handler';
 
 import { IdCodecService } from './id-codec.service';
@@ -87,6 +88,8 @@ export class ConfigurableOperationCodec {
                 return this.configService.catalogOptions.collectionFilters;
             case PaymentMethodHandler:
                 return this.configService.paymentOptions.paymentMethodHandlers;
+            case PaymentMethodEligibilityChecker:
+                return this.configService.paymentOptions.paymentMethodEligibilityCheckers || [];
             case PromotionItemAction:
             case PromotionOrderAction:
                 return this.configService.promotionOptions.promotionActions || [];

+ 13 - 0
packages/core/src/api/resolvers/admin/payment-method.resolver.ts

@@ -1,5 +1,6 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
+    ConfigurableOperationDefinition,
     MutationCreatePaymentMethodArgs,
     MutationUpdatePaymentMethodArgs,
     Permission,
@@ -56,4 +57,16 @@ export class PaymentMethodResolver {
     ): Promise<PaymentMethod> {
         return this.paymentMethodService.update(ctx, args.input);
     }
+
+    @Query()
+    @Allow(Permission.ReadSettings)
+    paymentMethodHandlers(@Ctx() ctx: RequestContext): ConfigurableOperationDefinition[] {
+        return this.paymentMethodService.getPaymentMethodHandlers(ctx);
+    }
+
+    @Query()
+    @Allow(Permission.ReadSettings)
+    paymentMethodEligibilityCheckers(@Ctx() ctx: RequestContext): ConfigurableOperationDefinition[] {
+        return this.paymentMethodService.getPaymentMethodEligibilityCheckers(ctx);
+    }
 }

+ 3 - 0
packages/core/src/api/schema/admin-api/payment-method.api.graphql

@@ -1,6 +1,7 @@
 type Query {
     paymentMethods(options: PaymentMethodListOptions): PaymentMethodList!
     paymentMethod(id: ID!): PaymentMethod
+    paymentMethodEligibilityCheckers: [ConfigurableOperationDefinition!]!
     paymentMethodHandlers: [ConfigurableOperationDefinition!]!
 }
 
@@ -24,6 +25,7 @@ input CreatePaymentMethodInput {
     code: String!
     description: String
     enabled: Boolean!
+    checker: ConfigurableOperationInput
     handler: ConfigurableOperationInput!
 }
 
@@ -33,5 +35,6 @@ input UpdatePaymentMethodInput {
     code: String
     description: String
     enabled: Boolean
+    checker: ConfigurableOperationInput
     handler: ConfigurableOperationInput
 }

+ 1 - 0
packages/core/src/api/schema/admin-api/payment-method.type.graphql

@@ -6,5 +6,6 @@ type PaymentMethod implements Node {
     code: String!
     description: String!
     enabled: Boolean!
+    checker: ConfigurableOperation
     handler: ConfigurableOperation!
 }

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

@@ -173,3 +173,13 @@ Indicates that an operation succeeded, where we do not want to return any more s
 type Success {
     success: Boolean!
 }
+
+type ShippingMethodQuote {
+    id: ID!
+    price: Int!
+    priceWithTax: Int!
+    name: String!
+    description: String!
+    "Any optional metadata returned by the ShippingCalculator in the ShippingCalculationResult"
+    metadata: JSON
+}

+ 0 - 9
packages/core/src/api/schema/common/order.type.graphql

@@ -94,15 +94,6 @@ type OrderList implements PaginatedList {
     totalItems: Int!
 }
 
-type ShippingMethodQuote {
-    id: ID!
-    price: Int!
-    priceWithTax: Int!
-    name: String!
-    description: String!
-    metadata: JSON
-}
-
 type ShippingLine {
     shippingMethod: ShippingMethod!
     price: Int!

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

@@ -4,7 +4,7 @@ type OrderModificationError implements ErrorResult {
     message: String!
 }
 
-"Returned when attempting to set a ShippingMethod for which the order is not eligible"
+"Returned when attempting to set a ShippingMethod for which the Order is not eligible"
 type IneligibleShippingMethodError implements ErrorResult {
     errorCode: ErrorCode!
     message: String!
@@ -16,6 +16,13 @@ type OrderPaymentStateError implements ErrorResult {
     message: String!
 }
 
+"Returned when attempting to add a Payment using a PaymentMethod for which the Order is not eligible."
+type IneligiblePaymentMethodError implements ErrorResult {
+    errorCode: ErrorCode!
+    message: String!
+    eligibilityCheckerMessage: String
+}
+
 "Returned when a Payment fails due to an error."
 type PaymentFailedError implements ErrorResult {
     errorCode: ErrorCode!

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

@@ -194,6 +194,7 @@ union ApplyCouponCodeResult = Order | CouponCodeExpiredError | CouponCodeInvalid
 union AddPaymentToOrderResult =
       Order
     | OrderPaymentStateError
+    | IneligiblePaymentMethodError
     | PaymentFailedError
     | PaymentDeclinedError
     | OrderStateTransitionError

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

@@ -125,6 +125,17 @@ export class OrderPaymentStateError extends ErrorResult {
   }
 }
 
+export class IneligiblePaymentMethodError extends ErrorResult {
+  readonly __typename = 'IneligiblePaymentMethodError';
+  readonly errorCode = 'INELIGIBLE_PAYMENT_METHOD_ERROR' as any;
+  readonly message = 'INELIGIBLE_PAYMENT_METHOD_ERROR';
+  constructor(
+    public eligibilityCheckerMessage: any,
+  ) {
+    super();
+  }
+}
+
 export class PaymentFailedError extends ErrorResult {
   readonly __typename = 'PaymentFailedError';
   readonly errorCode = 'PAYMENT_FAILED_ERROR' as any;
@@ -292,7 +303,7 @@ export class NoActiveOrderError extends ErrorResult {
 }
 
 
-const errorTypeNames = new Set(['NativeAuthStrategyError', 'InvalidCredentialsError', 'OrderStateTransitionError', 'EmailAddressConflictError', 'OrderLimitError', 'NegativeQuantityError', 'InsufficientStockError', 'OrderModificationError', 'IneligibleShippingMethodError', 'OrderPaymentStateError', 'PaymentFailedError', 'PaymentDeclinedError', 'CouponCodeInvalidError', 'CouponCodeExpiredError', 'CouponCodeLimitError', 'AlreadyLoggedInError', 'MissingPasswordError', 'PasswordAlreadySetError', 'VerificationTokenInvalidError', 'VerificationTokenExpiredError', 'IdentifierChangeTokenInvalidError', 'IdentifierChangeTokenExpiredError', 'PasswordResetTokenInvalidError', 'PasswordResetTokenExpiredError', 'NotVerifiedError', 'NoActiveOrderError']);
+const errorTypeNames = new Set(['NativeAuthStrategyError', 'InvalidCredentialsError', 'OrderStateTransitionError', 'EmailAddressConflictError', 'OrderLimitError', 'NegativeQuantityError', 'InsufficientStockError', 'OrderModificationError', 'IneligibleShippingMethodError', 'OrderPaymentStateError', 'IneligiblePaymentMethodError', 'PaymentFailedError', 'PaymentDeclinedError', 'CouponCodeInvalidError', 'CouponCodeExpiredError', 'CouponCodeLimitError', 'AlreadyLoggedInError', 'MissingPasswordError', 'PasswordAlreadySetError', 'VerificationTokenInvalidError', 'VerificationTokenExpiredError', 'IdentifierChangeTokenInvalidError', 'IdentifierChangeTokenExpiredError', 'PasswordResetTokenInvalidError', 'PasswordResetTokenExpiredError', 'NotVerifiedError', 'NoActiveOrderError']);
 function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').ErrorResult {
   return input instanceof ErrorResult || errorTypeNames.has(input.__typename);
 }

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

@@ -113,7 +113,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
     }
 
     private getConfigurableOperations(): Array<ConfigurableOperationDef<any>> {
-        const { paymentMethodHandlers } = this.configService.paymentOptions;
+        const { paymentMethodHandlers, paymentMethodEligibilityCheckers } = this.configService.paymentOptions;
         const { collectionFilters } = this.configService.catalogOptions;
         const { promotionActions, promotionConditions } = this.configService.promotionOptions;
         const {
@@ -122,6 +122,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             fulfillmentHandlers,
         } = this.configService.shippingOptions;
         return [
+            ...(paymentMethodEligibilityCheckers || []),
             ...paymentMethodHandlers,
             ...collectionFilters,
             ...(promotionActions || []),

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

@@ -115,6 +115,7 @@ export const defaultConfig: RuntimeVendureConfig = {
         orderCodeStrategy: new DefaultOrderCodeStrategy(),
     },
     paymentOptions: {
+        paymentMethodEligibilityCheckers: [],
         paymentMethodHandlers: [],
     },
     taxOptions: {

+ 1 - 1
packages/core/src/config/index.ts

@@ -28,6 +28,7 @@ export * from './order/stock-allocation-strategy';
 export * from './payment-method/dummy-payment-method-handler';
 export * from './payment-method/example-payment-method-handler';
 export * from './payment-method/payment-method-handler';
+export * from './payment-method/payment-method-eligibility-checker';
 export * from './promotion';
 export * from './session-cache/in-memory-session-cache-strategy';
 export * from './session-cache/noop-session-cache-strategy';
@@ -37,4 +38,3 @@ export * from './shipping-method/default-shipping-eligibility-checker';
 export * from './shipping-method/shipping-calculator';
 export * from './shipping-method/shipping-eligibility-checker';
 export * from './vendure-config';
-export { PriceCalculationResult } from '../common/types/common-types';

+ 79 - 0
packages/core/src/config/payment-method/payment-method-eligibility-checker.ts

@@ -0,0 +1,79 @@
+import { ConfigArg } from '@vendure/common/lib/generated-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import {
+    ConfigArgs,
+    ConfigArgValues,
+    ConfigurableOperationDef,
+    ConfigurableOperationDefOptions,
+} from '../../common/configurable-operation';
+import { Order } from '../../entity/order/order.entity';
+
+/**
+ * @description
+ * Configuration passed into the constructor of a {@link PaymentMethodEligibilityChecker} to
+ * configure its behavior.
+ *
+ * @docsCategory PaymentMethodEligibilityChecker
+ */
+export interface PaymentMethodEligibilityCheckerConfig<T extends ConfigArgs>
+    extends ConfigurableOperationDefOptions<T> {
+    check: CheckPaymentMethodEligibilityCheckerFn<T>;
+}
+/**
+ * @description
+ * The PaymentMethodEligibilityChecker class is used to check whether an order qualifies for a
+ * given {@link PaymentMethod}.
+ *
+ * @example
+ * ```ts
+ * const ccPaymentEligibilityChecker = new PaymentMethodEligibilityChecker({
+ *     code: 'order-total-payment-eligibility-checker',
+ *     description: [{ languageCode: LanguageCode.en, value: 'Checks that the order total is above some minimum value' }],
+ *     args: {
+ *         orderMinimum: { type: 'int', ui: { component: 'currency-form-input' } },
+ *     },
+ *     check: (ctx, order, args) => {
+ *         return order.totalWithTax >= args.orderMinimum;
+ *     },
+ * });
+ * ```
+ *
+ * @docsCategory paymentMethod
+ * @docsPage PaymentMethodEligibilityChecker
+ */
+export class PaymentMethodEligibilityChecker<
+    T extends ConfigArgs = ConfigArgs
+> extends ConfigurableOperationDef<T> {
+    private readonly checkFn: CheckPaymentMethodEligibilityCheckerFn<T>;
+
+    constructor(config: PaymentMethodEligibilityCheckerConfig<T>) {
+        super(config);
+        this.checkFn = config.check;
+    }
+
+    /**
+     * @description
+     * Check the given Order to determine whether it is eligible.
+     *
+     * @internal
+     */
+    async check(ctx: RequestContext, order: Order, args: ConfigArg[]): Promise<boolean | string> {
+        return this.checkFn(ctx, order, this.argsArrayToHash(args));
+    }
+}
+
+/**
+ * @description
+ * A function which implements logic to determine whether a given {@link Order} is eligible for
+ * a particular payment method. If the function resolves to `false` or a string, the check is
+ * considered to have failed. A string result can be used to provide information about the
+ * reason for ineligibility, if desired.
+ *
+ * @docsCategory PaymentMethodEligibilityChecker
+ */
+export type CheckPaymentMethodEligibilityCheckerFn<T extends ConfigArgs> = (
+    ctx: RequestContext,
+    order: Order,
+    args: ConfigArgValues<T>,
+) => boolean | string | Promise<boolean | string>;

+ 3 - 3
packages/core/src/config/shipping-method/shipping-eligibility-checker.ts

@@ -35,10 +35,10 @@ export interface ShippingEligibilityCheckerConfig<T extends ConfigArgs>
  *     code: 'min-order-total-eligibility-checker',
  *     description: [{ languageCode: LanguageCode.en, value: 'Checks that the order total is above some minimum value' }],
  *     args: {
- *         orderMinimum: { type: 'int', config: { inputType: 'money' } },
+ *         orderMinimum: { type: 'int', ui: { component: 'currency-form-input' } },
  *     },
- *     check: (order, args) => {
- *         return order.total >= args.orderMinimum;
+ *     check: (ctx, order, args) => {
+ *         return order.totalWithTax >= args.orderMinimum;
  *     },
  * });
  * ```

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

@@ -25,6 +25,7 @@ import { OrderCodeStrategy } from './order/order-code-strategy';
 import { OrderItemPriceCalculationStrategy } from './order/order-item-price-calculation-strategy';
 import { OrderMergeStrategy } from './order/order-merge-strategy';
 import { StockAllocationStrategy } from './order/stock-allocation-strategy';
+import { PaymentMethodEligibilityChecker } from './payment-method/payment-method-eligibility-checker';
 import { PaymentMethodHandler } from './payment-method/payment-method-handler';
 import { PromotionAction } from './promotion/promotion-action';
 import { PromotionCondition } from './promotion/promotion-condition';
@@ -584,9 +585,16 @@ export interface SuperadminCredentials {
 export interface PaymentOptions {
     /**
      * @description
-     * An array of {@link PaymentMethodHandler}s with which to process payments.
+     * Defines which {@link PaymentMethodHandler}s are available when configuring
+     * {@link PaymentMethod}s
      */
     paymentMethodHandlers: PaymentMethodHandler[];
+    /**
+     * @description
+     * Defines which {@link PaymentMethodEligibilityChecker}s are available when configuring
+     * {@link PaymentMethod}s
+     */
+    paymentMethodEligibilityCheckers?: PaymentMethodEligibilityChecker[];
 }
 
 /**

+ 2 - 0
packages/core/src/entity/payment-method/payment-method.entity.ts

@@ -25,5 +25,7 @@ export class PaymentMethod extends VendureEntity {
 
     @Column() enabled: boolean;
 
+    @Column('simple-json', { nullable: true }) checker?: ConfigurableOperation;
+
     @Column('simple-json') handler: ConfigurableOperation;
 }

+ 4 - 0
packages/core/src/service/helpers/config-arg/config-arg.service.ts

@@ -8,6 +8,7 @@ import { UserInputError } from '../../../common/error/errors';
 import { CollectionFilter } from '../../../config/catalog/collection-filter';
 import { ConfigService } from '../../../config/config.service';
 import { FulfillmentHandler } from '../../../config/fulfillment/fulfillment-handler';
+import { PaymentMethodEligibilityChecker } from '../../../config/payment-method/payment-method-eligibility-checker';
 import { PaymentMethodHandler } from '../../../config/payment-method/payment-method-handler';
 import { PromotionAction } from '../../../config/promotion/promotion-action';
 import { PromotionCondition } from '../../../config/promotion/promotion-condition';
@@ -17,6 +18,7 @@ import { ShippingEligibilityChecker } from '../../../config/shipping-method/ship
 export type ConfigDefTypeMap = {
     CollectionFilter: CollectionFilter;
     FulfillmentHandler: FulfillmentHandler;
+    PaymentMethodEligibilityChecker: PaymentMethodEligibilityChecker;
     PaymentMethodHandler: PaymentMethodHandler;
     PromotionAction: PromotionAction;
     PromotionCondition: PromotionCondition;
@@ -37,6 +39,8 @@ export class ConfigArgService {
         this.definitionsByType = {
             CollectionFilter: this.configService.catalogOptions.collectionFilters,
             FulfillmentHandler: this.configService.shippingOptions.fulfillmentHandlers,
+            PaymentMethodEligibilityChecker:
+                this.configService.paymentOptions.paymentMethodEligibilityCheckers || [],
             PaymentMethodHandler: this.configService.paymentOptions.paymentMethodHandlers,
             PromotionAction: this.configService.promotionOptions.promotionActions,
             PromotionCondition: this.configService.promotionOptions.promotionConditions,

+ 4 - 0
packages/core/src/service/services/order.service.ts

@@ -747,6 +747,10 @@ export class OrderService {
             input.metadata,
         );
 
+        if (isGraphQlErrorResult(payment)) {
+            return payment;
+        }
+
         const existingPayments = await this.getOrderPayments(ctx, orderId);
         order.payments = [...existingPayments, payment];
         await this.connection.getRepository(ctx, Order).save(order, { reload: false });

+ 46 - 8
packages/core/src/service/services/payment-method.service.ts

@@ -1,5 +1,6 @@
 import { Injectable } from '@nestjs/common';
 import {
+    ConfigurableOperationDefinition,
     CreatePaymentMethodInput,
     ManualPaymentInput,
     RefundOrderInput,
@@ -12,8 +13,10 @@ import { summate } from '@vendure/common/lib/shared-utils';
 import { RequestContext } from '../../api/common/request-context';
 import { UserInputError } from '../../common/error/errors';
 import { RefundStateTransitionError } from '../../common/error/generated-graphql-admin-errors';
+import { IneligiblePaymentMethodError } from '../../common/error/generated-graphql-shop-errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ConfigService } from '../../config/config.service';
+import { PaymentMethodEligibilityChecker } from '../../config/payment-method/payment-method-eligibility-checker';
 import { PaymentMethodHandler } from '../../config/payment-method/payment-method-handler';
 import { OrderItem } from '../../entity/order-item/order-item.entity';
 import { Order } from '../../entity/order/order.entity';
@@ -62,26 +65,54 @@ export class PaymentMethodService {
     async create(ctx: RequestContext, input: CreatePaymentMethodInput): Promise<PaymentMethod> {
         const paymentMethod = new PaymentMethod(input);
         paymentMethod.handler = this.configArgService.parseInput('PaymentMethodHandler', input.handler);
+        if (input.checker) {
+            paymentMethod.checker = this.configArgService.parseInput(
+                'PaymentMethodEligibilityChecker',
+                input.checker,
+            );
+        }
         return this.connection.getRepository(ctx, PaymentMethod).save(paymentMethod);
     }
 
     async update(ctx: RequestContext, input: UpdatePaymentMethodInput): Promise<PaymentMethod> {
         const paymentMethod = await this.connection.getEntityOrThrow(ctx, PaymentMethod, input.id);
-        const updatedPaymentMethod = patchEntity(paymentMethod, omit(input, ['handler']));
+        const updatedPaymentMethod = patchEntity(paymentMethod, omit(input, ['handler', 'checker']));
+        if (input.checker) {
+            paymentMethod.handler = this.configArgService.parseInput(
+                'PaymentMethodEligibilityChecker',
+                input.checker,
+            );
+        }
         if (input.handler) {
             paymentMethod.handler = this.configArgService.parseInput('PaymentMethodHandler', input.handler);
         }
         return this.connection.getRepository(ctx, PaymentMethod).save(updatedPaymentMethod);
     }
 
+    getPaymentMethodEligibilityCheckers(ctx: RequestContext): ConfigurableOperationDefinition[] {
+        return this.configArgService
+            .getDefinitions('PaymentMethodEligibilityChecker')
+            .map(x => x.toGraphQlType(ctx));
+    }
+
+    getPaymentMethodHandlers(ctx: RequestContext): ConfigurableOperationDefinition[] {
+        return this.configArgService.getDefinitions('PaymentMethodHandler').map(x => x.toGraphQlType(ctx));
+    }
+
     async createPayment(
         ctx: RequestContext,
         order: Order,
         amount: number,
         method: string,
         metadata: any,
-    ): Promise<Payment> {
-        const { paymentMethod, handler } = await this.getMethodAndHandler(ctx, method);
+    ): Promise<Payment | IneligiblePaymentMethodError> {
+        const { paymentMethod, handler, checker } = await this.getMethodAndOperations(ctx, method);
+        if (paymentMethod.checker && checker) {
+            const eligible = await checker.check(ctx, order, paymentMethod.checker.args);
+            if (eligible === false || typeof eligible === 'string') {
+                return new IneligiblePaymentMethodError(typeof eligible === 'string' ? eligible : undefined);
+            }
+        }
         const result = await handler.createPayment(
             ctx,
             order,
@@ -124,7 +155,7 @@ export class PaymentMethodService {
     }
 
     async settlePayment(ctx: RequestContext, payment: Payment, order: Order) {
-        const { paymentMethod, handler } = await this.getMethodAndHandler(ctx, payment.method);
+        const { paymentMethod, handler } = await this.getMethodAndOperations(ctx, payment.method);
         return handler.settlePayment(ctx, order, payment, paymentMethod.handler.args);
     }
 
@@ -135,7 +166,7 @@ export class PaymentMethodService {
         items: OrderItem[],
         payment: Payment,
     ): Promise<Refund | RefundStateTransitionError> {
-        const { paymentMethod, handler } = await this.getMethodAndHandler(ctx, payment.method);
+        const { paymentMethod, handler } = await this.getMethodAndOperations(ctx, payment.method);
         const itemAmount = summate(items, 'proratedUnitPriceWithTax');
         const refundAmount = itemAmount + input.shipping + input.adjustment;
         let refund = new Refund({
@@ -178,10 +209,14 @@ export class PaymentMethodService {
         return refund;
     }
 
-    private async getMethodAndHandler(
+    private async getMethodAndOperations(
         ctx: RequestContext,
         method: string,
-    ): Promise<{ paymentMethod: PaymentMethod; handler: PaymentMethodHandler }> {
+    ): Promise<{
+        paymentMethod: PaymentMethod;
+        handler: PaymentMethodHandler;
+        checker: PaymentMethodEligibilityChecker | undefined;
+    }> {
         const paymentMethod = await this.connection.getRepository(ctx, PaymentMethod).findOne({
             where: {
                 code: method,
@@ -192,6 +227,9 @@ export class PaymentMethodService {
             throw new UserInputError(`error.payment-method-not-found`, { method });
         }
         const handler = this.configArgService.getByCode('PaymentMethodHandler', paymentMethod.handler.code);
-        return { paymentMethod, handler };
+        const checker =
+            paymentMethod.checker &&
+            this.configArgService.getByCode('PaymentMethodEligibilityChecker', paymentMethod.checker.code);
+        return { paymentMethod, handler, checker };
     }
 }

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

@@ -49,6 +49,7 @@ export type Query = {
     orders: OrderList;
     paymentMethods: PaymentMethodList;
     paymentMethod?: Maybe<PaymentMethod>;
+    paymentMethodEligibilityCheckers: Array<ConfigurableOperationDefinition>;
     paymentMethodHandlers: Array<ConfigurableOperationDefinition>;
     productOptionGroups: Array<ProductOptionGroup>;
     productOptionGroup?: Maybe<ProductOptionGroup>;
@@ -1735,6 +1736,7 @@ export type CreatePaymentMethodInput = {
     code: Scalars['String'];
     description?: Maybe<Scalars['String']>;
     enabled: Scalars['Boolean'];
+    checker?: Maybe<ConfigurableOperationInput>;
     handler: ConfigurableOperationInput;
 };
 
@@ -1744,6 +1746,7 @@ export type UpdatePaymentMethodInput = {
     code?: Maybe<Scalars['String']>;
     description?: Maybe<Scalars['String']>;
     enabled?: Maybe<Scalars['Boolean']>;
+    checker?: Maybe<ConfigurableOperationInput>;
     handler?: Maybe<ConfigurableOperationInput>;
 };
 
@@ -1755,6 +1758,7 @@ export type PaymentMethod = Node & {
     code: Scalars['String'];
     description: Scalars['String'];
     enabled: Scalars['Boolean'];
+    checker?: Maybe<ConfigurableOperation>;
     handler: ConfigurableOperation;
 };
 
@@ -2679,6 +2683,16 @@ export type Success = {
     success: Scalars['Boolean'];
 };
 
+export type ShippingMethodQuote = {
+    id: Scalars['ID'];
+    price: Scalars['Int'];
+    priceWithTax: Scalars['Int'];
+    name: Scalars['String'];
+    description: Scalars['String'];
+    /** Any optional metadata returned by the ShippingCalculator in the ShippingCalculationResult */
+    metadata?: Maybe<Scalars['JSON']>;
+};
+
 export type Country = Node & {
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
@@ -3587,15 +3601,6 @@ export type OrderList = PaginatedList & {
     totalItems: Scalars['Int'];
 };
 
-export type ShippingMethodQuote = {
-    id: Scalars['ID'];
-    price: Scalars['Int'];
-    priceWithTax: Scalars['Int'];
-    name: Scalars['String'];
-    description: Scalars['String'];
-    metadata?: Maybe<Scalars['JSON']>;
-};
-
 export type ShippingLine = {
     shippingMethod: ShippingMethod;
     price: Scalars['Int'];

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


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