💰

stripeでプラン変更時の差額を計算して請求する

2023/03/17に公開

背景

あるサブスクのプランをアップグレードをするときに、もし前回の決済からあまり日数が経過していなかったら、そのまま新しく全部課金されるのは損に感じると思います。

たとえばFlutter大学というサービスにおいて

  • 3月13日13:35に2200円のコミュニティプランに入会
  • 3月17日16:51に8800円の修行プランにアップグレード

という行動をしたときに、

コミュニティプランの3/17~4/13の未使用時間分は考慮せず、新たに8800円が全部かかる仕様だと、ちょっと微妙ですよね。開発側の都合で、これまではそういう仕様でした。。。🙇

これを、ちゃんと差額を計算して必要分だけ払うという実装に変更しましたので、その仕様や具体的な方法を紹介したいと思います。

考えられる仕様

具体例があった方がいいので、下記状況をイメージした上で、考慮する変数、そして3つの実装パターン、それを日本語にしたらどうなるか、を解説します。

現在の状況

  • 3/13 13:35にコミュニティプランに入会して、2200円払っている
  • 3/13 13:35に払った分のサービス効果は4/13 13:35まで有効
  • 現在は 3/17 16:51
  • 3/17 16:51に修行プランにアップグレードする

アップグレード時に考慮する変数

  • 【⏰】請求サイクルを今からに変更する or しない(そのまま)
  • 【💰】今すぐ請求する or しない(次の決済時に請求)

3つの実装パターン

この2つの変数を踏まえて以下の3種類のプランアップグレードの実装パターンがあります。
変更するを⚡️、変更しないを☕️のように表すと、以下のようになります。

  • ①【⏰】☕️ 【💰】☕️
  • ②【⏰】☕️ 【💰】⚡️
  • ③【⏰】⚡️ 【💰】⚡️

【⏰】⚡️の場合は、【💰】も必ず⚡️になります。
請求サイクルを今からに変更するということは、今すぐ請求するからです。

それぞれのパターンの解説

①【⏰】☕️ 【💰】☕️

  • 請求サイクルを変更しない
  • 次の決済時(4/13 13:35)に日割り(3/17 16:51 ~ 4/13 13:35)の修行プランの金額を加算して請求する
  • つまり次の決済は4/13に 【3/17 ~ 4/13の日割の金額】 + 8800円

②【⏰】☕️ 【💰】⚡️

  • 請求サイクルを変更しない
  • 今日から次の決済までのコミュニティプランの未使用分」を今すぐ返却
  • 今日から次の決済までの修行プラン」の日割り料金を今すぐ請求
  • 次の決済は4/13に8800円

③【⏰】⚡️ 【💰】⚡️

  • 請求サイクルを今日から(3/17~)に変更する
  • 今日から次の決済までのコミュニティプランの未使用分」を今すぐ返却
  • 今日から1ヶ月分の修行プラン」を今すぐ請求
  • 次の決済は4/17に8800円

Flutter大学ではどれを採用したか

結論として、Flutter大学では今回③で実装しました。アップグレードしたい方には今すぐ始めてほしいし、最低1ヶ月は修行プランのサービスを活用してほしいからです。

①②についても考慮しましたので、本記事ではすべてのパターンの実装方法について解説していきたいと思います。

実装方法

参考にするドキュメントは以下です。
Proration(比例配分)の仕組みをStripeは持っているので、その仕組みをAPIを用いて実装するイメージです。

https://stripe.com/docs/billing/subscriptions/prorations

やることは2つです。

  • 比例配分をプレビューしてユーザーに示す
  • アップグレードボタンを押したらプレビューと同じ金額を正しく決済する

比例配分のプレビュー

プレビューするためには、Retrieve an upcoming invoiceのAPIを使います。

https://stripe.com/docs/api/invoices/upcoming

私の場合はFirebase Functions内にstripeのnpmをインストールし、以下のように実装しました。
それぞれのパラメータはFlutterのアプリから送る感じです。

const stripe = require('stripe')('sk_test_51H3eOmAMoVLSjIeBPlSVsoABFhukqS1Z0cXms9YVN4oN3ymmbseF5zCwVAa4ONCaVu177xWOaFxNW2t20ZQqo72r00IGDixqHK');

