Skip to content

Commit

Permalink
Merge pull request #255 from Pinelab-studio/feat/stripe-subscription-…
Browse files Browse the repository at this point in the history
…proxy

Feat/stripe subscription proxy
  • Loading branch information
martijnvdbrug authored Sep 8, 2023
2 parents f7fa659 + c5020f8 commit 7d1e609
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 52 deletions.
4 changes: 4 additions & 0 deletions packages/vendure-plugin-stripe-subscription/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 1.4.0 (2023-09-08)

- Expose proxy function to retrieve all subscriptions for current channel ([#255](https://github.com/Pinelab-studio/pinelab-vendure-plugins/pull/255))

# 1.3.2 (2023-09-06)

- Fixed selecting schedules on a variant ([#253](https://github.com/Pinelab-studio/pinelab-vendure-plugins/pull/253))
Expand Down
3 changes: 1 addition & 2 deletions packages/vendure-plugin-stripe-subscription/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,7 @@ plugins: [
6. Start the Vendure server and login to the admin UI
7. Go to `Settings > Subscriptions` and create a Schedule.
8. Create a variant and select a schedule in the variant detail screen in the admin UI.
9. Create a payment method with the code `stripe-subscription-payment` and select `stripe-subscription` as handler. **
Your payment method MUST have 'stripe-subscription' in the code field**
9. Create a payment method with the code `stripe-subscription-payment` and select `stripe-subscription` as handler. You can (and should) have only 1 payment method with the Stripe Subscription handler per channel.
10. Set your API key from Stripe in the apiKey field.
11. Get the webhook secret from you Stripe dashboard and save it on the payment method.

Expand Down
2 changes: 1 addition & 1 deletion packages/vendure-plugin-stripe-subscription/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pinelab/vendure-plugin-stripe-subscription",
"version": "1.3.2",
"version": "1.4.0",
"description": "Vendure plugin for selling subscriptions via Stripe",
"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 @@ -256,7 +256,7 @@ export class StripeSubscriptionController {
}
// Validate signature
const { stripeClient } =
await this.stripeSubscriptionService.getStripeHandler(ctx, order.id);
await this.stripeSubscriptionService.getStripeContext(ctx);
if (!this.options?.disableWebhookSignatureChecking) {
stripeClient.validateWebhookSignature(request.rawBody, signature);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,10 @@ import {
Logger,
PaymentMethodHandler,
SettlePaymentResult,
UserInputError,
} from '@vendure/core';
import { RequestContext } from '@vendure/core/dist/api/common/request-context';
import { Order, Payment, PaymentMethod } from '@vendure/core/dist/entity';
import {
CancelPaymentErrorResult,
CancelPaymentResult,
} from '@vendure/core/dist/config/payment/payment-method-handler';
import {
OrderLineWithSubscriptionFields,
OrderWithSubscriptionFields,
} from './subscription-custom-fields';
import { StripeSubscriptionService } from './stripe-subscription.service';
import { StripeClient } from './stripe.client';
import { loggerCtx } from '../constants';
import { printMoney } from './pricing.helper';
import { StripeSubscriptionService } from './stripe-subscription.service';

let service: StripeSubscriptionService;
export const stripeSubscriptionHandler = new PaymentMethodHandler({
Expand Down Expand Up @@ -111,7 +99,7 @@ export const stripeSubscriptionHandler = new PaymentMethodHandler({
payment,
args
): Promise<CreateRefundResult> {
const { stripeClient } = await service.getStripeHandler(ctx, order.id);
const { stripeClient } = await service.getStripeContext(ctx);
const refund = await stripeClient.refunds.create({
payment_intent: payment.transactionId,
amount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
OrderService,
OrderStateTransitionError,
PaginatedList,
PaymentMethod,
PaymentMethodService,
ProductVariantService,
RequestContext,
Expand Down Expand Up @@ -62,11 +63,11 @@ import { hasSubscriptions } from './has-stripe-subscription-products-payment-che
import { StripeSubscriptionPayment } from './stripe-subscription-payment.entity';
import { StripeInvoice } from './types/stripe-invoice';
import { StripePaymentIntent } from './types/stripe-payment-intent';
import Stripe from 'stripe';

export interface StripeHandlerConfig {
paymentMethodCode: string;
export interface StripeContext {
paymentMethod: PaymentMethod;
stripeClient: StripeClient;
webhookSecret: string;
}

interface CreateSubscriptionsJob {
Expand Down Expand Up @@ -212,7 +213,7 @@ export class StripeSubscriptionService {
);
}
await this.entityHydrator.hydrate(ctx, line, { relations: ['order'] });
const { stripeClient } = await this.getStripeHandler(ctx, line.order.id);
const { stripeClient } = await this.getStripeContext(ctx);
for (const subscriptionId of line.customFields.subscriptionIds) {
try {
await stripeClient.subscriptions.update(subscriptionId, {
Expand Down Expand Up @@ -244,6 +245,31 @@ export class StripeSubscriptionService {
}
}

/**
* Proxy to Stripe to retrieve subscriptions created for the current channel.
* Proxies to the Stripe api, so you can use the same filtering, parameters and options as defined here
* https://stripe.com/docs/api/subscriptions/list
*/
async getAllSubscriptions(
ctx: RequestContext,
params?: Stripe.SubscriptionListParams,
options?: Stripe.RequestOptions
): Promise<Stripe.ApiListPromise<Stripe.Subscription>> {
const { stripeClient } = await this.getStripeContext(ctx);
return stripeClient.subscriptions.list(params, options);
}

/**
* Get a subscription directly from Stripe
*/
async getSubscription(
ctx: RequestContext,
subscriptionId: string
): Promise<Stripe.Response<Stripe.Subscription>> {
const { stripeClient } = await this.getStripeContext(ctx);
return stripeClient.subscriptions.retrieve(subscriptionId);
}

async createPaymentIntent(ctx: RequestContext): Promise<string> {
let order = (await this.activeOrderService.getActiveOrder(
ctx,
Expand Down Expand Up @@ -276,8 +302,21 @@ export class StripeSubscriptionService {
'Cannot create payment intent for order without shippingMethod'
);
}
const { stripeClient } = await this.getStripeHandler(ctx, order.id);
const stripeCustomer = await stripeClient.getOrCreateClient(order.customer);
// Check if Stripe Subscription paymentMethod is eligible for this order
const eligibleStripeMethodCodes = (
await this.orderService.getEligiblePaymentMethods(ctx, order.id)
)
.filter((m) => m.isEligible)
.map((m) => m.code);
const { stripeClient, paymentMethod } = await this.getStripeContext(ctx);
if (!eligibleStripeMethodCodes.includes(paymentMethod.code)) {
throw new UserInputError(
`No eligible payment method found with code \'stripe-subscription\'`
);
}
const stripeCustomer = await stripeClient.getOrCreateCustomer(
order.customer
);
this.customerService
.update(ctx, {
id: order.customer.id,
Expand Down Expand Up @@ -498,7 +537,9 @@ export class StripeSubscriptionService {
object: StripePaymentIntent,
order: Order
): Promise<void> {
const { paymentMethodCode } = await this.getStripeHandler(ctx, order.id);
const {
paymentMethod: { code: paymentMethodCode },
} = await this.getStripeContext(ctx);
if (!object.customer) {
await this.logHistoryEntry(
ctx,
Expand Down Expand Up @@ -585,7 +626,7 @@ export class StripeSubscriptionService {
`Not creating subscriptions for order ${order.code}, because it doesn't have any subscription products`
);
}
const { stripeClient } = await this.getStripeHandler(ctx, order.id);
const { stripeClient } = await this.getStripeContext(ctx);
const customer = await stripeClient.customers.retrieve(stripeCustomerId);
if (!customer) {
throw Error(
Expand Down Expand Up @@ -768,32 +809,25 @@ export class StripeSubscriptionService {
}

/**
* Get the paymentMethod with the stripe handler, should be only 1!
* Get the Stripe context for the current channel.
* The Stripe context consists of the Stripe client and the Vendure payment method connected to the Stripe account
*/
async getStripeHandler(
ctx: RequestContext,
orderId: ID
): Promise<StripeHandlerConfig> {
const paymentMethodQuotes =
await this.orderService.getEligiblePaymentMethods(ctx, orderId);
const paymentMethodQuote = paymentMethodQuotes
.filter((quote) => quote.isEligible)
.find((pm) => pm.code.indexOf('stripe-subscription') > -1);
if (!paymentMethodQuote) {
async getStripeContext(ctx: RequestContext): Promise<StripeContext> {
const paymentMethods = await this.paymentMethodService.findAll(ctx, {
filter: { enabled: { eq: true } },
});
const stripePaymentMethods = paymentMethods.items.filter(
(pm) => pm.handler.code === stripeSubscriptionHandler.code
);
if (stripePaymentMethods.length > 1) {
throw new UserInputError(
`No eligible payment method found with code 'stripe-subscription'`
`Multiple payment methods found with handler 'stripe-subscription', there should only be 1 per channel!`
);
}
const paymentMethod = await this.paymentMethodService.findOne(
ctx,
paymentMethodQuote.id
);
if (
!paymentMethod ||
paymentMethod.handler.code !== stripeSubscriptionHandler.code
) {
const paymentMethod = stripePaymentMethods[0];
if (!paymentMethod) {
throw new UserInputError(
`Payment method '${paymentMethodQuote.code}' doesn't have handler '${stripeSubscriptionHandler.code}' configured.`
`No enabled payment method found with handler 'stripe-subscription'`
);
}
const apiKey = paymentMethod.handler.args.find(
Expand All @@ -812,11 +846,10 @@ export class StripeSubscriptionService {
);
}
return {
paymentMethodCode: paymentMethod.code,
paymentMethod: paymentMethod,
stripeClient: new StripeClient(webhookSecret, apiKey, {
apiVersion: null as any, // Null uses accounts default version
}),
webhookSecret,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ interface SubscriptionInput {
*/
export class StripeClient extends Stripe {
constructor(
private webhookSecret: string,
public webhookSecret: string,
apiKey: string,
config: Stripe.StripeConfig
) {
super(apiKey, config);
}

async getOrCreateClient(
async getOrCreateCustomer(
customer: CustomerWithSubscriptionFields
): Promise<Stripe.Customer> {
if (customer.customFields?.stripeSubscriptionCustomerId) {
Expand Down
12 changes: 11 additions & 1 deletion packages/vendure-plugin-stripe-subscription/test/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
LanguageCode,
LogLevel,
mergeConfig,
RequestContextService,
} from '@vendure/core';
import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
import { StripeTestCheckoutPlugin } from './stripe-test-checkout.plugin';
Expand All @@ -24,7 +25,11 @@ import {
import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
import * as path from 'path';
import { UPSERT_SCHEDULES } from '../src/ui/queries';
import { SubscriptionInterval, SubscriptionStartMoment } from '../src';
import {
StripeSubscriptionService,
SubscriptionInterval,
SubscriptionStartMoment,
} from '../src';

// Test published version
import { StripeSubscriptionPlugin } from '../src/stripe-subscription.plugin';
Expand Down Expand Up @@ -224,4 +229,9 @@ export let clientSecret = 'test';
);
clientSecret = secret;
console.log(`Go to http://localhost:3050/checkout/ to test your intent`);

// Uncomment these lines to list all subscriptions created in Stripe
// const ctx = await server.app.get(RequestContextService).create({apiType: 'admin'});
// const subscriptions = await server.app.get(StripeSubscriptionService).getAllSubscriptions(ctx);
// console.log(JSON.stringify(subscriptions));
})();

0 comments on commit 7d1e609

Please sign in to comment.