Help us understand the problem. What is going on with this article?

サブスクリプション(定額課金)サービスのDB設計

本稿ではサブスクリプションサービスを提供するシステムの裏に潜む仕様を考察しながら、DBの設計を中心として設計を考察していきます。
サブスクリプションサービスは日本語で「定期購読」「会費」等と訳しますが、購読以外にも定期的に一定金額を自動で請求するシステム全般をさして「定額課金」とします。
本稿ではこれ以降、サブスクリプションについて「定額課金」と記します。

1. 毎月1回、固定の金額の請求をする

定額課金のサービスではサービス提供者から利用者へ「月額〇〇円」として毎月請求を出します。利用者は請求の支払う度にサービスを継続して利用できるようにします。

支払方法はクレジットカードや携帯電話の契約による支払、その他の電子決済サービスが主になると想定します。
支払方法の一つとして、利用者が毎月口座振込をする方法もありますがここでは省き、システム的に処理できる支払方法のみを想定します。
このようにシステムで自動請求をする場合は、DBのデータとして請求処理に必要な情報を格納する必要があります。
定額課金の場合、毎月の請求の度に利用者に請求情報の入力をしなくても済むように、クレジットカード決済では請求に必要なクレジットカード番号等の情報が必要です。しかし、クレジットカード番号をデータとして取り扱うには「クレジットカード番号等取扱契約締結事業者」としての登録を受ける必要があります。
上記の問題に加えて、クレジットカード各社への対応やクレジットカード決済以外の電子決済への対応も考慮すると、決済代行サービスを利用する方が現実的です。
このような決済代行サービスでは都度支払の他に月額固定請求を代行するサービスを用意しているケースがあります。決済代行サービスが提供する月額固定請求を利用するケースを本稿では考慮していません。

本稿で想定する定額課金サービスのサービス提供システムの構成を示します。

図1システムと請求先の関連.png

サービス提供システムには「請求プログラム」と「サービス提供プログラム」とデータを格納する「DB」があります。
「請求プログラム」は定期的に実行して、利用者に課金サービス分の支払を請求先にリクエストします。
「サービス提供プログラム」は利用者がサービスを利用する度に実行され、利用者に課金サービスを利用する権利があるかを判定して利用可能な場合のみサービスを提供します。

1-1. 月初請求

月初にまとめて利用者全員へ請求する方法が、システムの処理としては最もシンプルです。
この請求で必要な情報は、利用者が課金対象であるか否かだけです。

図2利用者テーブルに課金対象フラグ列を追加.png

上の図で利用者の利用者ID下の二番目の属性が「:」となっているのは、利用者というテーブルについてIDや課金関連の属性以外にもサービス固有の属性を定義していることを表しています。

請求プログラムは月に1回(月初であれば毎月1日)に、請求対象となる利用者を抜き出して全利用者に請求を出せばよいだけです。
一方、サービス提供プログラムは課金対象フラグで利用者が課金サービスを受けられるか否かを判定します。

図2.5月末請求.png

上の図は左から右へ時間が流れるとして、利用者①と利用者②で加入日によらず毎月月初に定額課金の請求をしていることを表しています。1月15日と1月31日の加入では、支払う金額は同じでもサービスが提供される期間が異なることになります。

1-2. サービス加入日毎の請求

このように、月初請求では加入日によって利用者間での不公平が問題となります。
そこで、加入日の日にちで毎月の請求をするようにして、利用者間の不公平を無くすために利用者の属性として加入日を追加します。

図3利用者テーブルに加入日列を追加.png

請求プログラムは、毎日請求対象となる利用者を加入日の「日にち」で選んで請求を出すようにします。
例えば、請求プログラムが12月19日の請求をするのであれば、加入日が1月から12月までの「19日」の加入者のみを請求対象として請求処理をします。
サービス提供プログラムが利用者の課金サービスを受けられるか否かを判定するために、請求対象フラグはこのまま残しておきます。

