【Androidアプリ内課金】定期購読の猶予期間を1から100まで理解する

## はじめに
先日、[【iOS/Android】アプリ内課金 定期購読のサーバーサイド知識総まとめ](https://qiita.com/ckm/items/b8cf23ba4bd0ae5bbf34)という記事を投稿しました。投稿時点から運用経験が増え、その産物の1つとしてAndroidの定期購読における猶予期間について知見が溜まったので筆を執った次第です。

公式ドキュメントから読み取れる情報は如何せん限られているので、詳細な挙動を例とともに確認してきます。

あらかじめ以下の点はご留意・ご容赦ください。

 - **例として紹介するのは、1ヶ月単位の定期購読におけるものです。**異なる期間の購読では、細かな数値が異なる可能性があります。
 - 紹介するケースによっては観測できた事象が少なく、**情報に誤りを含む恐れがあります**。

## 猶予期間とは
まずは公式ドキュメントを基に、猶予期間の概要を説明します。

[定期購入の作成](https://support.google.com/googleplay/android-developer/answer/140504) より（ > 定期購入の変更を管理する > お支払いが承認されなかった場合の猶予期間とアカウントの保留）
> 猶予期間を設定すると、定期的なお支払いが不承認となった場合に定期購入者がお支払い方法を更新できる機会が設けられるようになります。このように設定しておくと、定期購入者のクレジットカードが有効期限切れになった場合や、プリペイドカードを使用して定期購入している場合、お支払い情報を更新していないためにカードが無効になった場合に役立ちます。

これだけだとよく分からないですが、猶予期間を設定しておくと、定期購読の更新日時に支払い情報に不備があった場合にもその購読は解除されません。デベロッパの設定した期間までは、支払に猶予が与えられてユーザは購読の権利を享受することができます（享受できるよう実装しなければなりません）。
猶予期間の選択肢は「なし / 3日間 / 7日間」の3つで、変更はPlayConsoleで行えます。この猶予期間は2015/4/28以降はデフォルトで設定されるようになっており、**既定は「なし」ではない**ことに注意が必要です。

| 定期購読の期間 | 既定の猶予期間 |
| --- | --- |
| 週単位 | 3日間 |
| 月単位以上 | 7日間 |

## ケース別詳細
日時の推移とユーザの行動（支払情報の状況を含む）のパターンの組ごとに、[ストアのAPI](https://developers.google.com/android-publisher/api-ref/purchases/subscriptions)が返す情報を確認していきます。
ストアのAPIの使い方や項目の説明については[以前の記事中](https://qiita.com/ckm/items/b8cf23ba4bd0ae5bbf34#android)で紹介してありますので、適宜参照してください。

各項目について、以下のパターンの違いでAPIの返す情報も変化します。
ただし最初の時点で、ユーザは自動更新をOFFにしておらず、支払情報には不備があることを前提とします。

| 影響する要素 | パターン |
|---|---|
| 日時の推移 | A：本来の期限切れ2時間前まで<br>B：A以降の猶予期間内 |
| ユーザの行動 | a：何もしていない <br> b：支払情報を正常化した <br> c：自動更新をOFFにした |

### ケース1：A-a
日時の推移：本来の期限切れ2時間前まで / ユーザの行動：何もしていない

日時の推移パターンAは猶予期間が影響する前の期間にあたるため、猶予なしの設定と同じ結果になります。

このケースでは、購入時点の情報と同じものがAPIから返却されます。ストアはまだユーザの支払情報に不備があることに気づいていません。
例えば以下のような内容です。（不要な項目は省略しています）

```json-doc
{
  "startTimeMillis": "1547469944277", // 2019/01/14 21:45:44.277
  "expiryTimeMillis": "1550069126138", // 2019/02/13 23:45:26.138
  "autoRenewing": true,
  "paymentState": 1,
  "orderId": "GPA.1111-1111-1111-11111"
}
```

### ケース2：A-b
日時の推移：本来の期限切れ2時間前まで / ユーザの行動：支払情報を正常化した

これは、正常に自動更新が成功するケースと同じです。
また、この時点ではまだ自動更新も行われていないので、APIから返される情報はケース1と同じです。

### ケース3：A-c
日時の推移：本来の期限切れ2時間前まで / ユーザの行動：自動更新をOFFにした

これは、支払情報や猶予期間に関係なく「ユーザが更新をキャンセルした」ケースに該当します。
更新がOFFになった情報はストアに即時反映されるため、APIから返される情報は以下のようになります。

```json-doc
{
  "startTimeMillis": "1547469944277", // 2019/01/14 21:45:44.277
  "expiryTimeMillis": "1550061926138", // 2019/02/13 21:45:26.138
  "autoRenewing": false,
  "cancelReason": 0,
  "orderId": "GPA.1111-1111-1111-11111"
}
```
1点注意したいのは、終了日時が2時間短縮されていることです。この2時間の意味についても[前回の記事](https://qiita.com/ckm/items/b8cf23ba4bd0ae5bbf34#android-1)で紹介してありますが、ユーザが更新をOFFに設定した場合この2時間が削除されます（不要な時間になるため）。

### ケース4：B-a
日時の推移：A以降の猶予期間内 / ユーザの行動：何もしていない

このケースが肝でしょうか。
猶予期間に入ってもユーザが支払情報を正常化しない場合、APIの戻り値で支払が保留されていることが示されます。
リクエストを送った日時が`2019/02/15 13:30:25.000`とすると、以下のような情報です。

```json-doc
{
  "startTimeMillis": "1547469944277", // 2019/01/14 21:45:44.277
  "expiryTimeMillis": "1550291425000", // 2019/02/16 13:30:25.000
  "autoRenewing": true,
  "paymentState": 0,
  "orderId": "GPA.1111-1111-1111-11111..0"
}
```
日時を明示したのは、有効期限`expiryTimeMillis`の値が**リクエストした日時 + 1日**になるためです[^grace-period1]。
もちろん、+1日した結果が猶予期間の上限を超える場合は、猶予期間の上限までの日時が返されます。

`orderId`を見ると、直前の購入のものとは値が異なります。猶予期間とは「**暫定で次回の購入が行われたことにする**」ものであり、「次の購読が始められる状況になるまで、購入済みの購入を延長する」ものではありません。この値で処理の制御やデータの分析を行う場合は、留意しておくとよいと思います。

`autoRenewing`は`true`のままですが、`paymentState`で支払情報に不備があることが示されているのが分かります。
また`orderId`には最新の購入の値が示されますが、`startTimeMillis`は最初に購入した日時のまま不変です。

[^grace-period1]: [GoogleDevelopers](https://developer.android.com/google/play/billing/billing_reference#purchase-data-table)には *The next billing date is extended dynamically every day until the end of the grace period or until the user fixes their payment method.* と書かれています。ここにおける"動的な延期"とは、一定時間ごとにいくらかずつ延期されるのではなく、こちらのリクエストごとに延期されるということのようです。

### ケース5：B-b
日時の推移：A以降の猶予期間内 / ユーザの行動：支払情報を正常化した

猶予期間内にユーザが支払情報を正常化した場合、暫定で与えられていた更新分の購読が正常に更新されてAPIの戻り値が以下のようになります。

```json-doc
{
  "startTimeMillis": "1547469944277", // 2019/01/14 21:45:44.277
  "expiryTimeMillis": "1552488326138", // 2019/03/13 23:45:26.138
  "autoRenewing": true,
  "paymentState": 1,
  "orderId": "GPA.1111-1111-1111-11111..0"
}
```
当然のことながら有効期限は「**前回購入時の有効期限 +1ヶ月**」であり、「猶予期間で伸びていた期限 +1ヶ月」にはなりません。

### ケース6：B-c
日時の推移：A以降の猶予期間内 / ユーザの行動：自動更新をOFFにした

猶予期間にユーザが自動更新をOFFにした場合、猶予期間の付与が無かったことのように扱われ、APIから以下のような戻り値が得られます。

```json-doc
{
  "startTimeMillis": "1547469944277", // 2019/01/14 21:45:44.277
  "expiryTimeMillis": "1550061926138", // 2019/02/13 21:45:26.138
  "autoRenewing": false,
  "cancelReason": 0,
  "orderId": "GPA.1111-1111-1111-11111..0"
}
```
`orderId`を見ると追加の購入があったことが分かりますが、`expiryTimeMillis`からは猶予期間による延長分と最初に2時間多かった分が差し引かれています。

### 補足　ケース7：猶予期間後
ユーザが何もせずに猶予期間の終了を迎えた場合、APIは以下のような情報を返します。

```json-doc
{
  "startTimeMillis": "1547469944277", // 2019/01/14 21:45:44.277
  "expiryTimeMillis": "1550061926138", // 2019/02/13 21:45:26.138
  "autoRenewing": false,
  "cancelReason": 1,
  "orderId": "GPA.1111-1111-1111-11111..0"
}
```
ケース6とほぼ同じですが、`cancelReason`が「支払情報の問題によって購読がキャンセルされたこと」を示してくれています。

## さいごに
簡単ながらAndroidの定期購読における、猶予期間の絡むケースについてストアのAPIの状態遷移を詳細に確認しました。
お気づきの点やご質問があればご連絡ください。