こんにちは。ハグテク(いとう)です。普段はオランダに巣食いつつフリーランスでデベロッパーをしています。AWSやAlexaの界隈によく顔を出しています。どうも。
この記事は、Stripe Advent Calendar 2021 10日目の記事です。テーマは。「サブスクリプションを誰かに引き継ぐケースを考えよう」です。
それでは行ってみましょう!
前提となる環境をStripeに作る
まずは今回のお題の元になるプロダクトをStripeに作ります。
このプロダクトにサブスクリプションしたカスタマーを1人作っておきます。(Payment Link を生成してテストカードで登録するだけです。)
サブスクリプションを引き継ぐとは?
テスト環境ができたところで、引き継ぐとは具体的にStripeでどのような処理が必要なのかを考えます。前提として、 Update Subscription API で Subscription が関連づいている Customer を変更することはできません。そのため、Stripe上での処理は以下のステップを踏むことになります。
- 引き継ぎ元のサブスクリプションをキャンセル (Cancel Subscription API / Delete subscription item API)
- 引き継ぎ先に同じサブスクリプションを作成 (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
パターン1、サブスクリプションの追加、削除による引き継ぎ
先ほど紹介したサンプルコードを実行する前に、実行前の状況を確認しておきます。まずは、引き継ぎ元 Customer の情報です。今回は、product1 (年払い、次の支払いサイクルは、2022年の12月8日) の Subscription を別の Customer に引き渡します。
つづいて、Subscription を 引き受ける側の Customer です。
サンプルコードを実行したあと、状況を確認します。まずは引き継ぎ元のCustomerです。Subscriptionがキャンセルされ、未使用分のInvoiceは発行されていないことがわかります。
つづいて、Subscriptionを引き受けた側のCustomerです。年払いのSubscriptionが1件追加されています。
Invoiceの箇所を見ると、新しく作成されたSubscriptionに対する Invoiceが即時発行されています。こちらは、BillingCycleAnchorを次の請求サイクルにセットしているため、差し引きされて0になっています。
引き渡しを受けたSubscriptionのページで、Upcoming Invoice を見てみます。年払いのサブスクリプションを引き受けたので、次の請求日が次の年になっているのがわかります。
引き継ぎの処理としては正しく行えているようです。(引き継ぎ元、引き継ぎ先に余分なチャージが発生していない)
パターン2、サブスクリプションアイテムの加算、減算による引き継ぎ
サブスクリプションアイテムの加算、減算による引き継ぎのパターンを考えて見ます。
まとめ
サブスクリプションの所有権を別のカスタマーに移す場合の考慮事項について実験してみました。サブスクリプションの商品を他のユーザーに譲る、というユースケースがあるSaaSであれば、参考にできるのではないでしょうか。