このように日にちで請求対象を選ぶ請求プログラムには注意点が一つあります。
例えば、1月31日に加入した利用者は請求プログラムが単純に加入日と同じ日のみを請求対象としてしまうと、翌月の2月は28日か29日までしかないため30日と31日加入の利用者は請求できなくなります。
請求プログラムは月末の処理としてその日にちだけではなく、それより後の日にちに加入した利用者も請求対象としなければいけません。

図41月31日の利用者の2月3月の請求日.png

2. 複数の課金プランに対応する

サービスによっては複数グレードの課金プランを利用者が選択できるようにする要件があるかもしれません。
この場合、課金プランの種類を利用者の属性として持つ必要があります。
これまでは請求金額を請求プログラム内で定義する前提でDBには保管していませんでしたが、課金プラン毎の請求金額を定義するための課金プラン情報を格納するテーブルを「課金プランマスタ」として新たに定義します。

図5課金プランマスタ追加.png

上の図で利用者と課金プランマスタの間にある水色の矢印はテーブル間の関連を表します。矢印が向き元から向き先で1:Nの関係であることを表します。

これで複数プランに対応できたかのように見えますが、実はまだ問題があります。

利用者が課金プランの変更できるとして、変更した課金プランによるサービスを利用者が受けられるのはいつからでしょうか?

仮に10月15日に「月額500円」のプランを選択して加入した利用者は、次の請求は11月15日となります。
このサービスは「月額500円」の他にも「月額1,000円」の課金プランも用意されていて、利用者は加入中に「月額1,000円」と「月額5,00円」を自由に選択できるものとします。
10月15日に「月額500円」で加入した利用者が、翌日の10月16日に「月額1,000円」に課金プランを変更して直後から「月額1,000円」のサービス提供を受けられるとすると、11月15日の請求は1,000円でしょうか?500円でしょうか?
また、10月16日に「月額1,000円」変更したものを11月14日に「月額500円」に戻した場合は11月15日の請求額はどうあるべきでしょうか?
請求日当日の課金プランで請求金額を決めてしまうと、それまでにより高額なプランを使用した実績があってもその料金を請求できません。利用者視点では「お得な裏技」となりますが、サービス提供視点では「取りっばぐれ」となります。

図6請求日とアップグレード、ダウングレード.png

この解決方法として三通りの方法を以下に挙げます。

2-1. サービス変更の反映は次回請求日から

一つ目の方法は、課金プランを変更しても、変更したサービスを適用するのは次の請求日以降とする方法です。

この方法は請求システムの処理はシンプルで利用者視点で支払った金額に対する不公平はありませんが、プラン変更後すぐにはアップグレードしたサービスをすぐには受けられない問題があります。

図7課金プラン変更後、サービスの変更は次回請求日.png

2-2. 最も高い課金プランで翌月に請求

二つ目の方法は、請求日の間で選択した最も高い課金プランで翌月に請求をしてしまう方法です。

これはサービス提供側の視点として最も「取りっぱぐれ」がありませんが、利用者視点では金額的に不利なルールです。ただし、利用者は課金プランを選択した直後からそのサービスを受けられる利点はあります。
一つ目と二つ目の中間の方法として、「月額5,00円」から「月額1,000円」のようなアップグレードは即時対応として、「月額1,000円」から「月額5,00円」は一つ目の翌月対応とする方法もあります。

図8一ヶ月内で最も高い課金プランで翌月清算.png

2-3. 従量制課金

三つ目の方法は、翌月の請求で利用した課金プラン分の時間に応じて請求額を決定する方法です。

利用者は好きな時に課金プランを変更してサービスの提供を受け、それぞれの課金プランが適用された期間分だけの料金を支払います。
これは定額課金ではなく、云わば従量制課金に近い課金方法になります。
利用者にとっては最も公平な課金方法ですが、システム処理が複雑になるのと、利用者にとっては請求額の明細を提示されないと請求額の根拠を理解できない可能性があります。

図9課金プラン変更後の度に利用料を積算.png

