この記事はBASE アドベントカレンダー 2022の20日目の記事です。
はじめに
BASE BANKというチームでBASEカードというサービスの開発をしている大垣(@re_yuzuy)です。
本記事ではBASEカードで行ったキャッシュバックキャンペーンについて、システム的な設計という観点から書いていきます。
そもそもBASEカードとは何なのか
BASEカードはBASEで作ったネットショップの売上を残高として全国のVisa加盟店(以下加盟店)で使うことができるプリペイドカードです。
BASEカードによって手数料をかけずに売上金を即時に使うことが可能になりました。
キャッシュバックキャンペーン
BASEカードでは利用者増加を促進するためにキャッシュバックキャンペーンを実施してきました。
第1回目はリリース直後の去年(2021年)の9/28~12/26に行われました。
そして第2回目は今年(2022年)の12/1~12/26で記事投稿時現在も行われています。
カード決済の裏側
キャッシュバックキャンペーンの設計を説明するあたってそもそもカード決済がどのように処理されるのかも簡単に説明しなければなりません。
基本的な流れ
加盟店でBASEカードを使って決済を行う場合、まずVisaにリクエストが行き、その後提携会社を通じてBASEカードに通知が送られてきます。
最も基本的な形は
残高確認→決済成功→決済確定
という順で通知が送られてくるパターンです。 各通知の詳細についてはこの後説明します。
決済がキャンセルされると途中で
残高確認→決済成功→キャンセル
残高確認→決済成功→決済確定→キャンセル
というようにキャンセルの通知が挟まったり、なんらかの理由によって途中で商品の値段が上がると以下のように増額の通知が挟まったりします。
残高確認→決済成功→増額→決済確定
残高確認→決済成功→決済確定→増額→決済確定
残高確認
先程BASEカードは売上を残高として使えるプリペイドカードと説明しましたが、実際の使用感はデビットカードに近いです。
例えば1,000円の売上残高があったとして500円の商品を購入したい場合
というようにサイレントにチャージが行われています。
残高確認とはこの例で言うと提携会社からBASEカードに送られてくる「500円残高ある? 」というところで、残高を確認する他にショップがBANされているか、カードがロックされているかなどの確認をしています。
1秒以内という速度要件があり、この要件を実現するためにした取り組みも面白いのでこれに関しても別でブログを書きたいと思っていますが、キャッシュバックにはあまり関係していないのであまり気にしなくても大丈夫です。
決済成功
残高確認を経て提携会社の向こう側であれこれやった結果、無事決済が成功すると送られてきます。
決済確定
加盟店(ネットショップなど)側で売上が確定すると送られてきます。 毎日特定の時間に送られてきます。
ネットショップだと商品を発送したときなどに送られてくることが多いです。 なので基本的に決済成功してから決済確定までは数日間のラグがあります。
ここで決済の詳細なデータ(加盟店や為替の情報など)がBASEカード側に送られてきます。
例えばBASEのショップで買い物をしていただくと、残高確認や決済成功の段階では加盟店名はBASEですが決済確定ではBASE*HOGE SHOPのようになります。
これはBASEカードに限ったことではないので自分のクレジットカードの利用履歴などを見て確認してみても面白いかもしれません。
キャンセル
決済がキャンセルされると送られてきます。
必ずしも全ての金額がキャンセルされるというわけでもなく、複数の商品のうち1つを返品するなどされた場合にはその商品の金額分だけキャンセルされます。
増額
上記の説明そのまんまなので割愛します。
キャッシュバックキャンペーンの設計
2021年
前述した通りBASEカードリリース直後で、もちろん初めてのキャッシュバックキャンペーンです。
BASEカードのキャッシュバックを設計するにあたって参考にできる記事等があまりなく、かつメインで設計した私が設計初心者だったこともあり苦労した印象があります。
仕様
2021年のキャッシュバックキャンペーンはざっくりと以下のような仕様でした。
- キャンペーン期間中に来た決済が確定されたら特定の割合をキャッシュバックする。
- キャッシュバックされた決済がキャンセルされたらキャッシュバックもキャンセルする。
- 本人確認の有無でキャッシュバックの割合・キャンペーン全体での上限金額を変更する。
- 本人確認済み: 5%・5,000円
- それ以外: 1%・2,000円
- 決済毎の上限金額は500円。
- ご利用履歴詳細でキャッシュバックされた金額を確認することができる。
DB設計
キャッシュバックキャンペーンの要素を保存するテーブルとして以下の3つを定義しました。 payment_transaction_logsに関しては決済に関する通知が蓄積されているとだけ考えていただければ大丈夫です。
cashback_campaigns
キャッシュバックキャンペーンそのものを表すテーブルとしてキャッシュバックキャンペーンに関わる全てのテーブルの親となっています。
このテーブルが持つ重要な情報はIDとキャンペーンの開始・終了時刻くらいで、他の情報は別のテーブルが保持しています。
cashback_yields
キャッシュバックの還元率や上限金額を保存するテーブルです。
targetというカラムを作りそれによって還元対象を判別できるようにしています。 2021年の場合は本人確認済と未確認の識別子を定義し、それぞれ還元率と上限金額が異なるレコードを用意しました。
cashback_logs
キャッシュバック・キャッシュバックキャンセルの情報を保存するテーブルです。
キャッシュバックに紐づく決済の識別子やキャッシュバックが行われたときの還元率の情報を保存しています。
アプリケーション設計
2021年のキャッシュバックキャンペーンではバッチを毎日定期実行し、そこでキャッシュバック処理に関する全てのことを実行するようにしました。
処理としては
前回の実行以降に確定した(キャンセルされた)決済を引っ張ってきて、キャッシュバック(キャンセル)対象かつキャッシュバック上限未到達であればユーザーの本人確認状況によって適切な還元率を適応してcashback_logsにレコードを保存し、ショップの売上残高にキャッシュバック(キャンセル)する。
と、シンプルです。
よかった点・反省点
よかった点は結構キャッシュバックキャンペーン開始までスケジュールギリギリで設計・実装していたものの根本的には長生きできそうな設計にできた点です。
例えばリアルカードでの決済にだけキャッシュバックしたいというキャンペーンが生まれてもcashback_yieldsに適当な識別子を追加して、それに紐づく判別の処理を実装し当てはまればキャッシュバックをする、という感じです。
逆に反省点は前述した通り急いで実装するために(ただの言い訳ですが)一般化があまりなされておらず、実質的には今回のキャッシュバック専用の実装が出来上がってしまった点です。
ですが1年越しに見てみても悪くない設計にはなったのかなと思っています。
2022年
さて今年のキャッシュバックキャンペーンですが、前回のキャンペーンとところどころ仕様や実行方法が異なっています。
さらにキャンペーン期間中にBASEカードで1回以上決済をする(500円以上)と抽選で10,000円がキャッシュバックされるという特定の決済に紐付かないキャッシュバックも新たに登場しました。 しかし、これについては設計的に面白みがあまりないのでこの記事では書かないことにします。
仕様
今年のキャンペーンでは新規ユーザー獲得のため、キャッシュバック対象ユーザーが「キャンペーン中にBASEカードで初めて決済をしたユーザー」に 変わっています。
更に以下のような仕様が追加されました。
- 予算を超えたらキャッシュバックキャンペーンを終了する。
- ご利用履歴画面で決済がキャッシュバックされる予定か確認できる。
このキャッシュバックされる予定(以下キャッシュバック予定)という概念が厄介で苦労しました。
DB設計
仕様を満たすためにテーブルや識別子を追加しました。
cashback_campaign_balances
予算を表すテーブルでキャッシュバック(キャンセル)が行われるたびに更新しています。
cashback_campaigns
2021年のキャッシュバックキャンペーンでは実施されているか否かを判断するためのカラムが開始・終了時刻しかありませんでしたが、今回は予算が消化されたら期間内であっても終了するという仕様が追加されたのでキャンペーンの実施可否が判断できるようにカラムを追加しました。
アプリケーション設計
前述したとおり決済成功と決済確定にはラグが存在しており、キャッシュバック予定を確認できるという仕様は正確に言えば、決済成功段階でその決済がキャッシュバックされる予定の金額などを確認できる、ということです。
これは日次バッチだけでは実現することができず、
決済成功が来たらキャッシュバック予定を保存し、日次バッチでは予定情報を見てその分売上残高にキャッシュバックする。
というように全体的に設計を変更することになりました。
キャンセル・増額の処理
2021年のキャッシュバックではバッチで処理していましたが、キャッシュバック予定の関係等でキャンセル・増額の通知が来たときに処理することにしました。
キャッシュバックが予定段階のときにキャンセル・増額された場合にはキャッシュバック予定・キャッシュバック予定キャンセルを追加で保存し、バッチでは決済IDに紐づく全てのキャッシュバック予定・予定キャンセルを足した値をキャッシュバックするようにしました。
キャッシュバック済みの決済が増額・キャンセルされた場合にはバッチで残高操作をせず、その場で残高操作まで行うようにしました。
日次バッチでキャンセル・増額のキャッシュバック処理をしない理由として、バッチ自体の実装を単純にしたかったことに加え、決済確定が毎日決まった時間に通知されるのと対照的にキャンセル・増額は決まった時間に通知されないという問題がありました。
これによってバッチで処理するようにするとキャンセルからバッチ実行までの間に余分にキャッシュバック可能額が狭められてしまい、本来キャッシュバックされるべき決済がキャッシュバックされない可能性が生まれてしまいます。
よかった点・反省点
よかった点は設計に関してちゃんと文書化できた点です。 これはキャッシュバックに限ったことではなく、BASEカードでは大抵の事柄(決済や本人確認など)について技術ドキュメントが書かれていて、カード決済というドメインそのものの複雑性と決済や本人確認などの複数のベンダーが関わっているという複雑性に立ち向かうためにこれらのドキュメントをとても重宝しています(もちろん前回のキャッシュバック設計時にも書きました)。
今回に関して言えばキャッシュバック予定という概念が新しく登場しましたが、きちんと文書化することで開発メンバー全員が新たな設計や概念に対して共通の認識を持つことができました。
反省点は前回と同じく、今回の仕様専用のような設計・実装になってしまった点です。 キャンセル・増額の処理のユースケースが巨大化してしまうなど、今年のキャッシュバックキャンペーンでは戦術的な設計になってしまった部分が多々ありました。
今後改善していきたいこと
今後もキャッシュバックキャンペーンは行われていくと思われるため、決済の実装からはできるだけ切り離すなど、より戦略的な設計にしていきたいです。
おわりに
BASEカードにはキャッシュバックに限らず面白い課題がたくさんありますし、手をあげれば設計やデータ分析などやってみたいことに取り組める環境です!(2021年のキャッシュバックの設計をした当時私は17歳かつインターン生でしたが、興味本位でやりたいと言ってみたらメンバーのサポートもありやることができました)
一緒に開発していただけるメンバーを鋭意募集中ですので、この記事を読んでみて少しでも興味をもっていただけましたらぜひカジュアル面談だけでもよろしくお願いします!
明日は緒方さんと松田さんの記事です!お楽しみに!