サブスクリプションを誰かに引き継ぐケースを考える

こんにちは。ハグテク(いとう)です。普段はオランダに巣食いつつフリーランスでデベロッパーをしています。AWSやAlexaの界隈によく顔を出しています。どうも。

この記事は、Stripe Advent Calendar 2021 10日目の記事です。テーマは。「サブスクリプションを誰かに引き継ぐケースを考えよう」です。

それでは行ってみましょう!

前提となる環境をStripeに作る

まずは今回のお題の元になるプロダクトをStripeに作ります。

このプロダクトにサブスクリプションしたカスタマーを1人作っておきます。(Payment Link を生成してテストカードで登録するだけです。)

サブスクリプションを引き継ぐとは?

テスト環境ができたところで、引き継ぐとは具体的にStripeでどのような処理が必要なのかを考えます。前提として、 Update Subscription API で Subscription が関連づいている Customer を変更することはできません。そのため、Stripe上での処理は以下のステップを踏むことになります。

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

サブスクリプションアイテムが1つの場合は、サブスクリプションをキャンセルして追加します。

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

今回使うコードサンプルです(Typescript, Logライブラリとして、bunyan を使用しています)。

Code Sample 

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

const FORMER_SUBSCRIPTION_ID = 'sub_xxxxxxx'
const NEW_CUSTOMER_ID = 'cus_xxxxxxx'

async function transfer (
  targetSubscriptionId: string, // SubscriptionId which is trainsferred
  receiverCustomerId: string) // CustomerId which who accepts transfer
{
  // Check subscription items
  const subscriptionItems = await stripe.subscriptionItems.list({
    subscription: targetSubscriptionId
  })
  Logger.info('SubscriptionItemsCount', subscriptionItems.data.length)
  let priceId: string
  let current_period_end: number | null = null
  if (subscriptionItems.data.length > 1) {
    // if there will be rest after reducing item, Do to delete a item. 
    priceId = subscriptionItems.data[0].price.id
    Logger.info('SubscriptionPriceId', priceId)
    const deleted = await stripe.subscriptionItems.del(
      subscriptionItems.data[0].id, {
        proration_behavior: 'none'
      })
    Logger.info('SubscriptionItemsDeleted', deleted)
  } else {
    // if there will be no rest after reducing item, Do to delete subscription itself. 
    priceId = subscriptionItems.data[0].price.id
    Logger.info('SubscriptionPriceId', priceId)
    const subscription = await stripe.subscriptions.retrieve(targetSubscriptionId)
    Logger.info('SubscriptionPeriod', subscription.billing_cycle_anchor, subscription.current_period_start, subscription.current_period_end)
    // Save next billing date to use configure new subscription for the customer who accepts subscription 
    current_period_end = subscription.current_period_end
    Logger.info('SavedCurrentPeriodEnd', current_period_end)
    const deleted = await stripe.subscriptions.del(targetSubscriptionId, {
      prorate: false
    })
    Logger.info('SubscriptionDeleted', deleted.id)
  }

  // Check new customer has same subscription
  const subscriptions = await stripe.subscriptions.list({
    customer: receiverCustomerId, 
    price:  priceId
  })
  Logger.info('SameSubscriptionExist', subscriptions.data.length)

  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'
    })
    Logger.info("SubscriptionItemsAdded", added.id)
  } 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)
    Logger.info("SubscriptionsAdded", created.id)
  }
}

transfer(FORMER_SUBSCRIPTION_ID, NEW_CUSTOMER_ID)

未使用期間の扱い

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

Subscriptionそのものをキャンセルするケース
    const deleted = await stripe.subscriptions.del(targetSubscriptionId, {
      prorate: false
    })
SubscriptionItemsを減らすケース
    const deleted = await stripe.subscriptionItems.del(
      subscriptionItems.data[0].id, {
        proration_behavior: 'none'
      })

※ Subscriptionそのものをキャンセルするケースと、SubscriptionItemsを削除するケースでは、属性の名前と指定する値が異なります。

引き継ぎを受ける側は、当月分の利用料はすでに支払われているため、当月分のProrationを発生させてはいけません。そのためこちらも当月分のProrationチャージがされないように調整します。具体的には、Subscription の次の請求サイクルを “引き継ぎ元の次の請求サイクル” にセットします。

Subscriptionそのものを作成するケース
    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)
SubscriptionItemsを加算するケース
    const added = await stripe.subscriptionItems.create({
      subscription: subscriptions.data[0].id,
      proration_behavior: 'none'
    })

参考 Prorations

実行してみる

先ほど紹介したサンプルコードを実行する前に、実行前の状況を確認しておきます。まずは、引き継ぎ元 Customer の情報です。今回は、product1 (年払い、次の支払いサイクルは、2022年の12月8日) の Subscription を別の Customer に引き渡します。

つづいて、Subscription を 引き受ける側の Customer です。

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

つづいて、Subscriptionを引き受けた側のCustomerです。年払いのSubscriptionが1件追加されています。

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

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

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

まとめ

サブスクリプションの所有権を別のカスタマーに移す場合の考慮事項について実験してみました。サブスクリプションの商品を他のユーザーに譲る、というユースケースがあるSaaSであれば、参考にできるのではないでしょうか。