2-4. 課金プランの変更を考慮したDB設計

上記の三つの方法はシステムの複雑性ではなく、サービス特性によりどの課金方式を採用するかを決めることになります。例えばクラウドサービスの仮想サーバ提供サービスであれば「2-3. 従量制課金」が好ましいでしょうし、動画やコンテンツを無制限で配信するサービスでは利用者がサービスを利用するときだけ課金プランを引き上げていては売上にならないので「2-1. サービス変更の反映は次回請求日から」か「2-2. 最も高い課金プランで翌月に請求」の課金方式を採用するのが適切です。
いずれにせよ、これまでのDB設計ではこれらの課金方式の処理に対応するには十分な情報を持っていません。ここでは、全ての方法に耐えうるDB設計として以下の様にします。

図10利用者情報から利用者課金情報を分割.png

これまでは利用者の情報と定額課金に関する情報は1:1の関係であるため、利用者の属性として定額課金に必要な情報を定義していましたが、これからは一つの利用者に対して選択した課金プランを履歴的に持つ必要があるため、利用者と1:N関係の利用者課金情報というテーブルを定義して利用者とリレーションを持つようにしています。
利用者課金情報テーブルは利用者のIDと利用期間を主キーにします。しかし、大抵のRDBMSは「期間」というデータ型は無いため利用期間の開始日を主キーとします。利用者課金上で、同一利用者の利用期間が重複しないようにするのは、DB側ではなくプログラム側(DBの利用者側)の責務となります。

利用者課金情報テーブルを各プログラムがどのように扱うかを考えます。
請求プログラムはこれまで通り、利用者の加入日から請求対象の利用者を選びます。請求金額は課金プランIDに紐づく課金プランマスタの金額で決定しますが、利用者課金情報テーブルのどの情報の課金プランIDで料金を決めるかは課金仕様によります。

  • 「2-1. サービス変更の反映は次回請求日から」では、請求日が利用期間に含まれる利用者課金情報で課金プランの請求金額が当月の請求額となります。
  • 「2-2. 最も高い課金プランで翌月に請求」では、利用者課金情報テーブルのその利用者の過去一ヶ月間の情報を全て参照して、最も高い課金プランの請求金額が当月の請求額となります。
  • 「2-3. 従量制課金」でも同じように利用者課金情報テーブルのその利用者の過去一ヶ月間の情報を全て参照しますが、利用していた期間と金額を積算して請求金額を算出します。

一方でサービス提供プログラムは、利用期間中の利用者課金情報を取得して提供対象の課金サービスを識別するようにします。

3. このサービスは先払いなのか?後払いなのか?

定額課金には先払いと後払いがあります。
具体的に先払いと後払いには以下の違いがあります。

  • 先払いサービスとした場合、利用者は毎月の請求でお金を支払うとそれから一ヶ月間はサービス提供のを受けられます。
  • 後払いサービスとした場合、利用者は一ヶ月間サービスの提供を受けた結果として最後に一ヶ月分の利用料を払います。

ガスや電気などの公共料金の場合、利用量に合わせた従量課金制となるため、必然的に支払は後払いとなります。
同様の理由で課金方式に「2-3. 従量制課金」を採用した場合も後払いとするのが適切です。
しかし、月額料金が固定であれば先払いでサービスを提供する方が、サービス提供者にとっては「取りっぱぐれ」がないサービスになります。
利用者は先払いか後払いかについてあまり意識をしないかもしれませんが、利用者にとっても入会時と退会時の支払が必要なのか否かに関わるので実は重要な問題です。

3-1. 入会(初回無料)

定額課金サービスにはクーリングオフ期間の意味もあり、加入直後の初回1ヶ月は無料提供をとしているサービスがあります。
先払いの場合は、加入時の支払が無いと初月の一ヶ月間は無償でのサービス提供となります。
しかし、後払いの場合はそもそも加入時支払をしないのが当然で、初月分の請求は加入後1ヶ月目の請求となります。これは初回無料ではなく利用者にとって無償ではありません。

