プレミアムプランの状態管理と決済ハンドリングの難しさ
はじめに
こんにちは、PIVOTでソフトウェアエンジニアをしている裾分です。PIVOTは2024年2月にアプリ・Webを本格始動しました。私はPIVOTにジョインして以降、サブスクリプション機能の開発をしてきたので設計の概要と決済プラットフォームが係る実装の難しさについてまとめてみました。
本題
冒頭のリリースの通り、PIVOTはYouTubeからプロダクトに集中するにあたり、サブスクリプション機能をリリースしています。
サブスクリプションを実装するにあたり考慮すべき点として、以下の状態を考慮する必要があります。
自サービスで管理する状態
ユーザーのサブスクリプション
ユーザーのプラン
他サービスで管理する状態
ユーザーへの課金を行うプラットフォームに登録されているサブスクリプションの状態
決済状態(成功 | 失敗 | …)
PIVOTの場合では、決済プラットフォームとして App Store, Play Store, Stripe の3つを利用していますが、3社でサブスクリプションの状態遷移が異なります。
設計に落とし込む際には、プラットフォーム側の状態と自サービスで管理する状態に境界を設定して整合させる必要があります。
プレミアムプランの状態管理設計
PIVOTのプレミアムプランの状態遷移は下記の通りです。
会員登録は無料プランまたはプレミアムプランを選択して会員登録でき、初回のプレミアムプラン契約時は14日間の無料期間が付与されます。14日間経過後はプレミアムプランに移行します。
PIVOTでは、上記の会員プラン遷移と決済プラットフォームのサブスクリプション状態を考慮して下記のように設計しました。
(プラットフォームからの請求に係る部分などを削除し、一部を抜粋しています。)
それぞれのエンティティの役割としては下記の通り。
会員
会員のランク(無料 | プレミアム)を管理する。
契約
会員のプレミアムプランの期間を管理する。これには、初回契約時の14日間の無料期間および1か月のプレミアムプラン期間が含まれます。
契約アクティビティ
契約に対する継続の意思表示や解約、購入などのアクティビティを記録する。
サブスクリプション
決済プラットフォームのサブスクリプションの ID を管理する。
ポイントとしては下記の3つになります。
会員のランクを決済プラットフォームで管理するサブスクリプションの状態に依存しない
会員のランクを契約の期間に依存しない
契約の期間別にユーザーまたはシステムによって行われたアクションをトレース可能にする
プレミアムプラン導入後は、会員のランクに応じた機能制御が導入される可能性があります。このため、会員ランクを決済プラットフォームの状態や契約期間の状態から切り離しています。
決済プラットフォームの状態変更や契約期間の終了時に会員ランクを更新する実装が必要になりますが、今回はその部分を省略しています。
本題(2)難しいところ
ここまではよくある設計の話でしたが、これからが決済プラットフォームにおけるサブスクリプション機能の難しい部分です。
決済プラットフォームごとにサブスクリプションの状態遷移が異なる
ライフサイクル全体では似ているものの、細部においては異なる点があります。たとえば、Play Store ではサブスクリプションの一時停止(SUBSCRIPTION_STATE_PAUSED)からアカウントの停止(SUBSCRIPTION_STATE_ON_HOLD)への状態遷移があります。Stripe やApp Store にも類似の状態遷移は存在しますが、これらの状態が各決済プラットフォームにおいてどのように扱われるかを注意深く理解し、適切に実装する必要があります。
状態遷移時に通知されるイベントを適切にハンドリングする
次に上記の状態遷移に加え、状態遷移時に通知されるイベントを適切にハンドリングする必要があります。
決済プラットフォームごとにハンドリングすべきイベントは10個程度ですが、サブスクリプションの状態を理解しつつイベントをハンドリングする必要があるためこれが想像以上に大変です。
App Store, Play Store のサブスクリプションのテストが難しい
App Store(Sandbox環境)およびPlay Store(内部テスト環境)で1ヶ月間のサブスクリプション更新をテストする際、自動更新は3〜5分の間隔で行われます。ただし、タイムトラベルのような時間を早送りする機能は提供されていません。このため、この挙動を前提にテスト計画を立てる必要があります。
決済が失敗するケースをテストする場合は、次回の更新が失敗するように、この3〜5分の間にApp StoreやPlay Storeの設定を調整する必要があります。また、イベント通知時に決済プラットフォームのサブスクリプション状態を確認する場合も、この時間内に行う必要があるため、サブスクリプションのデバッグは大変です。
サブスクリプションの状態遷移時に同時に複数のイベントが飛んでくることもある
例えばPlay Store の場合、アカウントの一時停止となったサブスクリプションが継続して決済に失敗すると期限切れとなりますが、この時に通知されるイベントは公式ドキュメントにも記載されている通り複数のイベントが通知されます。
イベントのハンドラーで最新のサブスクリプションの状態を取得している場合、通知されたイベントの種類とサブスクリプションの状態が異なるため注意が必要です。
App Store のサブスクリプションの状態はコードを書かないと見ることができない
Play Store は Web から、Stripe はダッシュボード上から個別のサブスクリプションの状態を確認することができますが、App Store はコンソール上から確認することができないため、Get Transaction Info API を叩いて状態を取得する必要があります。
iOS アプリでサブスクリプションを実装する場合は必須になるので早めに CLI などで実装しておくと良いでしょう。
月末が絡む場合、決済プラットフォームごとにの1ヶ月の考え方が異なる
サブスクリプション期間を1ヶ月としており、期間に月末が絡む場合、決済プラットフォーム側のサブスクリプション期間は下記の通りに期間が調整されます。
この違いにより、Play Store のサブスクリプションは2月を跨ぐと28日または29日に短縮されます。
Stripe | App Store
開始日が月末の場合、終了日は翌月末とする
開始日+1ヶ月のカレンダーが存在する場合、+1ヶ月を終了日とする
開始日+1ヶ月のカレンダーが存在しない場合、翌月末を終了日とする
Play Store
開始日+1ヶ月のカレンダーが存在する場合、+1ヶ月を終了日とする
開始日+1ヶ月のカレンダーが存在しない場合、翌月末を終了日とする
まとめ
ここまでサブスクリプション機能の実装の難しさを列挙してきましたが、新規で実装する場合は、決済プラットフォーム側の状態遷移とイベントを読み解いて実装するには慎重にデバッグしながらドキュメントを理解する必要があるため全てをミスなく実装するのは非常に困難です。
多少の実装ミスは想定しつつ、ユーザーに不利益となる可能性のある部分については状態の不一致となった場合にエラーログを出すようにしてエンジニア作業で修正できるようにしておくと良いでしょう。
感謝の言葉
この度のサブスクリプション機能の開発に際し、設計の面で大変お世話になった川島さん(@kawasima)に心からの感謝を申し上げます。川島さんの専門知識とご経験には大いに助けられました。ありがとうございました。
最後にいつもの
iOS アプリはこちら
Android アプリはこちら
PIVOTでは、「機械学習エンジニア」「Androidエンジニア」「映像プロデューサー」「ビジネスプロデューサー」「HRマネージャー」など幅広く募集しています。
PIVOTの技術ブログはマガジンとしてまとめています。こちらもぜひご覧ください!