const invoice = await stripe.invoices.retrieveUpcoming({
  customer: customerId,
  subscription: subscriptionId,
  subscription_items: items,
  subscription_proration_date: date,
  subscription_billing_cycle_anchor: 'now',
  subscription_proration_behavior: 'create_prorations', // defaultはcreate_prorationsなので消してもOK
});

めちゃくちゃ簡略化して書くと、APIのレスポンスは以下のようなJSONになっています。

{
  "lines": {
    "data": [
      {
        "amount": -2860,
        "currency": "jpy",
        "description": "17 Mar 2023 より後の コミュニティプラン の未使用時間",
      },
      {
        "amount": 8800,
        "currency": "jpy",
        "description": "1 × Flutter修行プラン (at ¥8,800 / month)",
      },
    ],
  },
  "total": 5940,
}

このAPIレスポンスのlines.dataの配列を使って、下図のように決済前にユーザーさんに金額の確認をしてもらうという用途を想定しています。

実際のアップグレード処理

プレビューを確認してもらい、実際にアップグレードボタンを押してくれたら、下記のAPIを叩きます。

https://stripe.com/docs/api/subscriptions/update

retrieveUpcomingと同様のパラメータを与えるのが重要です。
billing_cycle_anchorproration_behaviorが違うと金額や請求サイクルなどに違いが出てきます。

const subscription = await stripe.subscriptions.update(
  subscriptionId,
  {
    'items[0][id]': subscriptionItemId,
    'items[0][price]': priceId,
    'billing_cycle_anchor': 'now',
    'proration_behavior': 'create_prorations', // defaultはcreate_prorationsなので消してもOK
  }
);

プレビューと同様の金額が以下のように決済されました。

パターン別実装方法

上記の実装と結果は③の方法のものでしたので、①と②にする場合はどこを変えればいいのか記しておきます。全部実験したので間違い無いと思います。

前提としてデフォルト値は以下なので、デフォルト値でいい場合はパラメータを指定しなくても大丈夫です。

各パラメータのデフォルト値

  • subscription_billing_cycle_anchor → 'unchanged'
  • subscription_proration_behavior → 'create_prorations'
  • billing_cycle_anchor → 'unchanged'
  • proration_behavior → 'create_prorations'

①【⏰】☕️ 【💰】☕️

プレビュー時
  • subscription_billing_cycle_anchor → 'unchanged'
  • subscription_proration_behavior → 'create_prorations'
アップグレード時
  • billing_cycle_anchor → 'unchanged'
  • proration_behavior → 'create_prorations'

subscriptions.updateをしてもその瞬間には決済が走りません。

②【⏰】☕️ 【💰】⚡️

プレビュー時
  • subscription_billing_cycle_anchor → 'unchanged'
  • subscription_proration_behavior → 'always_invoice'
アップグレード時
  • billing_cycle_anchor → 'unchanged'
  • proration_behavior → 'always_invoice'

subscriptions.updateをしてもその瞬間には決済が走りません。

③【⏰】⚡️ 【💰】⚡️

プレビュー時
  • subscription_billing_cycle_anchor → 'now'
  • subscription_proration_behavior → 請求日が現在であることが確定しているので'create_prorations'でも'always_invoice'でも変わらない
アップグレード時
  • billing_cycle_anchor → 'now'
  • proration_behavior → 請求日が現在であることが確定しているので'create_prorations'でも'always_invoice'でも変わらない

このとき、subscriptions.updateのAPIが呼ばれる同時に決済が走ります。

④【⏰】⚡️ 【💰】⚡️ 【🍕】🙅‍♂️

プレビュー時

日割りしないので特にやる意味なし

アップグレード時
  • billing_cycle_anchor → 'now'
  • proration_behavior → 'none'

日割りせず今日から上書きリセットする場合はこれです。

まとめ

以上。

このプラン変更機能を実装したサービスがFlutter大学です。使いやすい最強のサービスを目指して、日々頑張っています!

https://flutteruniv.com/

ちなみにカスタマーポータルでもできます

ちなみに、こんな難しいAPIの実装をしなくても、①と②の実装方法については、stripeコンソールからカスタマーポータルの設定をしてあげれば、リンクを作成するだけで、対応できます。(ただ、UIの自由度は低いです。)

Flutter大学

Discussion