【GooglePlayアプリ内課金】購入と解約のシーケンス

# 概要

GooglePlay アプリ内課金(以下、IAB)の定期購入の一般的な購入と解約のシーケンスをまとめました。
実現方針について、所感も書いてみました。

# 購入

IAB の定期購入とサービス提供判断を実現する方法はいくつかあると思います

1. アプリで Billing Client の [queryPurchases API](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchases) を利用する
    * これは、クライアントAPIを利用してサービス提供中かを確認し分岐する方法です
    * Web アプリがないサービスであれば非常にシンプルに実装することが可能です
    * ただし、 queryPurchases API は、一部の購読情報を取得することができないため、購入に関する細かな制御は OS レイヤに任せることになります
1. 購読情報をもとにバックエンドでサービス提供状態を制御する
    * 購入時を含む、購読状態の変更を通知する RTDN(RealTimeDeveloperNotification)を利用して、バックエンドでサービス提供状況を管理する方法です
    * 購入に使われた google アカウントと自身のサービスのアカウントをなんらか関連付ける必要があります
        * サービス次第ですが、google アカウントと関連付けられたアカウントが 1:1 となるようなチェックや仕組みがあるべきでしょう

### 購入シーケンス

```plantuml
@startuml

actor "ユーザー" as user
participant "アプリ" as app
participant  "バックエンド" as server
participant "Google Play" as play
participant "Google Play Developer API" as api
participant  "RTDN" as rtdn

user -> app : 購入
activate app
app -> play : 決済要求
note right app: 購入時にバックエンドに任意の情報を付与することができる\nhttps://developer.android.com/google/play/billing/developer-payload?hl=ja#attribute
user <- play
user -> play: 購入処理

app <-- play : 購入完了(購読情報を得る)
app -> app: サービス提供状況情報のリフレッシュ
app -> user: 購入完了表示
deactivate app

group バックエンド処理(サービス提供開始)
    rtdn -> server: 購入完了通知
    activate server
    note right server: RTDN は、pubsub のため非同期処理ですが、ユーザ遷移より早く処理されることになると思う
    server -> api : purchases.subscriptions.get(購読情報を取得)
    server <-- api
    server -> server : サービス提供可能か確認
    alt 問題があった場合
        server --> api : purchases.subscriptions.revoke(定期購入の取消)
        server <-- api
        note right server: ここで処理終了
    end
    server -> server: サービス提供のための処理
    server -> api : purchases.products.acknowledge(購入を承認する)
    server <-- api
    server --> rtdn
    deactivate server
end

@enduml
```

* バックエンド処理は、購入処理が完了した後に行われます
* バックエンドAPIである [purchases.subscriptions.get](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptions/get) は、googleの購読情報のみしか取得できないため、RTDN 経由で処理を実装した場合、自身のサービスと関連付ける情報を直接的に取得することができません
    * developerPayload フィールドは、サービス側で任意の情報を付与することができ、そのフィールドを利用して情報の受け渡しをする
        * ただし、v3 でこのフィールドは削除されたようです https://developer.android.com/google/play/billing/developer-payload
    * アカウント情報だけであれば、 obfuscatedExternalAccountId フィールドに詰めることも可能
    * どちらのフィールドもアプリから決済要求するときに設定する情報のため、アプリ外(Playストアの定期購入画面)で再購入した場合などは設定されずにバックエンドで処理することになる
    * バックエンドで処理できない場合は、revoke API を呼び取り消しを行うか、アプリにリストアAPIを用意するかだと思う
* 定期購入は、クライアントかバックエンドのどちらかで acknowledge をする必要があり、3日間行われないと自動で取り消しされます
    * ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224207/e89264e5-c312-20b0-ec32-43e8ddf0e3b7.png)
    * https://developer.android.com/google/play/billing/integrate#process

### ポイント

* RTDN は、発火したイベントの種別がペイロードに含まれていますが、メッセージングサービス(google pub/sub)を利用しており、到達の順序が保証されていないため、バックエンドから API を経由し、購読情報を取得し、その状態でサービス提供開始か終了かを判断して実装するのが望ましいと思います
* obfuscatedExternalAccountId に設定する情報は、 PII(Personally Identifiable Information) にあたる情報を設定することを禁止されています
    * そのため、メールアドレスなどを直接入れることはできないため、アカウントID がメールアドレスの場合はハッシュ化するなどが必要です
    * ただし、64文字が上限
    * https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setObfuscatedProfileId(java.lang.String)
* 定期購入の Acknowledge は、ユーザビリティやクレームはともかく、負荷の高い返金運用を軽減してくれるため、サービス提供と近いトランザクションとするのがよいです
    * バックエンドでサービス提供処理を行っている場合、 Acknowledge もバックエンドに寄せる方がよいと思います
* 決済システム全般に言えることだと思いますが、トレーサビリティのために以下のログは取っておくといいと思います
    * RTDNのリクエスト(障害時の再送でパンクしないようにすること。RTDN の記事で詳しく書きます）
    * サービス提供したか、しなかったか
    * 取消したか

# 解約

IAB の定期購入の解約は、ユーザが解約したタイミングではサービス提供状態で、次回の更新日でサービス提供終了とすることが義務付けられています。
また、解約は、クライアント・バックエンド、どちらからもAPIで提供されており実施可能ですが、Play ストアからの解約は必ず対応しなければならず、まずは、Play ストアからの対応を実装したのち、アプリ内で解約できるようにするなどの機能拡張していく流れになると思います。

### 解約シーケンス

```plantuml
@startuml

actor "ユーザー" as user
participant "アプリ" as app
participant  "バックエンド" as server
participant "Google Play" as play
participant "Google Play Developer API" as api
participant  "RTDN" as rtdn

alt アプリ内解約
    user -> app: 解約ボタンとか？
    note right app : deep link とかで飛ばす
    app -> play
else Play ストア直接
    user -> play: 解約
end

play -> play : 解約処理
user <-- play

|||

... ～次回更新日～ ...

|||

group バックエンド処理(サービス提供終了)
    rtdn -> server: 期限切れ通知
    activate server
    server -> api : purchases.subscriptions.get(購読情報を取得)
    server <-- api
    server -> server: サービス提供終了のための処理
    server --> rtdn
    deactivate server
end

@enduml
```

* 解約後、期限切れの前はサービス提供中のため、バックエンド処理は、特に必要な処理はないと思います
    * 何らかの状態を保存したい場合、再開されることを考慮してください
    * 蛇足ですが、再開時にアプリ内から再購入させた場合、ユーザの請求は購入時には行われず次回更新日に行われるよう google play 内でうまく処理してくれるようです
* 期限切れ通知を受けてサービス提供終了のための処理を行います

### ポイント

* 別の定期購入のサービス提供を停止してしまわないように契約のIDで一致を確認するのがよいです
    * IAB の場合、purchase token で ID できるため、それを利用するのがよいです
* 期限切れはどうあっても非同期になるため、アプリとして定期でリフレッシュする仕組みは必要だと思います


# ユーザの継続失敗時

異常系で必ず対応しなければならない継続時のユーザの失敗に伴うサービス提供の停止。
更新失敗によりサービス停止させられた状態を保留中(アカウント保留中)といいます。


### (前提) アカウント保留中とは

* 定期購入の更新日に行われる請求に対して、ユーザの支払いが失敗するとアカウント保留中という状態になります
    * アカウント保留中になったらサービス提供を停止しなければならないです
    * アカウント保留中は、ユーザが、google play から支払方法を修正することで復帰します
    * 公式ドキュメントにはユーザに支払方法を修正させるためになんらか通知したほうがいいとのことですが、メールや通知は google から送信されているようでした
    * 30日以内に修正されないと自動更新は停止され期限切れ状態へと移行します
    * iOSアプリ内課金にある Billing Retry に相当する機構があるかはわからなかったです
    * https://developer.android.com/google/play/billing/subscriptions#account-hold
* 支払いに失敗しても即時にサービス提供を停止せずに注意を促す猶予期間を設定することができます
   * 猶予期間は、なし・3,7,14,30日を設定できる
   * 猶予期間になると、 `expiryTimeMillis` (有効期限)は、内部的に自動的に延長されます
   * https://developer.android.com/google/play/billing/subscriptions#grace
   * https://support.google.com/googleplay/android-developer/answer/140504?hl=ja#zippy=%2C%E7%8C%B6%E4%BA%88%E6%9C%9F%E9%96%93


### 保留中シーケンス

```plantuml
@startuml

actor "ユーザー" as user
participant "アプリ" as app
participant  "バックエンド" as server
participant "Google Play" as play
participant "Google Play Developer API" as api
participant  "RTDN" as rtdn

group ユーザの支払失敗
    play -> user: 自動更新による請求
    user --> play: 支払い失敗
    play -> user: 支払失敗と猶予期間になったことの通知
... 猶予期間 ...
end

note right app : 猶予期間の終了

group 保留時の処理
    rtdn -> server: アカウント保留通知
    activate server
    server -> api : purchases.subscriptions.get(購読情報を取得)
    server <-- api
    server -> server: サービス提供終了のための処理
    server --> rtdn
    deactivate server
end

group ユーザが支払方法を修正した
    user -> play: 支払方法修正
    play -> play: 再開処理

    rtdn -> server: 復帰通知
    activate server
    server -> api : purchases.subscriptions.get(購読情報を取得)
    server <-- api
    server -> server: サービス提供のための処理
    server --> rtdn
    deactivate server
end
@enduml
```

### ポイント

* アカウント保留中かの判断は、 `expiryTimeMillis` (有効期限)が、切れていて、かつ、 `autoRenewing` (自動更新)が、有効かで判断ができます
    * `paymentState` (支払いステータス)が、 `0` (Payment pending) かでも判断できます
        * 蛇足ですが、期限切れ時はこのフィールドは未設定になるのですが、私は Golang を利用しており、公式のライブラリでは、ゼロで初期化されてしまうため、見分けがつかない状態になっていました
* 支払方法の変更時のイベントは、それほどラグがなく通知されました
    * google pub/sub や RTDN 自体に到達時間保証に関するドキュメントはなかったため、検証してみた結果でしかないです
* アプリ内で支払方法を変更する機能を実装することも可能かとは思いますが、基本的にはアプリ外で行われることだと思います。そのため、RTDN を利用した非同期処理がメイン実装となると思います
    * アプリは、サーバサイドで行われた復帰処理で通知を送るか、リフレッシュ機能を用意するかが必要になるかと思います