先払いで初月を無料とする場合は注意点が一つあります。

利用者が初回無料期間中に入退会を繰り返しても、無償でサービスを使用し続けることができないようにする考慮が必要です。
先払いでは初月無料のサービスであっても既に初月無料サービスを使用した履歴のある新規利用者は、初月無料の権利無しとして加入時に支払請求をするようにします。
なお、後払いの場合は退会時に最後の請求をするので問題ありません。

図11入退会を繰り返しても初月無料が適用されないようにする.png

3-2. 退会

先払いの場合は、退会後の支払は不要となります。既に退会直前までに利用していたサービスの利用料は支払済みだからです。
後払いの場合は、最後の支払から退会までの利用料を支払ってはいないため、退会時あるいは退会後に最後の支払が必要になります。

4. 請求のリトライに対応する

定額課金サービスをシステム化する場合の支払処理として、システムが自動でクレジットカード決済システムや電子決済システム、あるいは決済代行システム等との連携処理を行うものとします。
このような自動請求システムでは請求プログラムが必ず正常に動作するとは限りません。請求プログラムが不正な動作や中断をしてしまい利用料の請求が失敗する可能性があります。
また、サービス提供側のシステムが正常に動作したとしても、対向の請求を受付ける側のシステムが正常に動作していなければやはり請求は失敗します。

これまでは請求プログラムは正常に請求を完了するケースのみを想定していましたが、実際の運用では様々な事情で請求処理が失敗する可能性があり、それをリカバリする必要があります。
運用コストの削減や作業ミスによる事故を抑止する観点から、請求処理失敗のリカバリは請求プラログラムのリトライで解決できるのがベターです。でなければ、失敗した請求を手動で処理するかリカバリ用のプログラムを請求プログラムとは別に用意することになります。

請求プログラムが請求対象の全利用者の請求に失敗した場合は、請求プログラムを当日中の再実行でリカバリが可能です。しかし、再実行が翌日にずれ込んだ場合は前日分の請求はできなくなります。これは、請求プログラムのオプション入力として対象日を指定するようにすればこの問題は回避できます。
一方で請求プログラムが一部の利用者の請求のみ失敗した場合は、単純に請求プログラムを再実行すると前回成功した利用者には前回の請求と合わせて二重に請求をしてしまいます。

4-1. 請求履歴を記録することでリトライに対応する

これを回避するシンプルな方法として、請求処理の成功記録をDBに登録するようにします。

図12請求履歴テーブルを追加.png

請求プログラムで請求履歴テーブルに請求当日の請求成功履歴が無いことも請求対象の条件に加えれば、請求プログラムが一部の利用者の請求に失敗しても、請求プログラムを再実行さえすれば未請求分の利用者の請求のみを処理できるようになります。また、これは二重課金の抑止チェック処理にもなります。
しかし、これだけでは前日以前の未請求分はリカバリ対象にはなりません。
全ての利用者について毎日請求対象であるかどうかを加入日と履歴を比較しても良いのですが、利用者数や請求履歴テーブルのレコード数が増えるとプログラムの実行時間がネックになる可能性があります。

4-2. 利用者の次回請求日をDBに保管することでリトライに対応する

そこで、請求プログラムが請求対象の利用者を抽出する条件を見直して以下のようにします。

図13次回請求日を追加.png

これまでは、利用者情報の加入日を条件に請求対象のユーザを検索していましたが、それは止めて請求プログラムは次回請求日が当日以前の利用者を請求対象とするようにします。
請求プログラムが請求に成功した場合は、次回請求日を翌月の請求予定日付に更新してその利用者の請求処理を完了します。
このようにすれば、請求が成功しないと次回請求日は更新されないので、請求プログラムを再実行すれば利用者毎に請求が成功するまで何度でもリトライが可能になります。

