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(stripe-subscription): Leaner, more extensible subscriptions #260

Merged
merged 26 commits into from
Nov 2, 2023

Conversation

martijnvdbrug
Copy link
Member

@martijnvdbrug martijnvdbrug commented Sep 21, 2023

Description

This PR aims to make the Stripe Subscription Plugin more extensible: each plugin-consumer should be able to define it's custom subscription use cases.

Some examples that should be supported:

  • A customer 'buys' a subscription that is $60 a month, starts at time of purchase, and requires a down payment of $25 right now
  • A customer buys a subscription that is $240 per 6 months, starts at time of purchase, auto renews after 6 months and requires no payment right now

This means we should support these types of payments:

  1. Orders with only recurring payments. These orders will use Stripe's SetupIntent: No charges are made, subscriptions are created off-session, after Stripe tells us that the SetupIntent has completed successfully
  2. Orders with both recurring and one-time payments will use PaymentIntent. Subscriptions for recurring payments are created off session after payment intent succeeds.
    ⚠️ Only one-time payments are not supported. The core Vendure Stripe plugin already does this. Use a payment eligibility checker to decide what payment method to use.

The configurable SubscriptionStrategy is now the core of this plugin, which should give a clear understanding of how subscriptions are created.

The GraphQL schema is simplified and now supports the following queries and mutations:

  • createStripeSubscriptionIntent to start
  • previewStripeSubscription to display subscriptions on the storefront
  • previewStripeSubscriptionForProduct to display subscription previews for all variants of a product.
  • Adding a subscription to cart will happen with the built-in addItemToOrder mutation, as usual.

See README for more detailed examples.

⚠️ Tests still need to be implemented

Downpayment example / guide

This example demonstrates how to create recurring down payments. For simplicity, all product variants are treated as monthly subscriptions, where their price is the monthly fee. The subscription duration is set to a fixed 12 months, but this can be made configurable per product variant (or with custom scheduling attached to a variant).

A customer can choose to make a down payment, which results in a reduced monthly price.

Prerequisites

  • subscriptionDownpayment is defined as a custom field on an order line
  1. Define a custom strategy
import {
  RequestContext,
  OrderLine,
  Injector,
  ProductVariant,
  Order,
} from '@vendure/core';
import { Subscription, SubscriptionStrategy } from '../src/';

/**
 * This strategy creates a monthly subscription + a recurring down payment based on the duration (12 by default) of the subscription:
 * * The variant's price is the price per month
 * * Down payment will be created as a subscription that renews every 12 months, based on the given duration
 */
export class DownPaymentSubscriptionStrategy implements SubscriptionStrategy {

  durationInMonths = 12;

  isSubscription(ctx: RequestContext, variant: ProductVariant): boolean {
    // This example treats all products as subscriptions
    return true;
  }

  defineSubscription(
    ctx: RequestContext,
    injector: Injector,
    productVariant: ProductVariant,
    order: Order,
    orderLineCustomFields: { [key: string]: any },
    quantity: number
  ): Subscription[] {
    return this.getSubscriptionsForVariant(
      productVariant,
      orderLineCustomFields.subscriptionDownpayment,
      this.durationInMonths
    );
  }

  previewSubscription(
    ctx: RequestContext,
    injector: Injector,
    productVariant: ProductVariant,
    customInputs: {
      subscriptionDownpayment: number;
    }
  ): Subscription[] {
    return this.getSubscriptionsForVariant(
      productVariant,
      customInputs.subscriptionDownpayment,
      this.durationInMonths
    );
  }

  private getSubscriptionsForVariant(
    productVariant: ProductVariant,
    downpayment: number,
    durationInMonths: number
  ): Subscription[] {
    const discountPerMonth = downpayment / durationInMonths;
    const subscriptions: Subscription[] = [];
    subscriptions.push(
      {
        name: `Monthly subscription - ${productVariant.name}`,
        variantId: productVariant.id,
        priceIncludesTax: productVariant.listPriceIncludesTax,
        amountDueNow: 0,
        recurring: {
          amount: productVariant.listPrice - discountPerMonth,
          interval: 'month',
          intervalCount: 1,
          startDate: new Date(),
        },
      },
    );
    if (downpayment > 0) {
      subscriptions.push(
        {
          name: `Downpayment subscription - ${productVariant.name}`,
          variantId: productVariant.id,
          priceIncludesTax: productVariant.listPriceIncludesTax,
          amountDueNow: 0,
          recurring: {
            amount: downpayment,
            interval: 'month',
            intervalCount: durationInMonths,
            startDate: new Date(),
          },
        },
      )
    }
    return subscriptions;
  }
}
  1. Install the strategy in the plugin:
StripeSubscriptionPlugin.init({
  vendureHost: 'your-public-host',
  subscriptionStrategy: new DownPaymentStrategy()
});
  1. Call the preview query:
{
  previewStripeSubscription(productVariantId: 1 , customInputs:{
    subscriptionDownpayment: 12000
  }) {
    name
    amountDueNow
    variantId
    priceIncludesTax
    recurring {
      amount
      interval
      intervalCount
      startDate
      endDate
    }
  }
}

This results in 2 subscriptions being returned for the variant: A monthly subscription and a downpayment subscription that occurs every 12 months.

  1. Add the item to order with the down payment:
mutation {
  addItemToOrder(
    productVariantId: 1, 
    quantity:1, 
    customFields:{
      subscriptionDownpayment: 12000
    }) {
    __typename
    ...on Order {
      id
      code
      lines {
        linePriceWithTax
      }
    }
  }
}

Breaking changes

Plenty, but the readme is all up to date, check it out :-)

Checklist

📌 Always:

  • I have set a clear title
  • My PR is small and contains only a single feature. (Split into multiple PR's if needed)
  • I have reviewed my own PR, fixed all typo's and removed unused/commented code

⚡ Most of the time:

  • I have added or updated test cases for important functionality
  • I have updated the README if needed

📦 For publishable packages:

@martijnvdbrug martijnvdbrug changed the base branch from main to feat/docs-website September 28, 2023 14:04
@martijnvdbrug martijnvdbrug changed the base branch from feat/docs-website to main September 28, 2023 14:04
@martijnvdbrug martijnvdbrug mentioned this pull request Nov 2, 2023
5 tasks
@martijnvdbrug martijnvdbrug marked this pull request as ready for review November 2, 2023 14:10
@martijnvdbrug martijnvdbrug changed the title feat(stripe-subscription): Extensible subscriptions feat(stripe-subscription): Leaner, more extensible subscriptions Nov 2, 2023
@martijnvdbrug martijnvdbrug merged commit 2641e32 into main Nov 2, 2023
26 checks passed
@martijnvdbrug martijnvdbrug deleted the feat/stripe-subscription-cleanup branch November 2, 2023 15:09
@coderabbitai coderabbitai bot mentioned this pull request Jun 21, 2024
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant