Stripe Billingで、サブスクリプションのプラン・料金変更後の請求金額を事前にプレビューする


Stripe Billingでサブスクリプションを提供している場合、契約期間中のプラン変更で「使った分だけ支払う」決済ができます。Stripeではこれを「比例配分」とよんでいます。

例えば「月1,000円のプラン」を途中で「月2,000円のプラン」に変更する場合、比例配分が有効になっていると、以下のような請求書が作られます。

  • 1: 月1,000円のプランの未使用分: ▲ 743円
  • 2: 月2,000円のプランの使用分:  1,485円
  • 3: 次回の契約期間の利用料金: 2,000円

この請求のうち、1と2が比例配分によるものです。
1は、「変更前プランの、プラン変更日から契約期間終了日までの日割料金」を請求から差し引いています。
そして2では、、「変更後プランの、プラン変更日から契約期間終了日までの日割料金」を新しく請求します。
3はこの次の契約期間の利用料金で、この3つを次の契約期間の請求書で決済することになります。(1と2だけ即時決済することも可能)

これにより、「プラン変更日前までは月1,000円のプラン、変更日からは月2,000円のプラン」の料金のみを顧客が支払ったことにできます。

比例配分を有効化すると、次回の請求金額の見積もりが難しくなる

この比例配分があることで、顧客は「使った分だけ払えば良い」ことになり、プランのアップグレードへの懸念を減らすことができます。

ただし、「プランをいつ更新するか」によって次回の請求金額が変わるため、プラン変更画面で「次回の請求金額」を顧客に提示することが難しくなる問題が発生します。

また、Stripeでは日割り計算を秒単位で行うため、自前で金額計算の実装は難易度が高くなります。

InvoicesのRetrieve Upcoming APIを利用する

この比例配分された金額を計算するために利用できるのが、InvoicesのRetrieve Upcoming APIです。

このAPIを利用することで、「サブスクリプションの次回請求予定金額」を取得することができます。

const upcoming = await stripe.invoices.retrieveUpcoming({
   customer: 'cus_xxx',
   subscription: 'sub_xxxx',
})

このコードでは、指定した顧客・サブスクリプションの次回請求予定金額を取得します。
もし、プラン変更後の金額をプレビューしたい場合は、以下のように変更後のitemsを設定しましょう。

      const customerId = 'cus_xxx'
      const subscriptionId = 'sub_xxxx'
+      const subscription = await stripe.subscriptions.retrieve(subscriptionId)
+      const subscriptionItems = [...subscription.items.data.map(item => ({
+        id: item.id,
+        deleted: true,
+      })),{
+        price: 'price_xxxx',
+        quantity: 1
+      }]
      const upcoming = await stripe.invoices.retrieveUpcoming({
        customer: customerId,
        subscription: subscriptionId,
+        subscription_items: subscriptionItems,
      })

この際に注意が必要なのは、「変更前の料金データを、明示的に削除する」必要があることです。

上のコードでは、先に現在のサブスクリプションデータを取得し、契約中のプランを全て削除する操作が実装されています。

const subscription = await stripe.subscriptions.retrieve(subscriptionId)
const subscriptionItems = [...subscription.items.data.map(item => ({
  id: item.id,
  deleted: true,
})),{
  price: 'price_xxxx',
  quantity: 1
}]

もし削除せず、新しい料金データの設定だけを行うと、「追加でこの料金も契約する」と判断されますのでご注意ください。

プレビュー金額と一致させるため、サブスクリプションの更新タイミングを明示的に指定する

APIによって「いまプランを変更した場合、次回の請求がどうなるか」をプレビューできるようになりました。しかしまだ問題は残っています。

それは、「比例配分は秒単位で実施されるため、プレビューの金額と実際の請求額が完全に一致しない」問題です。

プレビューと更新を同じタイミングで実施することは不可能です。
そのため、「この時間に更新した場合、次回の請求はこの内容です」のように、「更新タイミングを指定」する必要があります。

Stripeで更新タイミングを指定するには、proration_dateを利用します。

以下のサンプルコードでは、Day.jsを利用し、「その日の終わりにプラン変更を実行する」設定を行なっています。

      const customerId = 'cus_xxx'
      const subscriptionId = 'sub_xxxx'
+      const prorationDate = dayjs().endOf('date')
      const subscription = await stripe.subscriptions.retrieve(subscriptionId)
      const subscriptionItems = [...subscription.items.data.map(item => ({
        id: item.id,
        deleted: true,
      })),{
        price: 'price_xxxx',
        quantity: 1
      }]

      // 請求書のプレビューを取得する場合
      const upcoming = await stripe.invoices.retrieveUpcoming({
        customer: customerId,
        subscription: subscriptionId,
        subscription_items: subscriptionItems,
-        subscription_proration_date: prorationDate
+        subscription_proration_date: prorationDate.unix()
      })

      // サブスクリプションを実際に更新する場合
      await stripe.subscriptions.update(subscriptionId, {
+        proration_date: prorationDate.unix(),
        items: subscriptionItems,
      })

プレビューの明細を確認する

APIで取得したデータには、合計金額だけでなく明細も含まれています。
明細の表示には、upcoming.lines.dataのデータを利用します。

      const upcoming = await stripe.invoices.retrieveUpcoming({
        customer: customerId,
        subscription: subscriptionId,
        subscription_items: subscriptionItems,
        subscription_proration_date: prorationDate.unix()
      })

      let amount = 0
      upcoming.lines.data.forEach((line, i ) => {
        console.log(`[${i}]${line.description} - ${line.amount}`)
        amount = amount + line.amount
      })

      console.log({
        amount,
        upcomingAmount: upcoming.amount_due,
      })

このコードを実行すると、明細と次回請求金額のプレビューが表示されます。

  console.log
    [0]Unused time on Starter plan after 15 Feb 2022 - -814

  console.log
    [1]Remaining time on Business plan after 15 Feb 2022 - 1221

  console.log
    [2]1 × Business plan (at ¥1,500 / month) - 1500


  console.log
    { amount: 1907, upcomingAmount: 1907 }

明細には以下の3種類が含まれています。

  • Unused: 変更前のプランの未使用分(変更予定日から本来の更新予定日までの日割り分)
  • Remaining: 変更後のプランの使用分(変更予定日から本来の更新予定日までの日割り分)
  • 変更後のプラン: 次回のサイクルの請求金額

after 15 Feb 2022部分が、更新予定日の日付です。
現時点では、Stripe側では明細内容は完全に翻訳されません。
そのため、顧客からの問い合わせが想定される場合には、あらかじめヘルプドキュメントなどを作成しましょう。

またここで表示されている金額を全て足し合わせると、upcoming.amount_dueの金額と一致します。
もし0円を下回る場合は、0円として処理され、マイナスの金額はその次以降の請求内容と相殺されます。

契約期間(インターバル)が異なる料金への変更

月額から年額への変更などでも、「subscription.update APIが実行可能であれば」プレビューを取得できます。
1つのサブスクリプションには、1つの契約期間しか設定できません。
そのため、契約期間の異なる料金へ変更する場合は、必ずそれまで利用していた料金全てを削除する必要があります。

      const subscription = await stripe.subscriptions.retrieve(subscriptionId)
      const subscriptionItems = [...subscription.items.data.map(item => ({
        id: item.id,
        deleted: true,
      })),{
        price: 'price_xxxx',
        quantity: 1
      }]
      const upcoming = await stripe.invoices.retrieveUpcoming({
        customer: customerId,
        subscription: subscriptionId,
        subscription_items: subscriptionItems,
      })

「複数の契約期間が混在する状態」ですと、以下のようにエラーが発生しますので、ご注意ください。

StripeInvalidRequestError: Currency and interval fields must match across all plans on this subscription. Found mismatch in interval field.

従量課金プランへの変更について

従量課金プランへの変更についても、「更新可能な設定」であればプレビューができます。

ただし、従量課金プランの場合、使用量計測前の金額でプレビューされるため、あまり意味を成さないことにご注意ください。

段階的・数量ベースで、定額課金も含むプランの場合

基本的には、どのプランでもプレビューは可能です。

ただし、設定によっては、請求書明細の行数が長くなる場合がありますので、デザインやCSSの設定にはご注意ください。

関連ドキュメント

[PR] Stripe開発者向け情報をQiitaにて配信中!

  • [Stripe Updates]:開発者向けStripeアップデート紹介・解説
  • ユースケース別のStripe製品や実装サンプルの紹介
  • Stripeと外部サービス・OSSとの連携方法やTipsの紹介
  • 初心者向けのチュートリアル(予定)

など、Stripeを利用してオンラインビジネスを始める方法について週に2〜3本ペースで更新中です。

-> Stripe Organizationsをフォローして最新情報をQiitaで受け取る