Przeglądaj źródła

feat(core): Make PaymentMethod channel-aware

Relates to #587

BREAKING CHANGE: The PaymentMethod entity is now channel-aware which will require a DB migration
to migrate existing PaymentMethods
Michael Bromley 4 lat temu
rodzic
commit
1a3b04f4a5

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

@@ -6146,6 +6146,14 @@ export type GetPaymentMethodQueryVariables = Exact<{
 
 export type GetPaymentMethodQuery = { paymentMethod?: Maybe<PaymentMethodFragment> };
 
+export type GetPaymentMethodListQueryVariables = Exact<{
+    options?: Maybe<PaymentMethodListOptions>;
+}>;
+
+export type GetPaymentMethodListQuery = {
+    paymentMethods: Pick<PaymentMethodList, 'totalItems'> & { items: Array<PaymentMethodFragment> };
+};
+
 export type TransitionPaymentToStateMutationVariables = Exact<{
     id: Scalars['ID'];
     state: Scalars['String'];
@@ -8312,6 +8320,15 @@ export namespace GetPaymentMethod {
     export type PaymentMethod = NonNullable<GetPaymentMethodQuery['paymentMethod']>;
 }
 
+export namespace GetPaymentMethodList {
+    export type Variables = GetPaymentMethodListQueryVariables;
+    export type Query = GetPaymentMethodListQuery;
+    export type PaymentMethods = NonNullable<GetPaymentMethodListQuery['paymentMethods']>;
+    export type Items = NonNullable<
+        NonNullable<NonNullable<GetPaymentMethodListQuery['paymentMethods']>['items']>[number]
+    >;
+}
+
 export namespace TransitionPaymentToState {
     export type Variables = TransitionPaymentToStateMutationVariables;
     export type Mutation = TransitionPaymentToStateMutation;

+ 107 - 1
packages/core/e2e/payment-method.e2e-spec.ts

@@ -4,7 +4,12 @@ import {
     LanguageCode,
     PaymentMethodEligibilityChecker,
 } from '@vendure/core';
-import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
+import {
+    createErrorResultGuard,
+    createTestEnvironment,
+    E2E_DEFAULT_CHANNEL_TOKEN,
+    ErrorResultGuard,
+} from '@vendure/testing';
 import gql from 'graphql-tag';
 import path from 'path';
 
@@ -12,10 +17,13 @@ import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 
 import {
+    CreateChannel,
     CreatePaymentMethod,
+    CurrencyCode,
     GetPaymentMethod,
     GetPaymentMethodCheckers,
     GetPaymentMethodHandlers,
+    GetPaymentMethodList,
     UpdatePaymentMethod,
 } from './graphql/generated-e2e-admin-types';
 import {
@@ -25,6 +33,7 @@ import {
     GetEligiblePaymentMethods,
     TestOrderWithPaymentsFragment,
 } from './graphql/generated-e2e-shop-types';
+import { CREATE_CHANNEL } from './graphql/shared-definitions';
 import { ADD_ITEM_TO_ORDER, ADD_PAYMENT, GET_ELIGIBLE_PAYMENT_METHODS } from './graphql/shop-definitions';
 import { proceedToArrangingPayment } from './utils/test-order-utils';
 
@@ -281,6 +290,91 @@ describe('PaymentMethod resolver', () => {
             expect(checkerSpy).toHaveBeenCalledTimes(1);
         });
     });
+
+    describe('channels', () => {
+        const SECOND_CHANNEL_TOKEN = 'SECOND_CHANNEL_TOKEN';
+        const THIRD_CHANNEL_TOKEN = 'THIRD_CHANNEL_TOKEN';
+
+        beforeAll(async () => {
+            await adminClient.query<CreateChannel.Mutation, CreateChannel.Variables>(CREATE_CHANNEL, {
+                input: {
+                    code: 'second-channel',
+                    token: SECOND_CHANNEL_TOKEN,
+                    defaultLanguageCode: LanguageCode.en,
+                    currencyCode: CurrencyCode.GBP,
+                    pricesIncludeTax: true,
+                    defaultShippingZoneId: 'T_1',
+                    defaultTaxZoneId: 'T_1',
+                },
+            });
+            await adminClient.query<CreateChannel.Mutation, CreateChannel.Variables>(CREATE_CHANNEL, {
+                input: {
+                    code: 'third-channel',
+                    token: THIRD_CHANNEL_TOKEN,
+                    defaultLanguageCode: LanguageCode.en,
+                    currencyCode: CurrencyCode.GBP,
+                    pricesIncludeTax: true,
+                    defaultShippingZoneId: 'T_1',
+                    defaultTaxZoneId: 'T_1',
+                },
+            });
+        });
+
+        it('creates a PaymentMethod in channel2', async () => {
+            adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
+            const { createPaymentMethod } = await adminClient.query<
+                CreatePaymentMethod.Mutation,
+                CreatePaymentMethod.Variables
+            >(CREATE_PAYMENT_METHOD, {
+                input: {
+                    code: 'channel-2-method',
+                    name: 'Channel 2 method',
+                    description: 'This is a test payment method',
+                    enabled: true,
+                    handler: {
+                        code: dummyPaymentHandler.code,
+                        arguments: [{ name: 'automaticSettle', value: 'true' }],
+                    },
+                },
+            });
+
+            expect(createPaymentMethod.code).toBe('channel-2-method');
+        });
+
+        it('method is listed in channel2', async () => {
+            adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
+            const { paymentMethods } = await adminClient.query<GetPaymentMethodList.Query>(
+                GET_PAYMENT_METHOD_LIST,
+            );
+
+            expect(paymentMethods.totalItems).toBe(1);
+            expect(paymentMethods.items[0].code).toBe('channel-2-method');
+        });
+
+        it('method is not listed in channel3', async () => {
+            adminClient.setChannelToken(THIRD_CHANNEL_TOKEN);
+            const { paymentMethods } = await adminClient.query<GetPaymentMethodList.Query>(
+                GET_PAYMENT_METHOD_LIST,
+            );
+
+            expect(paymentMethods.totalItems).toBe(0);
+        });
+
+        it('method is listed in default channel', async () => {
+            adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
+            const { paymentMethods } = await adminClient.query<GetPaymentMethodList.Query>(
+                GET_PAYMENT_METHOD_LIST,
+            );
+
+            expect(paymentMethods.totalItems).toBe(4);
+            expect(paymentMethods.items.map(i => i.code).sort()).toEqual([
+                'channel-2-method',
+                'disabled-method',
+                'no-checks',
+                'price-check',
+            ]);
+        });
+    });
 });
 
 export const PAYMENT_METHOD_FRAGMENT = gql`
@@ -357,3 +451,15 @@ export const GET_PAYMENT_METHOD = gql`
     }
     ${PAYMENT_METHOD_FRAGMENT}
 `;
+
+export const GET_PAYMENT_METHOD_LIST = gql`
+    query GetPaymentMethodList($options: PaymentMethodListOptions) {
+        paymentMethods(options: $options) {
+            items {
+                ...PaymentMethod
+            }
+            totalItems
+        }
+    }
+    ${PAYMENT_METHOD_FRAGMENT}
+`;

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

@@ -1,8 +1,10 @@
 import { ConfigArg, ConfigurableOperation } from '@vendure/common/lib/generated-types';
 import { DeepPartial } from '@vendure/common/lib/shared-types';
-import { Column, Entity } from 'typeorm';
+import { Column, Entity, JoinTable, ManyToMany } from 'typeorm';
 
+import { ChannelAware } from '../../common/types/common-types';
 import { VendureEntity } from '../base/base.entity';
+import { Channel } from '../channel/channel.entity';
 
 /**
  * @description
@@ -12,7 +14,7 @@ import { VendureEntity } from '../base/base.entity';
  * @docsCategory entities
  */
 @Entity()
-export class PaymentMethod extends VendureEntity {
+export class PaymentMethod extends VendureEntity implements ChannelAware {
     constructor(input?: DeepPartial<PaymentMethod>) {
         super(input);
     }
@@ -28,4 +30,8 @@ export class PaymentMethod extends VendureEntity {
     @Column('simple-json', { nullable: true }) checker: ConfigurableOperation | null;
 
     @Column('simple-json') handler: ConfigurableOperation;
+
+    @ManyToMany(type => Channel)
+    @JoinTable()
+    channels: Channel[];
 }

+ 12 - 4
packages/core/src/service/services/payment-method.service.ts

@@ -11,6 +11,7 @@ import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { RequestContext } from '../../api/common/request-context';
 import { UserInputError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
+import { idsAreEqual } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
 import { PaymentMethodEligibilityChecker } from '../../config/payment/payment-method-eligibility-checker';
 import { PaymentMethodHandler } from '../../config/payment/payment-method-handler';
@@ -22,6 +23,8 @@ import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-build
 import { patchEntity } from '../helpers/utils/patch-entity';
 import { TransactionalConnection } from '../transaction/transactional-connection';
 
+import { ChannelService } from './channel.service';
+
 @Injectable()
 export class PaymentMethodService {
     constructor(
@@ -30,6 +33,7 @@ export class PaymentMethodService {
         private listQueryBuilder: ListQueryBuilder,
         private eventBus: EventBus,
         private configArgService: ConfigArgService,
+        private channelService: ChannelService,
     ) {}
 
     findAll(
@@ -37,7 +41,7 @@ export class PaymentMethodService {
         options?: ListQueryOptions<PaymentMethod>,
     ): Promise<PaginatedList<PaymentMethod>> {
         return this.listQueryBuilder
-            .build(PaymentMethod, options, { ctx })
+            .build(PaymentMethod, options, { ctx, relations: ['channels'], channelId: ctx.channelId })
             .getManyAndCount()
             .then(([items, totalItems]) => ({
                 items,
@@ -46,7 +50,7 @@ export class PaymentMethodService {
     }
 
     findOne(ctx: RequestContext, paymentMethodId: ID): Promise<PaymentMethod | undefined> {
-        return this.connection.getRepository(ctx, PaymentMethod).findOne(paymentMethodId);
+        return this.connection.findOneInChannel(ctx, PaymentMethod, paymentMethodId, ctx.channelId);
     }
 
     async create(ctx: RequestContext, input: CreatePaymentMethodInput): Promise<PaymentMethod> {
@@ -58,6 +62,7 @@ export class PaymentMethodService {
                 input.checker,
             );
         }
+        this.channelService.assignToCurrentChannel(paymentMethod, ctx);
         return this.connection.getRepository(ctx, PaymentMethod).save(paymentMethod);
     }
 
@@ -92,9 +97,12 @@ export class PaymentMethodService {
     async getEligiblePaymentMethods(ctx: RequestContext, order: Order): Promise<PaymentMethodQuote[]> {
         const paymentMethods = await this.connection
             .getRepository(ctx, PaymentMethod)
-            .find({ where: { enabled: true } });
+            .find({ where: { enabled: true }, relations: ['channels'] });
         const results: PaymentMethodQuote[] = [];
-        for (const method of paymentMethods) {
+        const paymentMethodsInChannel = paymentMethods.filter(p =>
+            p.channels.find(pc => idsAreEqual(pc.id, ctx.channelId)),
+        );
+        for (const method of paymentMethodsInChannel) {
             let isEligible = true;
             let eligibilityMessage: string | undefined;
             if (method.checker) {

+ 2 - 2
packages/core/src/service/services/shipping-method.service.ts

@@ -12,7 +12,7 @@ import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { RequestContext } from '../../api/common/request-context';
 import { EntityNotFoundError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
-import { assertFound } from '../../common/utils';
+import { assertFound, idsAreEqual } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
 import { Logger } from '../../config/logger/vendure-logger';
 import { Channel } from '../../entity/channel/channel.entity';
@@ -183,7 +183,7 @@ export class ShippingMethodService {
     }
 
     getActiveShippingMethods(channel: Channel): ShippingMethod[] {
-        return this.activeShippingMethods.filter(sm => sm.channels.find(c => c.id === channel.id));
+        return this.activeShippingMethods.filter(sm => sm.channels.find(c => idsAreEqual(c.id, channel.id)));
     }
 
     /**