サブスクサービスのDB設計
毎月1回、固定の金額の請求をする
- 定額課金のサービスではサービス提供者から利用者へ「月額〇〇円」として毎月請求を出します。利用者は請求の支払う度にサービスを継続して利用できるようにします。
- 利用者は請求の支払う度にサービスを継続して利用できるようにします。
支払方法はクレジットカードや携帯電話の契約による支払、その他の電子決済サービスが主になると想定します。
月初請求
月初にまとめて利用者全員へ請求する方法が、システムの処理としては最もシンプルです。
この請求で必要な情報は、利用者が課金対象であるか否かだけです。
会員IDというのは、利用者のIDの属性を定義していることを表しています。
請求先は月に1回(月初であれば毎月1日)に、請求対象となる利用者を抜き出して全利用者に請求を出せばよいだけです。
一方、サービス提供画面は課金対象フラグで利用者が課金サービスを受けられるか否かを判定します。
サービス加入日毎の請求
このように、月初請求では加入日によって利用者間での不公平が問題となります。
そこで、加入日の日にちで毎月の請求をするようにして、利用者間の不公平を無くすために利用者の属性として加入日を追加します。
請求先は、毎日請求対象となる利用者を加入日の「日にち」で選んで請求を出すようにします。
例 :
請求先が12月19日の請求をするのであれば、
加入日が1月から12月までの「19日」の加入者のみを請求対象として請求処理をします。
サービス提供画面が利用者の課金サービスを受けられるか否かを判定するために、請求対象フラグはこのまま残しておきます。
このように日にちで請求対象を選ぶ請求先には注意点が一つあります。
例えば、1月31日に加入した利用者は請求先が単純に加入日と同じ日のみを請求対象としてしまうと、
翌月の2月は28日か29日までしかないため30日と31日加入の利用者は請求できなくなります。
請求先は月末の処理としてその日にちだけではなく、
それより後の日にちに加入した利用者も請求対象としなければいけません。
サービス変更の反映は次回請求日から
課金プランを変更しても、変更したサービスを適用するのは次の請求日以降とする方法です。
この方法は請求システムの処理はシンプルで利用者視点で支払った金額に対する不公平はありませんが、
プラン変更後すぐにはアップグレードしたサービスをすぐには受けられない問題があります。
継続課金の入会で初回無料
定額課金サービスには加入直後の初回1ヶ月は無料提供をとしているサービスがあります。
先払いの場合は、加入時の支払が無いと初月の一ヶ月間は無償でのサービス提供となります。
しかし、後払いの場合はそもそも加入時支払をしないのが当然で、初月分の請求は加入後1ヶ月目の請求となります。
これは初回無料ではなく利用者にとって無償ではありません。
利用者が初回無料期間中に入退会を繰り返しても、無償でサービスを使用し続けることができないようにする考慮が必要です。
例えば、
- クレジットカードで同じカード番号であれば無料期間扱いにしない
- 電話番号、デバイスのIDといった情報を保持しておいて、別会員IDだけど、同じ電話番号の場合は無料期間扱いにしない
といった対策が必要になります。
先払いでは初月無料のサービスであっても既に初月無料サービスを使用した履歴のある新規利用者は、初月無料の権利無しとして加入時に支払請求をするようにします。
なお、後払いの場合は退会時に最後の請求をするので問題ありません。
退会
先払いの場合は、退会後の支払は不要となります。既に退会直前までに利用していたサービスの利用料は支払済みだからです。
後払いの場合は、最後の支払から退会までの利用料を支払ってはいないため、退会時あるいは退会後に最後の支払が必要になります。
請求のリトライに対応する
定額課金サービスをシステム化する場合の支払処理として、
システムが自動でクレジットカード決済システムや電子決済システム、あるいは決済代行システム等との連携処理を行うものとします。
このような自動請求システムでは請求プログラムが必ず正常に動作するとは限りません。
請求プログラムが不正な動作や中断をしてしまい利用料の請求が失敗する可能性があります。
また、サービス提供側のシステムが正常に動作したとしても、対向の請求を受付ける側のシステムが正常に動作していなければやはり請求は失敗します。
これまでは請求プログラムは正常に請求を完了するケースのみを想定していましたが、
実際の運用では様々な事情で請求処理が失敗する可能性があり、それをリカバリする必要があります。
運用コストの削減や作業ミスによる事故を抑止する観点から、請求処理失敗のリカバリは請求プラログラムのリトライで解決できるのがベターです。
でなければ、失敗した請求を手動で処理するかリカバリ用のプログラムを請求プログラムとは別に用意することになります。
請求プログラムが請求対象の全利用者の請求に失敗した場合は、請求プログラムを当日中の再実行でリカバリが可能です。
しかし、再実行が翌日にずれ込んだ場合は前日分の請求はできなくなります。
これは、請求プログラムのオプション入力として対象日を指定するようにすればこの問題は回避できます。
一方で請求プログラムが一部の利用者の請求のみ失敗した場合は、
単純に請求プログラムを再実行すると前回成功した利用者には前回の請求と合わせて二重に請求をしてしまいます。
請求履歴を記録することでリトライに対応する
これを回避するシンプルな方法として、請求処理の成功記録をDBに登録するようにします。
請求プログラムで請求履歴テーブルに請求当日の請求成功履歴が無いことも請求対象の条件に加えれば、
請求プログラムが一部の利用者の請求に失敗しても、請求プログラムを再実行さえすれば未請求分の利用者の請求のみを処理できるようになります。
また、これは二重課金の抑止チェック処理にもなります。
しかし、これだけでは前日以前の未請求分はリカバリ対象にはなりません。
全ての利用者について毎日請求対象であるかどうかを加入日と履歴を比較しても良いのですが、
利用者数や請求履歴テーブルのレコード数が増えるとプログラムの実行時間がネックになる可能性があります。
利用者の次回請求日をDBに保管することでリトライに対応する
請求プログラムは次回請求日が当日以前の利用者を請求対象とするようにします。
請求プログラムが請求に成功した場合は、次回請求日を翌月の請求予定日付に更新してその利用者の請求処理を完了します。
このようにすれば、請求が成功しないと次回請求日は更新されないので、
請求プログラムを再実行すれば利用者毎に請求が成功するまで何度でもリトライが可能になります。
この設計の懸念点は、請求処理に成功した後の次回請求日の更新で失敗してしまうケースです。
請求先システムがトランザクションIDのような二重請求を抑止するインターフェースを用意していれば設計的に防ぐことができます。
そのような環境ではない場合、請求プログラムの異常発生を検知したら請求先システムとサービス提供システムの請求状況に差分が無いことを確認する必要があります。
しかし、これはどのような設計でも本来は行うべき運用です。
DBの正規化や不要な情報を保管しないようにするという原則に則れば、次回請求日をあえてDBに記録するのは冗長です。
加入日と請求履歴があれば次回請求日を算出することができるからです。
しかし、この設計により請求リトライの容易性の他にプログラムの処理で請求対象とする利用者の条件をシンプルにして、
プログラム処理の障害による誤請求の発生確率を低くする狙いがあります。
また、各利用者の請求タイミングは次回請求日のみによって決まるので、データパッチで請求の操作をしやすいという利点もあります。
支払不能利用者の強制退会に対応する
ここは解約とかになるので、運用を考慮した話になります。
請求が失敗するケースとしては、システム障害以外にも利用者の問題で支払いができなくなるケースがあります。
例えば、クレジットカードの有効期限が超過した場合や事故で停止した場合、あるいは他の電子決済についてもアカウントが停止されている場合などです。
利用者の問題で請求が失敗した場合は、サービス提供システムとしては利用者の課金サービスを停止するなどの措置を取る必要があります。
課金サービスの停止はこれまでのDB設計で実現可能です。
利用者課金情報の現在有効な情報の利用期間の終了日を請求プログラムの実行前日で更新してしまえば、
サービス提供プログラムはその利用者の有効な課金サービスが見つけられなくなるために課金利用者とは認識しなくなります。
まとめ
定額課金の仕様はシンプルなようで裏に複雑な仕様が潜んでいることが多いので、リトライ、運用面を考慮した設計が必要になります。
文章にまとめてはみたものの、いい方法が自分自身見つかっておりません。(お恥ずかしいですが...)
利用者の課金情報を分離するという意味で、あるいは複数のサービスの請求に対応するには利用者課金情報で次回請求日を定義する方が適切だと思いました。
- データの正規化という原則から外れて、プログラムの処理をシンプルにするために「次回請求日」のような冗長データを格納するようにする。
- 適用プランの変更や金額変更などの現在日付で状態が切り替わる処理は、DB設計としてはフラグやコード値ではなく期間を定義するようにする。
DB設計で悩むことが多く、時間もかけているので、色々な情報を参考にしながら対応していますが、
既存プロダクトへの影響範囲や将来を見越してある程度作成しておきたいと思っています。
良い方法があれば教えてください。
Discussion