Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(accept-blue): implemented update subscription #557

Merged
merged 9 commits into from
Jan 10, 2025
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -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
/packages/vendure-plugin-accept-blue/ @is0utfitters @mschipperheyn @martijnvdbrug
7 changes: 6 additions & 1 deletion packages/vendure-plugin-accept-blue/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# 2.1.0 (2025-01-10)

- Allow updating created subscriptions via the Admin API
- Moved refunding to admin-api, and only allow with permission UpdateOrder

# 2.0.2 (2025-01-09)

- Correct ctx not being public
- Piublicly expose ctx in AcceptBlueTransactionEvent

# 2.0.1 (2025-01-09)

Expand Down
25 changes: 25 additions & 0 deletions packages/vendure-plugin-accept-blue/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
```
martijnvdbrug marked this conversation as resolved.
Show resolved Hide resolved

## 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.
Expand Down
2 changes: 1 addition & 1 deletion packages/vendure-plugin-accept-blue/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pinelab/vendure-plugin-accept-blue",
"version": "2.0.2",
"version": "2.1.0",
"description": "Vendure plugin for creating subscriptions with the Accept Blue platform",
"author": "Martijn van de Brug <[email protected]>",
"homepage": "https://pinelab-plugins.com/",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,11 +20,11 @@ export type AcceptBluePluginOptions = Required<AcceptBluePluginOptionsInput>;
@VendurePlugin({
imports: [PluginCommonModule],
adminApiExtensions: {
schema: commonApiExtensions,
resolvers: [AcceptBlueCommonResolver],
schema: adminApiExtensions,
resolvers: [AcceptBlueCommonResolver, AcceptBlueAdminResolver],
},
shopApiExtensions: {
schema: commonApiExtensions,
schema: shopApiExtensions,
resolvers: [AcceptBlueCommonResolver],
},
controllers: [AcceptBlueController],
Expand Down
Original file line number Diff line number Diff line change
@@ -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<GraphqlMutation['refundAcceptBlueTransaction']> {
return await this.acceptBlueService.refund(
ctx,
transactionId,
amount ?? undefined,
cvv2 ?? undefined
);
}

@Mutation()
@Allow(Permission.UpdateOrder)
async updateAcceptBlueSubscription(
@Ctx() ctx: RequestContext,
@Args()
{ input }: MutationUpdateAcceptBlueSubscriptionArgs
): Promise<GraphqlMutation['updateAcceptBlueSubscription']> {
return await this.acceptBlueService.updateSubscription(ctx, input);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import {
AcceptBlueCustomer,
AcceptBluePaymentMethod,
AcceptBlueRecurringSchedule,
AcceptBlueRecurringScheduleInput,
AcceptBlueRecurringScheduleCreateInput,
AcceptBlueRecurringScheduleTransaction,
CheckPaymentMethodInput,
NoncePaymentMethodInput,
AcceptBlueTransaction,
AcceptBlueWebhookInput,
AcceptBlueWebhook,
CustomFields,
AcceptBlueRecurringScheduleUpdateInput,
} from '../types';
import { isSameCard, isSameCheck } from '../util';

Expand Down Expand Up @@ -132,6 +133,26 @@ export class AcceptBlueClient {
);
}

async updateRecurringSchedule(
id: number,
input: AcceptBlueRecurringScheduleUpdateInput
): Promise<AcceptBlueRecurringSchedule> {
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<AcceptBlueRecurringScheduleTransaction[]> {
Expand Down Expand Up @@ -179,7 +200,7 @@ export class AcceptBlueClient {

async createRecurringSchedule(
customerId: number,
input: AcceptBlueRecurringScheduleInput
input: AcceptBlueRecurringScheduleCreateInput
): Promise<AcceptBlueRecurringSchedule> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const result: AcceptBlueRecurringSchedule = await this.request(
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -24,10 +15,8 @@ import { AcceptBlueService } from './accept-blue-service';
import {
AcceptBlueSubscription,
Query as GraphqlQuery,
Mutation as GraphqlMutation,
QueryPreviewAcceptBlueSubscriptionsArgs,
QueryPreviewAcceptBlueSubscriptionsForProductArgs,
MutationRefundAcceptBlueTransactionArgs,
} from './generated/graphql';

@Resolver()
Expand Down Expand Up @@ -76,21 +65,6 @@ export class AcceptBlueCommonResolver {
}));
}

@Mutation()
@Allow(Permission.Authenticated)
async refundAcceptBlueTransaction(
@Ctx() ctx: RequestContext,
@Args()
{ transactionId, amount, cvv2 }: MutationRefundAcceptBlueTransactionArgs
): Promise<GraphqlMutation['refundAcceptBlueTransaction']> {
return await this.acceptBlueService.refund(
ctx,
transactionId,
amount ?? undefined,
cvv2 ?? undefined
);
}

@ResolveField('acceptBlueHostedTokenizationKey')
@Resolver('PaymentMethodQuote')
async acceptBlueHostedTokenizationKey(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Logger,
Order,
OrderLine,
OrderService,
PaymentMethod,
PaymentMethodEvent,
PaymentMethodService,
Expand Down Expand Up @@ -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 {
Expand All @@ -61,7 +64,8 @@ export class AcceptBlueService implements OnApplicationBootstrap {
private readonly paymentMethodService: PaymentMethodService,
private readonly customerService: CustomerService,
private readonly entityHydrator: EntityHydrator,
private connection: TransactionalConnection,
private readonly orderService: OrderService,
private readonly connection: TransactionalConnection,
private eventBus: EventBus,
moduleRef: ModuleRef,
@Inject(PLUGIN_INIT_OPTIONS)
Expand Down Expand Up @@ -297,6 +301,47 @@ export class AcceptBlueService implements OnApplicationBootstrap {
return recurringSchedules;
}

async updateSubscription(
ctx: RequestContext,
input: UpdateAcceptBlueSubscriptionInput
): Promise<AcceptBlueSubscription> {
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', 'productVariant'],
});
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,
});
martijnvdbrug marked this conversation as resolved.
Show resolved Hide resolved
const subcription = this.mapToGraphqlSubscription(
subscription,
orderLine.productVariant.id
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix typo in variable name

The variable name subcription is misspelled.

-    const subcription = this.mapToGraphqlSubscription(
+    const subscription = this.mapToGraphqlSubscription(
       subscription,
       orderLine.productVariant.id
     );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const subcription = this.mapToGraphqlSubscription(
subscription,
orderLine.productVariant.id
);
const subscription = 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;
}
martijnvdbrug marked this conversation as resolved.
Show resolved Hide resolved

/**
* Resolve the subscriptions for an order line. For a placed order, this will also fetch transactions per subscription
*/
Expand Down
51 changes: 50 additions & 1 deletion packages/vendure-plugin-accept-blue/src/api/api-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const _codegenAdditions = gql`
scalar JSON
`;

export const commonApiExtensions = gql`
const commonApiExtensions = gql`
enum AcceptBlueSubscriptionInterval {
week
month
Expand Down Expand Up @@ -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!
Expand All @@ -128,12 +159,30 @@ export const commonApiExtensions = gql`
customInputs: JSON
): [AcceptBlueSubscription!]!
}
`;

export const shopApiExtensions = gql`
${commonApiExtensions}
`;

export const adminApiExtensions = gql`
${commonApiExtensions}

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!
}
`;
Loading
Loading