この設計の懸念点は、請求処理に成功した後の次回請求日の更新で失敗してしまうケースです。
請求先システムがトランザクションIDのような二重請求を抑止するインターフェースを用意していれば設計的に防ぐことができます。そのような環境ではない場合、請求プログラムの異常発生を検知したら請求先システムとサービス提供システムの請求状況に差分が無いことを確認する必要があります。しかし、これはどのような設計でも本来は行うべき運用です。

DBの正規化や不要な情報を保管しないようにするという原則に則れば、次回請求日をあえてDBに記録するのは冗長です。加入日と請求履歴があれば次回請求日を算出することができるからです。
しかし、この設計により請求リトライの容易性の他にプログラムの処理で請求対象とする利用者の条件をシンプルにして、プログラム処理の障害による誤請求の発生確率を低くする狙いがあります。
また、各利用者の請求タイミングは次回請求日のみによって決まるので、データパッチで請求の操作をしやすいという利点もあります。

5. 支払不能利用者の強制退会に対応する

「4. 請求のリトライに対応する」では主にシステム障害による請求失敗時の考慮を設計に盛り込みました。

請求が失敗するケースとしては、システム障害以外にも利用者の問題で支払いができなくなるケースがあります。例えば、クレジットカードの有効期限が超過した場合や事故で停止した場合、あるいは他の電子決済についてもアカウントが停止されている場合などです。

利用者の問題で請求が失敗した場合は、サービス提供システムとしては利用者の課金サービスを停止するなどの措置を取る必要があります。課金サービスの停止はこれまでのDB設計で実現可能です。利用者課金情報の現在有効な情報の利用期間の終了日を請求プログラムの実行前日で更新してしまえば、サービス提供プログラムはその利用者の有効な課金サービスが見つけられなくなるために課金利用者とは認識しなくなります。

6. 金額の変更に対応する

日本では2019年10月で消費税が8%から10%に引き上げられました。それにより、全国の値札は一斉に書き換えられたと思います。

消費税の変更に限らずサービスの値段がサービス提供期間中に変更になるのは稀にあることです。
これまでの設計では値段の切替えは課金プランマスタテーブルの更新により実現できますが、値段の切替えを厳密に〇月〇日の午前0時に行いたいとしたい場合には、その時刻に課金プランマスタを更新するか時限処理を設定しておく必要があります。
しかし、直接更新するにせよ時限処理にせよ、そのタイミングで更新に失敗すると更新失敗からリトライで更新に成功するまでの期間は不正な価格で請求をすることになります。
このようなことを防ぐために課金プランマスタに有効期間を設けて、請求プログラムは請求日に対して有効な期間の情報の請求金額で請求をするようにします。

図14課金プランマスタに有効期間を追加.png

この方式は「4. 請求のリトライに対応する」で挙げたシステム障害による請求リトライでも、請求日と実行日がずれた場合の金額差異を防ぐ効果もあります。

7.最後に

定額課金の仕様はシンプルなようで裏に複雑な仕様が潜んでいることが多く、今回の記事を執筆しました。

実際のシステムでは本稿で挙げた設計では十分ではなく、課金仕様を細かく詰めたりにプログラムの設計を詳細化するとさらに細かい問題が現れて設計を見直すことになるはずです。
例えば、今回の記事では説明の流れで利用者テーブルに次回請求日属性を定義しましたが、利用者の課金情報を分離するという意味で、あるいは複数のサービスの請求に対応するには利用者課金情報で次回請求日を定義する方が適切です。

本稿のテーマとしてDB設計がありました。
この記事での考察を通してDB設計について2点の提案があります。

  • データの正規化という原則から外れて、プログラムの処理をシンプルにするために「次回請求日」のような冗長データを格納するようにする。
  • 適用プランの変更や金額変更などの現在日付で状態が切り替わる処理は、DB設計としてはフラグやコード値ではなく期間を定義するようにする。

もちろん、全てのケースに上記の提案が導入するのが適切とは限りませんが、設計の選択肢の一つとして考慮いただければ筆者として幸いです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした