From 8b8fc57a53650d8c64456213acab15cbd2e70db7 Mon Sep 17 00:00:00 2001 From: Martijn Date: Tue, 7 Jan 2025 15:44:06 +0100 Subject: [PATCH 1/8] feat(accept-blue): implemented update subscription --- .../vendure-plugin-accept-blue/CHANGELOG.md | 5 ++ .../vendure-plugin-accept-blue/package.json | 2 +- .../src/api/accept-blue-client.ts | 25 +++++- .../src/api/accept-blue-common-resolvers.ts | 13 ++- .../src/api/accept-blue-service.ts | 50 ++++++++++- .../src/api/api-extensions.ts | 41 +++++++++ .../events/accept-blue-subscription-event.ts | 19 +++++ .../accept-blue-transaction-event.ts | 0 .../vendure-plugin-accept-blue/src/index.ts | 2 +- .../vendure-plugin-accept-blue/src/types.ts | 18 +++- .../test/accept-blue.spec.ts | 83 ++++++++++++++++--- .../test/helpers/graphql-helpers.ts | 27 +++--- 12 files changed, 252 insertions(+), 33 deletions(-) create mode 100644 packages/vendure-plugin-accept-blue/src/events/accept-blue-subscription-event.ts rename packages/vendure-plugin-accept-blue/src/{api => events}/accept-blue-transaction-event.ts (100%) diff --git a/packages/vendure-plugin-accept-blue/CHANGELOG.md b/packages/vendure-plugin-accept-blue/CHANGELOG.md index a2f3b78d3..77d6e1908 100644 --- a/packages/vendure-plugin-accept-blue/CHANGELOG.md +++ b/packages/vendure-plugin-accept-blue/CHANGELOG.md @@ -1,3 +1,8 @@ +# 2.1.0 (2025-01-01) + +- Allow updating created subscriptions via the Admin API +- Only allow refunding for users with permission `UpdateOrder` + # 2.0.0 (2024-12-19) - Update Vendure to 3.1.1 diff --git a/packages/vendure-plugin-accept-blue/package.json b/packages/vendure-plugin-accept-blue/package.json index ed754fe78..4924b2f6b 100644 --- a/packages/vendure-plugin-accept-blue/package.json +++ b/packages/vendure-plugin-accept-blue/package.json @@ -1,6 +1,6 @@ { "name": "@pinelab/vendure-plugin-accept-blue", - "version": "2.0.0", + "version": "2.1.0", "description": "Vendure plugin for creating subscriptions with the Accept Blue platform", "author": "Martijn van de Brug ", "homepage": "https://pinelab-plugins.com/", diff --git a/packages/vendure-plugin-accept-blue/src/api/accept-blue-client.ts b/packages/vendure-plugin-accept-blue/src/api/accept-blue-client.ts index 69a30be5b..be8bc8c7d 100644 --- a/packages/vendure-plugin-accept-blue/src/api/accept-blue-client.ts +++ b/packages/vendure-plugin-accept-blue/src/api/accept-blue-client.ts @@ -7,7 +7,7 @@ import { AcceptBlueCustomer, AcceptBluePaymentMethod, AcceptBlueRecurringSchedule, - AcceptBlueRecurringScheduleInput, + AcceptBlueRecurringScheduleCreateInput, AcceptBlueRecurringScheduleTransaction, CheckPaymentMethodInput, NoncePaymentMethodInput, @@ -15,6 +15,7 @@ import { AcceptBlueWebhookInput, AcceptBlueWebhook, CustomFields, + AcceptBlueRecurringScheduleUpdateInput, } from '../types'; import { isSameCard, isSameCheck } from '../util'; @@ -132,6 +133,26 @@ export class AcceptBlueClient { ); } + async updateRecurringSchedule( + id: number, + input: AcceptBlueRecurringScheduleUpdateInput + ): Promise { + const formattedInput = { + ...input, + amount: input.amount ? input.amount / 100 : undefined, + // Accept Blue requires dates to be in 'yyyy-mm-dd' format + next_run_date: input.next_run_date + ? this.toDateString(input.next_run_date) + : undefined, + }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return await this.request( + 'patch', + `recurring-schedules/${id}`, + formattedInput + ); + } + async getTransactionsForRecurringSchedule( id: number ): Promise { @@ -179,7 +200,7 @@ export class AcceptBlueClient { async createRecurringSchedule( customerId: number, - input: AcceptBlueRecurringScheduleInput + input: AcceptBlueRecurringScheduleCreateInput ): Promise { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const result: AcceptBlueRecurringSchedule = await this.request( diff --git a/packages/vendure-plugin-accept-blue/src/api/accept-blue-common-resolvers.ts b/packages/vendure-plugin-accept-blue/src/api/accept-blue-common-resolvers.ts index 2d9e91f91..1f29f7936 100644 --- a/packages/vendure-plugin-accept-blue/src/api/accept-blue-common-resolvers.ts +++ b/packages/vendure-plugin-accept-blue/src/api/accept-blue-common-resolvers.ts @@ -28,6 +28,7 @@ import { QueryPreviewAcceptBlueSubscriptionsArgs, QueryPreviewAcceptBlueSubscriptionsForProductArgs, MutationRefundAcceptBlueTransactionArgs, + MutationUpdateAcceptBlueSubscriptionArgs, } from './generated/graphql'; @Resolver() @@ -77,7 +78,7 @@ export class AcceptBlueCommonResolver { } @Mutation() - @Allow(Permission.Authenticated) + @Allow(Permission.UpdateOrder) async refundAcceptBlueTransaction( @Ctx() ctx: RequestContext, @Args() @@ -91,6 +92,16 @@ export class AcceptBlueCommonResolver { ); } + @Mutation() + @Allow(Permission.UpdateOrder) + async updateAcceptBlueSubscription( + @Ctx() ctx: RequestContext, + @Args() + { input }: MutationUpdateAcceptBlueSubscriptionArgs + ): Promise { + return await this.acceptBlueService.updateSubscription(ctx, input); + } + @ResolveField('acceptBlueHostedTokenizationKey') @Resolver('PaymentMethodQuote') async acceptBlueHostedTokenizationKey( diff --git a/packages/vendure-plugin-accept-blue/src/api/accept-blue-service.ts b/packages/vendure-plugin-accept-blue/src/api/accept-blue-service.ts index 9b182e5f5..1bfd51cb8 100644 --- a/packages/vendure-plugin-accept-blue/src/api/accept-blue-service.ts +++ b/packages/vendure-plugin-accept-blue/src/api/accept-blue-service.ts @@ -11,6 +11,7 @@ import { Logger, Order, OrderLine, + OrderService, PaymentMethod, PaymentMethodEvent, PaymentMethodService, @@ -51,8 +52,10 @@ import { AcceptBlueRefundResult, AcceptBlueSubscription, AcceptBlueTransaction, + UpdateAcceptBlueSubscriptionInput, } from './generated/graphql'; -import { AcceptBlueTransactionEvent } from './accept-blue-transaction-event'; +import { AcceptBlueTransactionEvent } from '../events/accept-blue-transaction-event'; +import { AcceptBlueSubscriptionEvent } from '../events/accept-blue-subscription-event'; @Injectable() export class AcceptBlueService implements OnApplicationBootstrap { @@ -61,8 +64,9 @@ export class AcceptBlueService implements OnApplicationBootstrap { private readonly paymentMethodService: PaymentMethodService, private readonly customerService: CustomerService, private readonly entityHydrator: EntityHydrator, - private connection: TransactionalConnection, - private eventBus: EventBus, + private readonly connection: TransactionalConnection, + private readonly eventBus: EventBus, + private readonly orderService: OrderService, moduleRef: ModuleRef, @Inject(PLUGIN_INIT_OPTIONS) private readonly options: AcceptBluePluginOptions @@ -297,6 +301,45 @@ export class AcceptBlueService implements OnApplicationBootstrap { return recurringSchedules; } + async updateSubscription( + ctx: RequestContext, + input: UpdateAcceptBlueSubscriptionInput + ): Promise { + const scheduleId = input.id; + const orderLine = await this.findOrderLineByScheduleId(ctx, scheduleId); + if (!orderLine) { + throw new UserInputError( + `No order exists with an Accept Blue subscription id of ${scheduleId}` + ); + } + await this.entityHydrator.hydrate(ctx, orderLine, { relations: ['order'] }); + const client = await this.getClientForChannel(ctx); + const subscription = await client.updateRecurringSchedule(scheduleId, { + title: input.title || undefined, + amount: input.amount || undefined, + frequency: input.frequency ?? undefined, + next_run_date: input.nextRunDate || undefined, + num_left: input.numLeft || undefined, + active: input.active || undefined, + receipt_email: input.receiptEmail || undefined, + }); + const subcription = this.mapToGraphqlSubscription( + subscription, + orderLine.productVariant.id + ); + // Write History entry on order + await this.orderService.addNoteToOrder(ctx, { + id: orderLine.order.id, + note: `Subscription updated: ${JSON.stringify(input)}`, + isPublic: true, + }); + // Publish event + await this.eventBus.publish( + new AcceptBlueSubscriptionEvent(ctx, subcription, 'updated', input) + ); + return subcription; + } + /** * Resolve the subscriptions for an order line. For a placed order, this will also fetch transactions per subscription */ @@ -545,6 +588,7 @@ export class AcceptBlueService implements OnApplicationBootstrap { const result = await this.connection .getRepository(ctx, OrderLine) .createQueryBuilder('orderLine') + .leftJoinAndSelect('orderLine.productVariant', 'productVariant') .where( 'orderLine.customFields.acceptBlueSubscriptionIds LIKE :scheduleId', { scheduleId: `%${scheduleId}%` } diff --git a/packages/vendure-plugin-accept-blue/src/api/api-extensions.ts b/packages/vendure-plugin-accept-blue/src/api/api-extensions.ts index f905ba38a..cc5758256 100644 --- a/packages/vendure-plugin-accept-blue/src/api/api-extensions.ts +++ b/packages/vendure-plugin-accept-blue/src/api/api-extensions.ts @@ -118,6 +118,37 @@ export const commonApiExtensions = gql` savedAcceptBluePaymentMethods: [AcceptBluePaymentMethod!]! } + enum AcceptBlueFrequencyInput { + daily + weekly + biweekly + monthly + bimonthly + quarterly + biannually + annually + } + + input UpdateAcceptBlueSubscriptionInput { + id: Int! + title: String + frequency: AcceptBlueFrequencyInput + """ + Amount in cents to bill customer + """ + amount: Int + nextRunDate: DateTime + """ + Number of times the schedule has left to bill. Set to 0 for ongoing + """ + numLeft: Int + active: Boolean + """ + An email address to send a customer receipt to each time the schedule runs + """ + receiptEmail: String + } + extend type Query { previewAcceptBlueSubscriptions( productVariantId: ID! @@ -130,10 +161,20 @@ export const commonApiExtensions = gql` } extend type Mutation { + """ + Refund a transaction by ID + """ refundAcceptBlueTransaction( transactionId: Int! amount: Int cvv2: String ): AcceptBlueRefundResult! + + """ + Update the given subscription in Accept Blue + """ + updateAcceptBlueSubscription( + input: UpdateAcceptBlueSubscriptionInput! + ): AcceptBlueSubscription! } `; diff --git a/packages/vendure-plugin-accept-blue/src/events/accept-blue-subscription-event.ts b/packages/vendure-plugin-accept-blue/src/events/accept-blue-subscription-event.ts new file mode 100644 index 000000000..6424587d0 --- /dev/null +++ b/packages/vendure-plugin-accept-blue/src/events/accept-blue-subscription-event.ts @@ -0,0 +1,19 @@ +import { RequestContext, VendureEvent } from '@vendure/core'; +import { + AcceptBlueSubscription, + UpdateAcceptBlueSubscriptionInput, +} from '../api/generated/graphql'; + +/** + * This event is fired when a subscription (schedule) has been updated in Accept Blue via the graphql API. + */ +export class AcceptBlueSubscriptionEvent extends VendureEvent { + constructor( + ctx: RequestContext, + public subscription: AcceptBlueSubscription, + public type: 'created' | 'updated' | 'deleted', + public input?: UpdateAcceptBlueSubscriptionInput + ) { + super(); + } +} diff --git a/packages/vendure-plugin-accept-blue/src/api/accept-blue-transaction-event.ts b/packages/vendure-plugin-accept-blue/src/events/accept-blue-transaction-event.ts similarity index 100% rename from packages/vendure-plugin-accept-blue/src/api/accept-blue-transaction-event.ts rename to packages/vendure-plugin-accept-blue/src/events/accept-blue-transaction-event.ts diff --git a/packages/vendure-plugin-accept-blue/src/index.ts b/packages/vendure-plugin-accept-blue/src/index.ts index e5031dc29..93032494e 100644 --- a/packages/vendure-plugin-accept-blue/src/index.ts +++ b/packages/vendure-plugin-accept-blue/src/index.ts @@ -4,7 +4,7 @@ export * from '../../util/src/subscription/subscription-helper'; export * from './accept-blue-plugin'; export * from './api/accept-blue-service'; export * from './api/accept-blue-client'; -export * from './api/accept-blue-transaction-event'; +export * from './events/accept-blue-transaction-event'; export * from './api/accept-blue-handler'; export * from './api/accept-blue-common-resolvers'; export * from './api/custom-field-types'; diff --git a/packages/vendure-plugin-accept-blue/src/types.ts b/packages/vendure-plugin-accept-blue/src/types.ts index 66748d583..cacdf8ba7 100644 --- a/packages/vendure-plugin-accept-blue/src/types.ts +++ b/packages/vendure-plugin-accept-blue/src/types.ts @@ -262,7 +262,10 @@ export interface AcceptBlueRecurringSchedule { transaction_count: number; } -export interface AcceptBlueRecurringScheduleInput { +/** + * Input type for creating a new recurring schedule + */ +export interface AcceptBlueRecurringScheduleCreateInput { title: string; amount: number; payment_method_id: number; @@ -273,6 +276,19 @@ export interface AcceptBlueRecurringScheduleInput { receipt_email?: string; use_this_source_key?: boolean; } +/** + * Input type for updating an existing recurring schedule + */ +export interface AcceptBlueRecurringScheduleUpdateInput { + title?: string; + amount?: number; + payment_method_id?: number; + frequency?: Frequency; + next_run_date?: Date; + num_left?: number; + active?: boolean; + receipt_email?: string; +} export interface AcceptBlueRecurringScheduleTransaction { id: number; diff --git a/packages/vendure-plugin-accept-blue/test/accept-blue.spec.ts b/packages/vendure-plugin-accept-blue/test/accept-blue.spec.ts index 69a19156b..477f181e9 100644 --- a/packages/vendure-plugin-accept-blue/test/accept-blue.spec.ts +++ b/packages/vendure-plugin-accept-blue/test/accept-blue.spec.ts @@ -48,7 +48,7 @@ import { REFUND_TRANSACTION, SET_SHIPPING_METHOD, TRANSITION_ORDER_TO, - UPDATE_CUSTOMER_BLUE_ID, + UPDATE_SUBSCRIPTION, } from './helpers/graphql-helpers'; import { checkChargeResult, @@ -60,7 +60,12 @@ import { createMockWebhook, createSignature, } from './helpers/mocks'; -import { AcceptBlueTransactionEvent } from '../src/api/accept-blue-transaction-event'; +import { AcceptBlueTransactionEvent } from '../src/events/accept-blue-transaction-event'; +import { + AcceptBlueSubscription, + MutationUpdateAcceptBlueSubscriptionArgs, + UpdateAcceptBlueSubscriptionInput, +} from '../src/api/generated/graphql'; let server: TestServer; let adminClient: SimpleGraphQLClient; @@ -457,7 +462,7 @@ describe('Payment with Saved Payment Method', () => { }); }); -describe('Refunds and transactions', () => { +describe('Transactions', () => { let orderLineWithSubscription: OrderLine; it('Emits transaction event for incoming schedule payments webhook', async () => { @@ -536,8 +541,11 @@ describe('Refunds and transactions', () => { expect(transaction.cardDetails).toBeDefined(); expect(transaction.amount).toBeDefined(); }); +}); +describe('Admin API', () => { it('Refunds a transaction', async () => { + await adminClient.asSuperAdmin(); let refundRequest: any; nockInstance .post(`/transactions/refund`, (body) => { @@ -552,7 +560,7 @@ describe('Refunds and transactions', () => { error_details: { detail: 'An error detail object' }, reference_number: 123, }); - const { refundAcceptBlueTransaction } = await shopClient.query( + const { refundAcceptBlueTransaction } = await adminClient.query( REFUND_TRANSACTION, { transactionId: 123, @@ -574,10 +582,10 @@ describe('Refunds and transactions', () => { }); it('Fails to refund when not logged in', async () => { - await shopClient.asAnonymousUser(); + await adminClient.asAnonymousUser(); let error: any; try { - await shopClient.query(REFUND_TRANSACTION, { + await adminClient.query(REFUND_TRANSACTION, { transactionId: 123, amount: 4567, cvv2: '999', @@ -587,10 +595,6 @@ describe('Refunds and transactions', () => { } expect(error?.response?.errors?.[0]?.extensions.code).toEqual('FORBIDDEN'); }); -}); - -describe('Admin API', () => { - // Just smoke test 1 call, so we know resolvers and schema are also loaded for admin API it('Gets saved payment methods for customer', async () => { nockInstance @@ -599,9 +603,68 @@ describe('Admin API', () => { `/customers/${haydenZiemeCustomerDetails.id}/payment-methods?limit=100` ) .reply(200, haydenSavedPaymentMethods); + await adminClient.asSuperAdmin(); const { customer } = await adminClient.query(GET_CUSTOMER_WITH_ID, { id: '1', }); expect(customer?.savedAcceptBluePaymentMethods?.length).toBeGreaterThan(0); }); + + it('Does not allow updating subscriptions by unauthorized admins', async () => { + await adminClient.asAnonymousUser(); + const updateRequest = adminClient.query< + { updateAcceptBlueSubscription: AcceptBlueSubscription }, + MutationUpdateAcceptBlueSubscriptionArgs + >(UPDATE_SUBSCRIPTION, { + input: { + id: 123, + active: false, + }, + }); + expect(updateRequest).rejects.toThrowError( + 'You are not currently authorized to perform this action' + ); + }); + + it('Updates a subscription', async () => { + await adminClient.asSuperAdmin(); + const scheduleId = 6014; // This ID was created earlier in test, and added to an order + let updateRequest: any; + nockInstance + .persist() + .patch(`/recurring-schedules/${scheduleId}`, (body) => { + updateRequest = body; + return true; + }) + .reply(200, createMockRecurringScheduleResult(scheduleId)); + const tenDaysFromNow = new Date(); + tenDaysFromNow.setDate(tenDaysFromNow.getDate() + 10); + await adminClient.query( + UPDATE_SUBSCRIPTION, + { + input: { + id: scheduleId, + amount: 4321, + active: false, + frequency: 'biannually', + nextRunDate: tenDaysFromNow, + numLeft: 5, + title: 'Updated title', + receiptEmail: 'newCustomer@pinelab.studio', + }, + } + ); + expect(updateRequest).toEqual({ + title: 'Updated title', + amount: 43.21, + frequency: 'biannually', + next_run_date: '2025-01-17', + num_left: 5, + receipt_email: 'newCustomer@pinelab.studio', + }); + }); + + // TODO check order history entry + + // Check event publishing }); diff --git a/packages/vendure-plugin-accept-blue/test/helpers/graphql-helpers.ts b/packages/vendure-plugin-accept-blue/test/helpers/graphql-helpers.ts index 720681426..986ae555f 100644 --- a/packages/vendure-plugin-accept-blue/test/helpers/graphql-helpers.ts +++ b/packages/vendure-plugin-accept-blue/test/helpers/graphql-helpers.ts @@ -223,20 +223,19 @@ export const GET_CUSTOMER_WITH_ID = gql` ${ACCEPT_BLUE_PAYMENT_METHOD_FRAGMENT} `; -export const UPDATE_CUSTOMER_BLUE_ID = gql` - mutation UpdateCustomer($customerId: ID!, $acceptBlueCustomerId: Int!) { - updateCustomer( - input: { - id: $customerId - customFields: { acceptBlueCustomerId: $acceptBlueCustomerId } - } - ) { - ... on Customer { - id - emailAddress - } - ... on ErrorResult { - message +export const UPDATE_SUBSCRIPTION = gql` + mutation UpdateSubscription($input: UpdateAcceptBlueSubscriptionInput!) { + updateAcceptBlueSubscription(input: $input) { + name + variantId + amountDueNow + priceIncludesTax + recurring { + amount + interval + intervalCount + startDate + endDate } } } From 903b41082b926c1f471f19ddf26a734fc8c4e835 Mon Sep 17 00:00:00 2001 From: Martijn Date: Tue, 7 Jan 2025 16:59:10 +0100 Subject: [PATCH 2/8] feat(accept-blue): martijn can also approve accept blue --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d56cf85de..af8e2359a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,4 +2,4 @@ * @martijnvdbrug # IS Outfitters have the ability to approve and merge eachothers PR's for Accept Blue -/packages/vendure-plugin-accept-blue/ @is0utfitters @mschipperheyn \ No newline at end of file +/packages/vendure-plugin-accept-blue/ @is0utfitters @mschipperheyn @martijnvdbrug \ No newline at end of file From 1ffc3075ca3ce76d4d8d0333c1775777fadfccec Mon Sep 17 00:00:00 2001 From: Martijn Date: Fri, 10 Jan 2025 10:29:46 +0100 Subject: [PATCH 3/8] feat(accept-blue): implemented all tests and move admin to specific --- .../src/accept-blue-plugin.ts | 9 +++-- .../src/api/accept-blue-admin-resolver.ts | 38 ++++++++++++++++++ .../src/api/accept-blue-common-resolvers.ts | 39 +------------------ .../src/api/accept-blue-service.ts | 9 +++-- .../src/api/api-extensions.ts | 10 ++++- .../vendure-plugin-accept-blue/src/index.ts | 1 + .../test/accept-blue.spec.ts | 29 ++++++++++++-- .../test/helpers/graphql-helpers.ts | 16 ++++++++ 8 files changed, 100 insertions(+), 51 deletions(-) create mode 100644 packages/vendure-plugin-accept-blue/src/api/accept-blue-admin-resolver.ts diff --git a/packages/vendure-plugin-accept-blue/src/accept-blue-plugin.ts b/packages/vendure-plugin-accept-blue/src/accept-blue-plugin.ts index 01a61b56e..22d5435b2 100644 --- a/packages/vendure-plugin-accept-blue/src/accept-blue-plugin.ts +++ b/packages/vendure-plugin-accept-blue/src/accept-blue-plugin.ts @@ -3,11 +3,12 @@ import { SubscriptionStrategy } from '../../util/src/subscription/subscription-s import { AcceptBlueService } from './api/accept-blue-service'; import { acceptBluePaymentHandler } from './api/accept-blue-handler'; import { PLUGIN_INIT_OPTIONS } from './constants'; -import { commonApiExtensions } from './api/api-extensions'; +import { adminApiExtensions, shopApiExtensions } from './api/api-extensions'; import { AcceptBlueCommonResolver } from './api/accept-blue-common-resolvers'; import { AcceptBlueController } from './api/accept-blue-controller'; import { DefaultSubscriptionStrategy } from '../../util/src/subscription/default-subscription-strategy'; import { rawBodyMiddleware } from '../../util/src/raw-body.middleware'; +import { AcceptBlueAdminResolver } from './api/accept-blue-admin-resolver'; interface AcceptBluePluginOptionsInput { subscriptionStrategy?: SubscriptionStrategy; @@ -19,11 +20,11 @@ export type AcceptBluePluginOptions = Required; @VendurePlugin({ imports: [PluginCommonModule], adminApiExtensions: { - schema: commonApiExtensions, - resolvers: [AcceptBlueCommonResolver], + schema: adminApiExtensions, + resolvers: [AcceptBlueCommonResolver, AcceptBlueAdminResolver], }, shopApiExtensions: { - schema: commonApiExtensions, + schema: shopApiExtensions, resolvers: [AcceptBlueCommonResolver], }, controllers: [AcceptBlueController], diff --git a/packages/vendure-plugin-accept-blue/src/api/accept-blue-admin-resolver.ts b/packages/vendure-plugin-accept-blue/src/api/accept-blue-admin-resolver.ts new file mode 100644 index 000000000..e2bf4919a --- /dev/null +++ b/packages/vendure-plugin-accept-blue/src/api/accept-blue-admin-resolver.ts @@ -0,0 +1,38 @@ +import { Args, Mutation, Resolver } from '@nestjs/graphql'; +import { Allow, Ctx, Permission, RequestContext } from '@vendure/core'; +import { AcceptBlueService } from './accept-blue-service'; +import { + Mutation as GraphqlMutation, + MutationRefundAcceptBlueTransactionArgs, + MutationUpdateAcceptBlueSubscriptionArgs, +} from './generated/graphql'; + +@Resolver() +export class AcceptBlueAdminResolver { + constructor(private acceptBlueService: AcceptBlueService) {} + + @Mutation() + @Allow(Permission.UpdateOrder) + async refundAcceptBlueTransaction( + @Ctx() ctx: RequestContext, + @Args() + { transactionId, amount, cvv2 }: MutationRefundAcceptBlueTransactionArgs + ): Promise { + return await this.acceptBlueService.refund( + ctx, + transactionId, + amount ?? undefined, + cvv2 ?? undefined + ); + } + + @Mutation() + @Allow(Permission.UpdateOrder) + async updateAcceptBlueSubscription( + @Ctx() ctx: RequestContext, + @Args() + { input }: MutationUpdateAcceptBlueSubscriptionArgs + ): Promise { + return await this.acceptBlueService.updateSubscription(ctx, input); + } +} diff --git a/packages/vendure-plugin-accept-blue/src/api/accept-blue-common-resolvers.ts b/packages/vendure-plugin-accept-blue/src/api/accept-blue-common-resolvers.ts index 1f29f7936..1184cafff 100644 --- a/packages/vendure-plugin-accept-blue/src/api/accept-blue-common-resolvers.ts +++ b/packages/vendure-plugin-accept-blue/src/api/accept-blue-common-resolvers.ts @@ -1,18 +1,9 @@ +import { Args, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; import { - Args, - Parent, - Query, - ResolveField, - Resolver, - Mutation, -} from '@nestjs/graphql'; -import { - Allow, Ctx, Customer, EntityHydrator, OrderLine, - Permission, RequestContext, } from '@vendure/core'; import { @@ -24,11 +15,8 @@ import { AcceptBlueService } from './accept-blue-service'; import { AcceptBlueSubscription, Query as GraphqlQuery, - Mutation as GraphqlMutation, QueryPreviewAcceptBlueSubscriptionsArgs, QueryPreviewAcceptBlueSubscriptionsForProductArgs, - MutationRefundAcceptBlueTransactionArgs, - MutationUpdateAcceptBlueSubscriptionArgs, } from './generated/graphql'; @Resolver() @@ -77,31 +65,6 @@ export class AcceptBlueCommonResolver { })); } - @Mutation() - @Allow(Permission.UpdateOrder) - async refundAcceptBlueTransaction( - @Ctx() ctx: RequestContext, - @Args() - { transactionId, amount, cvv2 }: MutationRefundAcceptBlueTransactionArgs - ): Promise { - return await this.acceptBlueService.refund( - ctx, - transactionId, - amount ?? undefined, - cvv2 ?? undefined - ); - } - - @Mutation() - @Allow(Permission.UpdateOrder) - async updateAcceptBlueSubscription( - @Ctx() ctx: RequestContext, - @Args() - { input }: MutationUpdateAcceptBlueSubscriptionArgs - ): Promise { - return await this.acceptBlueService.updateSubscription(ctx, input); - } - @ResolveField('acceptBlueHostedTokenizationKey') @Resolver('PaymentMethodQuote') async acceptBlueHostedTokenizationKey( diff --git a/packages/vendure-plugin-accept-blue/src/api/accept-blue-service.ts b/packages/vendure-plugin-accept-blue/src/api/accept-blue-service.ts index 1bfd51cb8..33d11d313 100644 --- a/packages/vendure-plugin-accept-blue/src/api/accept-blue-service.ts +++ b/packages/vendure-plugin-accept-blue/src/api/accept-blue-service.ts @@ -64,9 +64,9 @@ export class AcceptBlueService implements OnApplicationBootstrap { private readonly paymentMethodService: PaymentMethodService, private readonly customerService: CustomerService, private readonly entityHydrator: EntityHydrator, - private readonly connection: TransactionalConnection, - private readonly eventBus: EventBus, private readonly orderService: OrderService, + private readonly connection: TransactionalConnection, + private eventBus: EventBus, moduleRef: ModuleRef, @Inject(PLUGIN_INIT_OPTIONS) private readonly options: AcceptBluePluginOptions @@ -312,7 +312,9 @@ export class AcceptBlueService implements OnApplicationBootstrap { `No order exists with an Accept Blue subscription id of ${scheduleId}` ); } - await this.entityHydrator.hydrate(ctx, orderLine, { relations: ['order'] }); + await this.entityHydrator.hydrate(ctx, orderLine, { + relations: ['order', 'productVariant'], + }); const client = await this.getClientForChannel(ctx); const subscription = await client.updateRecurringSchedule(scheduleId, { title: input.title || undefined, @@ -588,7 +590,6 @@ export class AcceptBlueService implements OnApplicationBootstrap { const result = await this.connection .getRepository(ctx, OrderLine) .createQueryBuilder('orderLine') - .leftJoinAndSelect('orderLine.productVariant', 'productVariant') .where( 'orderLine.customFields.acceptBlueSubscriptionIds LIKE :scheduleId', { scheduleId: `%${scheduleId}%` } diff --git a/packages/vendure-plugin-accept-blue/src/api/api-extensions.ts b/packages/vendure-plugin-accept-blue/src/api/api-extensions.ts index cc5758256..43d477763 100644 --- a/packages/vendure-plugin-accept-blue/src/api/api-extensions.ts +++ b/packages/vendure-plugin-accept-blue/src/api/api-extensions.ts @@ -6,7 +6,7 @@ const _codegenAdditions = gql` scalar JSON `; -export const commonApiExtensions = gql` +const commonApiExtensions = gql` enum AcceptBlueSubscriptionInterval { week month @@ -159,6 +159,14 @@ export const commonApiExtensions = gql` customInputs: JSON ): [AcceptBlueSubscription!]! } +`; + +export const shopApiExtensions = gql` + ${commonApiExtensions} +`; + +export const adminApiExtensions = gql` + ${commonApiExtensions} extend type Mutation { """ diff --git a/packages/vendure-plugin-accept-blue/src/index.ts b/packages/vendure-plugin-accept-blue/src/index.ts index 93032494e..21ce13159 100644 --- a/packages/vendure-plugin-accept-blue/src/index.ts +++ b/packages/vendure-plugin-accept-blue/src/index.ts @@ -5,6 +5,7 @@ export * from './accept-blue-plugin'; export * from './api/accept-blue-service'; export * from './api/accept-blue-client'; export * from './events/accept-blue-transaction-event'; +export * from './events/accept-blue-subscription-event'; export * from './api/accept-blue-handler'; export * from './api/accept-blue-common-resolvers'; export * from './api/custom-field-types'; diff --git a/packages/vendure-plugin-accept-blue/test/accept-blue.spec.ts b/packages/vendure-plugin-accept-blue/test/accept-blue.spec.ts index 477f181e9..1443e7f3d 100644 --- a/packages/vendure-plugin-accept-blue/test/accept-blue.spec.ts +++ b/packages/vendure-plugin-accept-blue/test/accept-blue.spec.ts @@ -25,7 +25,7 @@ import { } from '@vendure/testing'; import { afterEach, beforeAll, describe, expect, it } from 'vitest'; import { initialData } from '../../test/src/initial-data'; -import { AcceptBluePlugin } from '../src'; +import { AcceptBluePlugin, AcceptBlueSubscriptionEvent } from '../src'; import { AcceptBlueClient } from '../src/api/accept-blue-client'; import { acceptBluePaymentHandler } from '../src/api/accept-blue-handler'; import { DataSource } from 'typeorm'; @@ -41,6 +41,7 @@ import { ADD_PAYMENT_TO_ORDER, CREATE_PAYMENT_METHOD, GET_CUSTOMER_WITH_ID, + GET_HISTORY_ENTRIES, GET_ORDER_BY_CODE, GET_USER_SAVED_PAYMENT_METHOD, PREVIEW_SUBSCRIPTIONS_FOR_PRODUCT, @@ -626,7 +627,13 @@ describe('Admin API', () => { ); }); + const events: AcceptBlueSubscriptionEvent[] = []; + it('Updates a subscription', async () => { + server.app + .get(EventBus) + .ofType(AcceptBlueSubscriptionEvent) + .subscribe((event) => events.push(event)); await adminClient.asSuperAdmin(); const scheduleId = 6014; // This ID was created earlier in test, and added to an order let updateRequest: any; @@ -658,13 +665,27 @@ describe('Admin API', () => { title: 'Updated title', amount: 43.21, frequency: 'biannually', - next_run_date: '2025-01-17', + next_run_date: tenDaysFromNow.toISOString().substring(0, 10), // Take yyyy-mm-dd num_left: 5, receipt_email: 'newCustomer@pinelab.studio', }); }); - // TODO check order history entry + it('Has created history entries', async () => { + await adminClient.asSuperAdmin(); + const { order } = await adminClient.query(GET_HISTORY_ENTRIES, { + id: placedOrder?.id, + }); + const entry = order.history.items.find( + (entry: any) => entry.type === 'ORDER_NOTE' + ); + expect(entry?.data.note).toContain('Subscription updated:'); + }); - // Check event publishing + it('Has published Subscription Event', async () => { + const event = events[0]; + expect(events.length).toBe(1); + expect(event.subscription.id).toBe(6014); + expect(event.type).toBe('updated'); + }); }); diff --git a/packages/vendure-plugin-accept-blue/test/helpers/graphql-helpers.ts b/packages/vendure-plugin-accept-blue/test/helpers/graphql-helpers.ts index 986ae555f..40edf9b39 100644 --- a/packages/vendure-plugin-accept-blue/test/helpers/graphql-helpers.ts +++ b/packages/vendure-plugin-accept-blue/test/helpers/graphql-helpers.ts @@ -169,6 +169,22 @@ export const GET_ORDER_BY_CODE = gql` } `; +export const GET_HISTORY_ENTRIES = gql` + query GetHistoryEntries($id: ID!) { + order(id: $id) { + id + code + history { + items { + id + type + data + } + } + } + } +`; + export const SET_SHIPPING_METHOD = gql` mutation SetShippingMethod($id: [ID!]!) { setOrderShippingMethod(shippingMethodId: $id) { From a5190dfc7c90533548efc4c35c2dcb24e46c0a4d Mon Sep 17 00:00:00 2001 From: Martijn Date: Fri, 10 Jan 2025 10:58:58 +0100 Subject: [PATCH 4/8] feat(accept-blue): readme --- packages/vendure-plugin-accept-blue/README.md | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/vendure-plugin-accept-blue/README.md b/packages/vendure-plugin-accept-blue/README.md index 182c3625e..2d6b152eb 100644 --- a/packages/vendure-plugin-accept-blue/README.md +++ b/packages/vendure-plugin-accept-blue/README.md @@ -175,6 +175,31 @@ mutation { The arguments `amount` and `cvv2` are optional, see [the Accept Blue Docs for more info](https://docs.accept.blue/api/v2#tag/processing-credit/paths/~1transactions~1refund). +## Updating Subscriptions + +You can update created subscriptions in Accept Blue as Admin via de admin-api with `UpdateOrder` permissions: + +```graphql +mutation { + updateAcceptBlueSubscription( + input: { + id: 11820 + title: "New Title For Updated Subscription" + frequency: daily + } + ) { + id + name + variantId + recurring { + interval + intervalCount + } + # ... additional subscription fields + } +} +``` + ## CORS If you run into CORS issues loading the Accept Blue hosted tokenization javascript library, you might need to remove the `cross-origin` key on your `script` tag. From 4f37db21aeceefb3068fa132411d40bee56e0790 Mon Sep 17 00:00:00 2001 From: Martijn Date: Fri, 10 Jan 2025 11:11:32 +0100 Subject: [PATCH 5/8] feat(accept-blue): pr improvements --- .../src/api/accept-blue-service.ts | 24 +++++++++---------- .../test/accept-blue.spec.ts | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/vendure-plugin-accept-blue/src/api/accept-blue-service.ts b/packages/vendure-plugin-accept-blue/src/api/accept-blue-service.ts index 440347ea6..31623b0f1 100644 --- a/packages/vendure-plugin-accept-blue/src/api/accept-blue-service.ts +++ b/packages/vendure-plugin-accept-blue/src/api/accept-blue-service.ts @@ -316,30 +316,30 @@ export class AcceptBlueService implements OnApplicationBootstrap { relations: ['order', 'productVariant'], }); const client = await this.getClientForChannel(ctx); - const subscription = await client.updateRecurringSchedule(scheduleId, { - title: input.title || undefined, - amount: input.amount || undefined, + const schedule = await client.updateRecurringSchedule(scheduleId, { + title: input.title ?? undefined, + amount: input.amount ?? undefined, frequency: input.frequency ?? undefined, - next_run_date: input.nextRunDate || undefined, - num_left: input.numLeft || undefined, - active: input.active || undefined, + next_run_date: input.nextRunDate ?? undefined, + num_left: input.numLeft ?? undefined, + active: input.active ?? undefined, receipt_email: input.receiptEmail || undefined, }); - const subcription = this.mapToGraphqlSubscription( - subscription, - orderLine.productVariant.id - ); // Write History entry on order await this.orderService.addNoteToOrder(ctx, { id: orderLine.order.id, note: `Subscription updated: ${JSON.stringify(input)}`, isPublic: true, }); + const subscription = this.mapToGraphqlSubscription( + schedule, + orderLine.productVariant.id + ); // Publish event await this.eventBus.publish( - new AcceptBlueSubscriptionEvent(ctx, subcription, 'updated', input) + new AcceptBlueSubscriptionEvent(ctx, subscription, 'updated', input) ); - return subcription; + return subscription; } /** diff --git a/packages/vendure-plugin-accept-blue/test/accept-blue.spec.ts b/packages/vendure-plugin-accept-blue/test/accept-blue.spec.ts index 1443e7f3d..a46fc8341 100644 --- a/packages/vendure-plugin-accept-blue/test/accept-blue.spec.ts +++ b/packages/vendure-plugin-accept-blue/test/accept-blue.spec.ts @@ -622,7 +622,7 @@ describe('Admin API', () => { active: false, }, }); - expect(updateRequest).rejects.toThrowError( + await expect(updateRequest).rejects.toThrowError( 'You are not currently authorized to perform this action' ); }); From a15fd320764a8c0a70be7a4d2d7b4cbdefc5db07 Mon Sep 17 00:00:00 2001 From: Martijn Date: Fri, 10 Jan 2025 11:14:17 +0100 Subject: [PATCH 6/8] feat(accept-blue): pr improvements --- packages/vendure-plugin-accept-blue/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vendure-plugin-accept-blue/CHANGELOG.md b/packages/vendure-plugin-accept-blue/CHANGELOG.md index d1303c821..091ce6f7a 100644 --- a/packages/vendure-plugin-accept-blue/CHANGELOG.md +++ b/packages/vendure-plugin-accept-blue/CHANGELOG.md @@ -5,7 +5,7 @@ # 2.0.2 (2025-01-09) -- Piublicly expose ctx in AcceptBlueTransactionEvent +- Publicly expose ctx in AcceptBlueTransactionEvent # 2.0.1 (2025-01-09) From 3ef51ed5900d924a1ba3ca9d76a49d0ae7b1624c Mon Sep 17 00:00:00 2001 From: Martijn Date: Fri, 10 Jan 2025 11:15:45 +0100 Subject: [PATCH 7/8] feat(accept-blue): pr improvements --- packages/vendure-plugin-accept-blue/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/vendure-plugin-accept-blue/README.md b/packages/vendure-plugin-accept-blue/README.md index 2d6b152eb..797d99e19 100644 --- a/packages/vendure-plugin-accept-blue/README.md +++ b/packages/vendure-plugin-accept-blue/README.md @@ -200,6 +200,8 @@ mutation { } ``` +This wil emit an `AcceptBlueSubscriptionEvent` of type `updated`. + ## CORS If you run into CORS issues loading the Accept Blue hosted tokenization javascript library, you might need to remove the `cross-origin` key on your `script` tag. From 67c7adb4f9b0e2aa72cdb863d340e9ef85c1cbe2 Mon Sep 17 00:00:00 2001 From: Martijn Date: Fri, 10 Jan 2025 14:49:30 +0100 Subject: [PATCH 8/8] feat(accept-blue): test fix --- packages/vendure-plugin-accept-blue/test/accept-blue.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vendure-plugin-accept-blue/test/accept-blue.spec.ts b/packages/vendure-plugin-accept-blue/test/accept-blue.spec.ts index a46fc8341..a297e7c8e 100644 --- a/packages/vendure-plugin-accept-blue/test/accept-blue.spec.ts +++ b/packages/vendure-plugin-accept-blue/test/accept-blue.spec.ts @@ -662,6 +662,7 @@ describe('Admin API', () => { } ); expect(updateRequest).toEqual({ + active: false, title: 'Updated title', amount: 43.21, frequency: 'biannually',