Kaynağa Gözat

feat(core): Implement deletion of PaymentMethods

Michael Bromley 4 yıl önce
ebeveyn
işleme
f97cd4fcf3

Dosya farkı çok büyük olduğundan ihmal edildi
+ 515 - 654
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts


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

@@ -423,6 +423,8 @@ export type Mutation = {
   createPaymentMethod: PaymentMethod;
   /** Update an existing PaymentMethod */
   updatePaymentMethod: PaymentMethod;
+  /** Delete a PaymentMethod */
+  deletePaymentMethod: DeletionResponse;
   /** Create a new ProductOptionGroup */
   createProductOptionGroup: ProductOptionGroup;
   /** Update an existing ProductOptionGroup */
@@ -830,6 +832,12 @@ export type MutationUpdatePaymentMethodArgs = {
 };
 
 
+export type MutationDeletePaymentMethodArgs = {
+  id: Scalars['ID'];
+  force?: Maybe<Scalars['Boolean']>;
+};
+
+
 export type MutationCreateProductOptionGroupArgs = {
   input: CreateProductOptionGroupInput;
 };

Dosya farkı çok büyük olduğundan ihmal edildi
+ 515 - 654
packages/core/e2e/graphql/generated-e2e-admin-types.ts


+ 92 - 0
packages/core/e2e/payment-method.e2e-spec.ts

@@ -20,6 +20,8 @@ import {
     CreateChannel,
     CreatePaymentMethod,
     CurrencyCode,
+    DeletePaymentMethod,
+    DeletionResult,
     GetPaymentMethod,
     GetPaymentMethodCheckers,
     GetPaymentMethodHandlers,
@@ -374,6 +376,87 @@ describe('PaymentMethod resolver', () => {
                 'price-check',
             ]);
         });
+
+        it('delete from channel', async () => {
+            adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
+            const { paymentMethods } = await adminClient.query<GetPaymentMethodList.Query>(
+                GET_PAYMENT_METHOD_LIST,
+            );
+
+            expect(paymentMethods.totalItems).toBe(1);
+
+            const { deletePaymentMethod } = await adminClient.query<
+                DeletePaymentMethod.Mutation,
+                DeletePaymentMethod.Variables
+            >(DELETE_PAYMENT_METHOD, {
+                id: paymentMethods.items[0].id,
+            });
+
+            expect(deletePaymentMethod.result).toBe(DeletionResult.DELETED);
+
+            const { paymentMethods: checkChannel } = await adminClient.query<GetPaymentMethodList.Query>(
+                GET_PAYMENT_METHOD_LIST,
+            );
+            expect(checkChannel.totalItems).toBe(0);
+
+            adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
+            const { paymentMethods: checkDefault } = await adminClient.query<GetPaymentMethodList.Query>(
+                GET_PAYMENT_METHOD_LIST,
+            );
+            expect(checkDefault.totalItems).toBe(4);
+        });
+
+        it('delete from default channel', async () => {
+            adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
+            const { createPaymentMethod } = await adminClient.query<
+                CreatePaymentMethod.Mutation,
+                CreatePaymentMethod.Variables
+            >(CREATE_PAYMENT_METHOD, {
+                input: {
+                    code: 'channel-2-method2',
+                    name: 'Channel 2 method 2',
+                    description: 'This is a test payment method',
+                    enabled: true,
+                    handler: {
+                        code: dummyPaymentHandler.code,
+                        arguments: [{ name: 'automaticSettle', value: 'true' }],
+                    },
+                },
+            });
+
+            adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
+            const { deletePaymentMethod: delete1 } = await adminClient.query<
+                DeletePaymentMethod.Mutation,
+                DeletePaymentMethod.Variables
+            >(DELETE_PAYMENT_METHOD, {
+                id: createPaymentMethod.id,
+            });
+
+            expect(delete1.result).toBe(DeletionResult.NOT_DELETED);
+            expect(delete1.message).toBe(
+                'The selected PaymentMethod is assigned to the following Channels: second-channel. Set "force: true" to delete from all Channels.',
+            );
+
+            const { paymentMethods: check1 } = await adminClient.query<GetPaymentMethodList.Query>(
+                GET_PAYMENT_METHOD_LIST,
+            );
+            expect(check1.totalItems).toBe(5);
+
+            const { deletePaymentMethod: delete2 } = await adminClient.query<
+                DeletePaymentMethod.Mutation,
+                DeletePaymentMethod.Variables
+            >(DELETE_PAYMENT_METHOD, {
+                id: createPaymentMethod.id,
+                force: true,
+            });
+
+            expect(delete2.result).toBe(DeletionResult.DELETED);
+
+            const { paymentMethods: check2 } = await adminClient.query<GetPaymentMethodList.Query>(
+                GET_PAYMENT_METHOD_LIST,
+            );
+            expect(check2.totalItems).toBe(4);
+        });
     });
 });
 
