AppStore
GooglePlay
サブスクリプション

サブスクリプションのサーバサイド開発で得た知見

More than 1 year has passed since last update.

初めに

こんにちは。CYBIRDエンジニア Advent Calendar 15日目担当の@sakamoto_kojiです。
普段はサーバサイドの開発・技術ディレクションをしています。最近はアプリのサーバサイドに関わることが多くなりました。

14日目は@kanachaさんのGrowth Push SDKのサポートが停止するらしいので、最新のGrowthbeat SDKに載せ変えようとしたらめちゃくちゃ苦戦した話でした。
PUSH周りのハマリどころが判って勉強になります。

内容について

今回はAndorid、iOSのサブスクリプションのサーバサイド開発で得た知見を記載します。
マーケットとやり取りする詳細は別のqiita記事で判りやすく書かれているのがあるのでそちらを参照頂くとして、
ここでは検索しても見つからない内容や気をつけるべきと思ったことをなるべく書きます。
※現在関わった案件は全てサブスク期間1カ月のものなのでそれを基本に記載します。

Google Playの払い戻し注文に対する取り扱い

課金直後にPlay上から払い戻しをされた場合にどのように検出するのが効率よいかという話しです。
https://support.google.com/googleplay/answer/2479637?hl=ja
の通りだとすると
購入後 48 時間以内の払い戻しはGoogle Play ストアアプリ上から操作できるが
48 時間以上経過後の払い戻しはデベロッパーに問い合わせるしかありません。
だとすると購入から48時間経過した注文に対してサーバからAPIで契約状況をチェックすればシンプルにチェックできそうです。
48時間待たずに数時間おきにAPIでチェックするのも可能ですが、どこまで厳密に行うかはプロダクトオーナーと握っておけば良いと思います。

Androidのテスト購入にorderIdが入らない。

2016年の夏ごろまではテストアカウント購入でもorderIdが付いてきたのですが途中から入らなくなりました。
公式ドキュメントを読むと代わりにpurchaseTokenを使ってくれとのことです。
ドキュメントをちゃんと読んでいれば難なく気づけるところですが、飛ばし読みしていると見逃します。
purchaseTokenの文字列はorderIdに比べるとかなり長いのでテーブルのカラムサイズは大きめにしておくべきです。

アプリからgoogle問い合わせしてサーバに届くレシート情報は通常だとこんな感じです。
{"orderId":"GPA.XXXX-XXXX-XXXX-XXXXX","packageName":"xxx","productId":"xxx","purchaseTime":1481686740982,"purchaseState":0,
"purchaseToken":"poepbpjpimbolcbagbjldhedxxx-J1OzZaW9eJvu4FyfF_K7cu01Hixxx-4r2nbFzOh4c95sV1P7blVyVtHPl_Rc8Nvxxxx-KRwvFZjGpJjJnlBmvUBXb8UxoNxDVRaDGMNHR49Nxxx-ckxnhEA2azdyTr5fQzrLOBDnLC3ZdX6zXZO40zVWGCxxx-1uPk2SBpf1rZ572oHav0xxx-xxxiDLAaPDiyNvXWxxx",
"autoRenewing":true}

それがテストアカウント購入だとorderIdの項目がすっぽり抜けます。
{"packageName":"xxx","productId":"xxx","purchaseTime":1478857942344,"purchaseState":0,
"purchaseToken":"poepbpjpimbolcbagbjldhedxxx-J1OzZaW9eJvu4FyfF_K7cu01Hixxx-4r2nbFzOh4c95sV1P7blVyVtHPl_Rc8Nvxxxx-KRwvFZjGpJjJnlBmvUBXb8UxoNxDVRaDGMNHR49Nxxx-ckxnhEA2azdyTr5fQzrLOBDnLC3ZdX6zXZO40zVWGCxxx-1uPk2SBpf1rZ572oHav0xxx-xxxiDLAaPDiyNvXWxxx",
"autoRenewing":true}

※隠す情報は「xxx」の文字を入れてます

AndroidのorderIdが都度変化する

Androidの場合は購入の都度orderIdが変化するので同じ購読期間内でも複数のorderIdが発生します。

定期購読を解除して、再度購読開始する際に新しくorderIdが発行されるので仕方ないですが
その際、レシートの開始日は再度購読開始した日で終了日は元々の期限になります。
知らないエラー原因になったり、加入者数の集計をだすときに違った数字が出てしまいます。

サーバに届いたレシート情報をgoogleのAPIに検証出して戻ってくる情報はこんな感じです。
{"kind":"androidpublisher#subscriptionPurchase","initiationTimestampMsec":"1478857942344","validUntilTimestampMsec":"1478927092748","autoRenewing":true}

定期購読の解除、再購入を繰り返すと以下のように
initiationTimestampMsecは購入時のタイムスタンプ、
validUntilTimestampMsecは最初に定期購読開始時の期限になります。

"initiationTimestampMsec":"1480594363510","validUntilTimestampMsec":"1482121223909"
"initiationTimestampMsec":"1480739977541","validUntilTimestampMsec":"1482121223909"
"initiationTimestampMsec":"1480927476498","validUntilTimestampMsec":"1482121223909"
"initiationTimestampMsec":"1481209296478","validUntilTimestampMsec":"1482121223909"

iOSの場合は仕組みが違うこともあり、同問題は発生しないです。

サブスクリプションの期限

仕様に関して公式ドキュメントを読むと期限切れの1~3日ぐらい前から再決済に関する事前処理が動くと書いてありますが
実際に期限切れになる日付がどうなるかについての明記がどこにもなかったので
調べてみるとAndroidとiOSで若干ポリシー的な違いを発見できました。
具体的には次のようになります。

iOS

Appleから戻ってくるレシートデータを確認した結果、以下のような状態でした。
・基本は課金した日時を基準に翌月同日同日時が期限になる。
・月末の日数が足りない場合は月末最終日かその翌月初日に合わせられる。
・jsonにはいるデータの順番はかならず昇順ではいるとは限らない。
(順番が違って入ることがあった。)

Android

・基本は課金した日時を基準に翌月同日同時刻+2時間が期限。
・月末の日数が足りない場合は翌月最終日の同時刻+2時間になる。
たとえば、
2016/1/31 3:24 に課金の場合、
2016/2/29 5:24が1ヶ月目の期限、
2016/3/29 5:24が2ヶ月目の期限になります。
なので、30日、31日に購入の場合は何カ月か継続すると自動的に29日に丸められる感じです。
・定期購読を解除してる場合は翌月同時刻の「+2時間」が付かなくなる。
継続処理の遅延を想定して+2時間になっているようだが、継続しないと判っている場合は+2時間は不要ってことですね。
こまかい。。。

定期購読の決済手段

意外に情報探すのが大変でした。

App Storeの定期購入

 クレカは利用可能
 キャリア決済は現在のところauのみ可能。今後 ソフトバンクも可能になる予定(時期未定)
 ギフトカードやプロモーションコードは利用可能

GooglePlayの定期購入

 クレカは利用可能
 キャリア決済(3キャリア)は利用可能
 ギフトカードやプロモーションコードは利用不可

itunesカードを使用して月額サービスに登録した場合、

2カ月目にitunesカードの残高が月額料金に達していない時にどのようになるか。

・iTunesカードの残高あり ⇒自動更新される
・iTunesカードの残高なし&クレカ登録済み ⇒クレカに自動的に請求がいく
・iTunesカードの残高なし&クレカ登録なし ⇒Appleから通知がいき、apple IDに登録しているメアド宛てに警告メールが事前に届く。

iOS レシートのoriginal_purchase_date

一度期限切れになって再度購読し始めた定期購読者のレシートチェックをした際、
original_purchase_date が直近の日付になっているケースがあった。

サーバ実装やDB設計で気をつけるべきこといくつか

個別に詳細を書くほどでもないけど事前に知っておくとよいと思う内容です。

iOSのレシート受信でサーバが高負荷で落ちる。

通常のユーザでは起きにくい内容ですが、sandbox設定した1端末を長く使い続けているようなとき
レシート1つでファイルサイズが数メガ単位になってしまいます。
特に定期購読だと、一回の購入で複数の購入情報が作成されますので、一気にサイズが大きくなります。

アプリの作りによりますが、iOSのレシート情報を非効率な方法でサーバに送ってると
購入数の多いレシート情報を受けたときにサーバ負荷がかかってしまいます。
それに合わせてDBのデータサイズやログファイルのデータサイズがびっくりする大きさに成長して後で面倒です。。

sandbox環境の定期購入で5分毎の更新間隔がたまに大きくずれる。

通常は5分毎に次の定期購読が開始されますが、きっちり5分にならない場合があります。
そのときはたしかレシートに記載される日時もずれてた覚えがあります。

Androidでprchaese tokenをサーバ(DB)に保存していなくて困った。

ちゃんと取得していれば加入状況や解約・返金処理をAPIで操作可能です。
アプリのクローズ対応やユーザ問い合わせ対応でなんらか操作必要な時に必ず必要になります。
google developer api (サーバからAPIで契約状況を操作するAPI
https://developers.google.com/android-publisher/api-ref/purchases/subscriptions?hl=ja

Androidの場合、定期購読の期限内のみアプリ側で購入情報取得ができる。

ドキュメントにも記載がありますが、期限切れの定期購読はアプリ側で検出できないので
サーバサイドの情報が頼りになります。この仕様についてはgoogleのドキュメントに記載があります。

「定期購入登録が更新されずに期限切れになると、返される Bundle には表示されません。」
https://developer.android.com/google/play/billing/billing_integrate.html?hl=ja#Subs

ドキュメントはちゃんと読もう

とりあえず構築開始時は時間が無くてもドキュメントはちゃんと読みましょう。手戻りが確実に減ります。

アプリ内課金

https://developer.android.com/google/play/billing/billing_overview.html?hl=ja

In-App Purchaseプログラミングガイド - Apple Developer

https://developer.apple.com/jp/documentation/StoreKitGuide.pdf

最後に

今回は使用できませんでしたが無料試用期間、支払い猶予期間というのも使えるそうです。
https://support.google.com/googleplay/android-developer/answer/140504?hl=ja
実際に使ってみてどのような動作をするのか体験してみたいですね。

CYBIRDエンジニア Advent Calendar明日は、@yuichi_komatsuさんの「DigdagでembulkとBigQueryの黄金コンビがさらに輝く」です!
Digdagもembulkも恥ずかしながら初めて見るワードなので読んで勉強させていただきます!