Stripeのデザインパターン:サブスクリプションの譲渡

前書き

サブスクリプション型のSaaSサービスを利用するとき、多くの場合はアカウントを作成し、何らかのサブスクリプションを購入します。1つだけ購入する場合もあれば、複数のサブスクリプションを購入したり、組織単位で購入するケースでは、部署単位でアカウントがあり、各部署で複数のサブスクリプションを購入している、ということも考えられます。当然のことながら、そのサブスクリプションには、購入したサービスに関連するリソースが紐づいています。多くの場合、サブスクリプションをキャンセルすると、そのリソースは使用できなくなります。リソースを別のアカウントに移す場合は、別のアカウントを作成し、そちらにリソースのバックアップなどからデータを復元する作業が必要です。

解決する課題

  • 現行利用しているサービスの状態をそのままに、サブスクリプションの対象となるアカウントを変更する。

Stripeを利用したサブスクリプションの一般的な構造

多くの場合、Stripeを利用したサブスクリプションでは、サービスを購入すると、Stripe上にサブスクリプションを作成し、そのIDをサービスのリソースと関連付けます。この記事では、上のようなSaaSの構造を前提として説明を進めます。

期待する結果

Stripeのサブスクリプションを更新するAPI Update Subscription API は、 そのサブスクリプションに関連づいた カスタマー(Customer) を変更することはできません。そのため、サブスクリプションの譲渡は、新しいサブスクリプションを譲渡先のカスタマーに作成し、譲渡の対象となるサービスのリソースを新しいサブスクリプションに関連づけます。

サブスクリプションをキャンセルするケース
サブスクリプションアイテムを加算、減算するケース

サブスクリプションの譲渡方法

前述したように、サブスクリプションを譲渡するには、サブスクリプションのキャンセルと作成の操作が必要です。

  1. 引き継ぎ元のサブスクリプションをキャンセル (Cancel Subscription API / Delete subscription item API)
  2. 引き継ぎ先に同じサブスクリプションを作成 (Create Subscription API / Create subscription item API)

譲渡元 カスタマーの 保持するサブスクリプションが1つの場合は、サブスクリプションをキャンセルし、譲渡先 カスタマー に新たに同じサブスクリプションを 作成します。

譲渡元 カスタマーの 保持するサブスクリプションアイテム2つ以上のサブスクリプションアイテムがある場合は、アイテムを1つ減算します。そして、引き継ぎ先のサブスクリプションアイテムに1つ加算します。もし、引き継ぎ先が同一のサブスクリプションを持っていない場合は、新たに作成します。

実装

まずは前提となる環境をStripe上に作成します。サブスクリプションの対象となるプロダクト(product)を作成し、このプロダクトのサブスクリプションを購入したカスタマーを1つ作成しておきます。

サブスクリプションの元となるプロダクトです。

(Stripe Dashboard: Product)

サブスクリプションの譲渡元となるカスタマーです。120ユーロの年間サブスクリプションを1件保持しています。

(Stripe Dashboard: Subscription)

サブスクリプションの譲渡先となるカスタマーです。10 ユーロの月払いサブスクリプションを1件保持しています。

今回使うコードサンプルです。

Code Sample 

import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_TEST_KEY || '', {
  apiVersion: '2020-08-27'
})

async function transfer (
  targetSubscriptionId: string, // 譲渡の対象となるサブスクリプション
  targetPriceId: string, // 譲渡の対象となるサブスクリプションのPrice
  receiverCustomerId: string) // 譲渡先のカスタマー
{
  // SubscriptionItemを確認
  const subscriptionItems = await stripe.subscriptionItems.list({
    subscription: targetSubscriptionId
  })
  let priceId: string
  let current_period_end: number | null = null
  if (subscriptionItems.data.length > 1) {
    const targetItemIndex = subscriptionItems.data.findIndex(si => si.price.id === targetPriceId)
    if (targetItemIndex >= 0) {
      await stripe.subscriptionItems.del(
        subscriptionItems.data[0].id, {
          proration_behavior: 'none'
        }
      )
    } else {
      throw new Error('Subscription not found')
    }
  } else {
    // if there will be no rest after reducing item, Do to delete subscription itself. 
    const targetItemIndex = subscriptionItems.data.findIndex(si => si.price.id === targetPriceId)
    if (targetItemIndex >= 0) {
      const subscription = await stripe.subscriptions.retrieve(targetSubscriptionId)
      // Save next billing date to use configure new subscription for the customer who accepts subscription 
      current_period_end = subscription.current_period_end
      const deleted = await stripe.subscriptions.del(targetSubscriptionId, {
        prorate: false
      })
    } else {
      throw new Error('Subscription not found')
    }
  }

  // Check new customer has same subscription
  const subscriptions = await stripe.subscriptions.list({
    customer: receiverCustomerId, 
    price:  priceId
  })

  if (subscriptions.data.length > 0) {
    // if new customer has same subscription, add an item.
    const added = await stripe.subscriptionItems.create({
      subscription: subscriptions.data[0].id,
      proration_behavior: 'none'
    })
  } else {
    // if new customer doesn't have same subscription, create new subscription 
    const param: Stripe.SubscriptionCreateParams = {
      customer: receiverCustomerId,
      items: [
        {
          price: priceId
        }
      ],
    }
    if (current_period_end) {
      param.billing_cycle_anchor = current_period_end
    }
    const created = await stripe.subscriptions.create(param)
  }
}

const FORMER_SUBSCRIPTION_ID = 'sub_xxxxxxx'
const NEW_CUSTOMER_ID = 'cus_xxxxxxx'
const TARGET_PRICE_ID = 'price_xxxxxxxx'

transfer(FORMER_SUBSCRIPTION_ID, NEW_CUSTOMER_ID, TARGET_PRICE_ID)

未使用期間の扱い

サブスクリプション を引き継ぐ場合の考慮事項は、未使用期間の課金の調整です。比例配分(Prorations) が有効になっている サブスクリプション では、キャンセル時に未使用分がリファンドされます。当月もしくは、当年分に関してはすでに支払い済みであるため、サブスクリプションのキャンセル時には、比例配分を無効にするオプションを指定します。

譲渡元に対する処理

※ サブスクリプションをキャンセルするケースと、サブスクリプションアイテムを削除するケースでは、属性の名前と指定する値が異なります。

サブスクリプションをキャンセルするケース
    const deleted = await stripe.subscriptions.del(targetSubscriptionId, {
      prorate: false
    })
サブスクリプションアイテムを減らすケース
    const deleted = await stripe.subscriptionItems.del(
      subscriptionItems.data[0].id, {
        proration_behavior: 'none'
      })

譲渡先に対する処理

引き継ぎを受ける側から見ると、当月分のサブスクリプション利用料はすでに支払われているため、当月分の比例配分を発生させてはいけません。そのため譲渡先に対しても当月分の比例配分が計算されないように調整します。具体的には、サブスクリプションの次の請求サイクルを “引き継ぎ元の次の請求サイクル” にセットします。

サブスクリプションを新規に作成するケース
    const param: Stripe.SubscriptionCreateParams = {
      customer: receiverCustomerId,
      items: [
        {
          price: priceId
        }
      ],
    }
    if (current_period_end) {
      // Set the billing date to the next billing cycle Anchor.
      // This value is taken from the subscription of the transferor.
      param.billing_cycle_anchor = current_period_end 
    }
    const created = await stripe.subscriptions.create(param)
サブスクリプションアイテムを加算するケース
    const added = await stripe.subscriptionItems.create({
      subscription: subscriptions.data[0].id,
      proration_behavior: 'none'
    })

参考 Prorations

実行、確認

サンプルコードを実行したあとの状況を確認します。まずは引き継ぎ元のカスタマーです。サブスクリプションがキャンセルされ、未使用分のInvoiceは発行されていないことがわかります。

つづいて、サブスクリプションを引き受けた側のカスタマーです。年払いのサブスクリプションが1件追加されています。

Invoiceの箇所を見ると、新しく作成されたサブスクリプションに対するインボイスが即時発行されています。こちらは、BillingCycleAnchorを次の請求サイクルにセットしているため、差し引きされて0になっています。

引き渡しを受けたサブスクリプションのページで、Upcoming Invoice を見てみます。年払いのサブスクリプションを引き受けたので、次の請求日が次の年になっているのがわかります。

引き継ぎの処理としては正しく行えているようです。(引き継ぎ元、引き継ぎ先に余分なチャージが発生していない)

サンプルコードの詳解

今回実行した例では、サブスクリプションが譲渡元でキャンセルされ、譲渡先で新たに作成されるパターンでした。他のパターンについてもサンプルコードで解説をしておきます。

1. メソッドの定義

async function transfer (
  targetSubscriptionId: string, // 譲渡の対象となるサブスクリプション
  targetPriceId: string, // 譲渡の対象となるサブスクリプションのPrice
  receiverCustomerId: string) // 譲渡先のカスタマー

2. 譲渡元の処理

サブスクリプションアイテムを検索して、同一の価格設定をを持つアイテムを見つけます。アイテムが1つしかない場合でも、同一の価格設定を持つアイテムがない場合はエラーとします。(詳細はProducts and prices guide を参照)

ここで、サブスクリプションのキャンセル、サブスクリプションアイテムの減算時には、比例配分をOFFにするオプションを指定しています。

current_period_end は、新たに作成するサブスクリプションの次の支払日として設定するため、保持しておきます。

  let priceId: string
  let current_period_end: number | null = null

  if (subscriptionItems.data.length > 1) {
    const targetItemIndex = subscriptionItems.data.findIndex(si => si.price.id === targetPriceId)
    if (targetItemIndex >= 0) {
      await stripe.subscriptionItems.del(
        subscriptionItems.data[0].id, {
          proration_behavior: 'none'
        }
      )
    } else {
      throw new Error('Subscription not found')
    }
  } else {
    // if there will be no rest after reducing item, Do to delete subscription itself. 
    const targetItemIndex = subscriptionItems.data.findIndex(si => si.price.id === targetPriceId)
    if (targetItemIndex >= 0) {
      const subscription = await stripe.subscriptions.retrieve(targetSubscriptionId)
      // Save next billing date to use configure new subscription for the customer who accepts subscription 
      current_period_end = subscription.current_period_end
      const deleted = await stripe.subscriptions.del(targetSubscriptionId, {
        prorate: false
      })
    } else {
      throw new Error('Subscription not found')
    }
  }

3. 譲渡先の処理

譲渡先の処理では、まず同一の価格設定を持つサブスクリプションの存在を確認します。サブスクリプションが見つかれば、サブスクリプションを加算する処理を行い、見つからなければ、サブスクリプションを作成します。

サブスクリプションアイテムを加算する場合は、比例配分を無効にして、当月内のチャージが発生しないようにします。

サブスクリプションを作成する場合は、次の請求日(billing_cycle_anchor) に先ほど保存したサブスクリプションの次の請求日をセットします。

  // Check new customer has same subscription
  const subscriptions = await stripe.subscriptions.list({
    customer: receiverCustomerId, 
    price:  priceId
  })

  if (subscriptions.data.length > 0) {
    // if new customer has same subscription, add an item.
    const added = await stripe.subscriptionItems.create({
      subscription: subscriptions.data[0].id,
      proration_behavior: 'none'
    })
  } else {
    // if new customer doesn't have same subscription, create new subscription 
    const param: Stripe.SubscriptionCreateParams = {
      customer: receiverCustomerId,
      items: [
        {
          price: priceId
        }
      ],
    }
    if (current_period_end) {
      param.billing_cycle_anchor = current_period_end
    }
    const created = await stripe.subscriptions.create(param)
  }

補足

サブスクリプションを譲渡する場合の考慮事項と実装について解説しました。比例配分と支払い日に関しては、ビジネス要件に応じて設定を調整してください。

関連ページ