@@ -463,3 +546,12 @@ export const GET_PAYMENT_METHOD_LIST = gql`
     }
     ${PAYMENT_METHOD_FRAGMENT}
 `;
+
+export const DELETE_PAYMENT_METHOD = gql`
+    mutation DeletePaymentMethod($id: ID!, $force: Boolean) {
+        deletePaymentMethod(id: $id, force: $force) {
+            message
+            result
+        }
+    }
+`;

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

@@ -1,7 +1,9 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
     ConfigurableOperationDefinition,
+    DeletionResponse,
     MutationCreatePaymentMethodArgs,
+    MutationDeletePaymentMethodArgs,
     MutationUpdatePaymentMethodArgs,
     Permission,
     QueryPaymentMethodArgs,
@@ -58,6 +60,16 @@ export class PaymentMethodResolver {
         return this.paymentMethodService.update(ctx, args.input);
     }
 
+    @Transaction()
+    @Mutation()
+    @Allow(Permission.DeleteSettings)
+    deletePaymentMethod(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MutationDeletePaymentMethodArgs,
+    ): Promise<DeletionResponse> {
+        return this.paymentMethodService.delete(ctx, args.id, args.force);
+    }
+
     @Query()
     @Allow(Permission.ReadSettings)
     paymentMethodHandlers(@Ctx() ctx: RequestContext): ConfigurableOperationDefinition[] {

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

@@ -10,6 +10,8 @@ type Mutation {
     createPaymentMethod(input: CreatePaymentMethodInput!): PaymentMethod!
     "Update an existing PaymentMethod"
     updatePaymentMethod(input: UpdatePaymentMethodInput!): PaymentMethod!
+    "Delete a PaymentMethod"
+    deletePaymentMethod(id: ID!, force: Boolean): DeletionResponse!
 }
 
 type PaymentMethodList implements PaginatedList {

+ 34 - 3
packages/core/src/config/payment/dummy-payment-method-handler.ts

@@ -6,6 +6,23 @@ import { CreatePaymentResult, PaymentMethodHandler } from './payment-method-hand
  * @description
  * A dummy PaymentMethodHandler which simply creates a Payment without any integration
  * with an external payment provider. Intended only for use in development.
+ *
+ * By specifying certain metadata keys, failures can be simulated:
+ * @example
+ * ```GraphQL
+ * addPaymentToOrder(input: {
+ *   method: 'dummy-payment-method',
+ *   metadata: {
+ *     shouldDecline: false,
+ *     shouldError: false,
+ *     shouldErrorOnSettle: true,
+ *   }
+ * }) {
+ *   # ...
+ * }
+ * ```
+ *
+ * @docsCategory payment
  */
 export const dummyPaymentHandler = new PaymentMethodHandler({
     code: 'dummy-payment-handler',
@@ -34,11 +51,20 @@ export const dummyPaymentHandler = new PaymentMethodHandler({
             defaultValue: false,
         },
     },
-    createPayment: async (ctx, order, amount, args, metadata): Promise<CreatePaymentResult> => {
+    createPayment: async (ctx, order, amount, args, metadata) => {
         if (metadata.shouldDecline) {
             return {
                 amount,
-                state: 'Declined' as 'Declined',
+                state: 'Declined' as const,
+                metadata: {
+                    errorMessage: 'Simulated decline',
+                },
+            };
+        } else if (metadata.shouldError) {
+            return {
+                amount,
+                state: 'Error' as const,
+                errorMessage: 'Simulated error',
                 metadata: {
                     errorMessage: 'Simulated error',
                 },
@@ -53,9 +79,14 @@ export const dummyPaymentHandler = new PaymentMethodHandler({
         }
     },
     settlePayment: async (ctx, order, payment, args) => {
+        if (payment.metadata.shouldErrorOnSettle) {
+            return {
+                success: false,
+                errorMessage: 'Simulated settlement error',
+            };
+        }
         return {
             success: true,
-            metadata: {},
         };
     },
 });

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

@@ -109,6 +109,7 @@
     "facet-used": "The selected Facet includes FacetValues which are assigned to {products, plural, =0 {} one {1 Product} other {# Products}}{both, select, both { , } single {}}{variants, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}}",
     "facet-value-force-deleted": "The selected FacetValue was removed from {products, plural, =0 {} one {1 Product} other {# Products}}{both, select, both { , } single {}}{variants, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}} and deleted",
     "facet-value-used": "The selected FacetValue is assigned to {products, plural, =0 {} one {1 Product} other {# Products}}{both, select, both { , } single {}}{variants, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}}",
+    "payment-method-used-in-channels": "The selected PaymentMethod is assigned to the following Channels: { channelCodes }. Set \"force: true\" to delete from all Channels.",
     "zone-used-in-channels": "The selected Zone cannot be deleted as it used as a default in the following Channels: { channelCodes }",
     "zone-used-in-tax-rates": "The selected Zone cannot be deleted as it is used in the following TaxRates: { taxRateNames }"
   }

+ 45 - 0
packages/core/src/service/services/payment-method.service.ts

@@ -3,9 +3,12 @@ import { PaymentMethodQuote } from '@vendure/common/lib/generated-shop-types';
 import {
     ConfigurableOperationDefinition,
     CreatePaymentMethodInput,
+    DeletionResponse,
+    DeletionResult,
     UpdatePaymentMethodInput,
 } from '@vendure/common/lib/generated-types';
 import { omit } from '@vendure/common/lib/omit';
+import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../api/common/request-context';
@@ -84,6 +87,48 @@ export class PaymentMethodService {
         return this.connection.getRepository(ctx, PaymentMethod).save(updatedPaymentMethod);
     }
 
+    async delete(
+        ctx: RequestContext,
+        paymentMethodId: ID,
+        force: boolean = false,
+    ): Promise<DeletionResponse> {
+        const paymentMethod = await this.connection.getEntityOrThrow(ctx, PaymentMethod, paymentMethodId, {
+            relations: ['channels'],
+            channelId: ctx.channelId,
+        });
+        if (ctx.channel.code === DEFAULT_CHANNEL_CODE) {
+            const nonDefaultChannels = paymentMethod.channels.filter(
+                channel => channel.code !== DEFAULT_CHANNEL_CODE,
+            );
+            if (0 < nonDefaultChannels.length && !force) {
+                const message = ctx.translate('message.payment-method-used-in-channels', {
+                    channelCodes: nonDefaultChannels.map(c => c.code).join(', '),
+                });
+                const result = DeletionResult.NOT_DELETED;
+                return { result, message };
+            }
+            try {
+                await this.connection.getRepository(ctx, PaymentMethod).remove(paymentMethod);
+                return {
+                    result: DeletionResult.DELETED,
+                };
+            } catch (e) {
+                return {
+                    result: DeletionResult.NOT_DELETED,
+                    message: e.message || String(e),
+                };
+            }
+        } else {
+            // If not deleting from the default channel, we will not actually delete,
+            // but will remove from the current channel
+            paymentMethod.channels = paymentMethod.channels.filter(c => !idsAreEqual(c.id, ctx.channelId));
+            await this.connection.getRepository(ctx, PaymentMethod).save(paymentMethod);
+            return {
+                result: DeletionResult.DELETED,
+            };
+        }
+    }
+
     getPaymentMethodEligibilityCheckers(ctx: RequestContext): ConfigurableOperationDefinition[] {
         return this.configArgService
             .getDefinitions('PaymentMethodEligibilityChecker')

Dosya farkı çok büyük olduğundan ihmal edildi
+ 515 - 654
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
schema-admin.json


Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor