StripeのAlipay決済をサブスクリプションサービスに導入してみた話

この記事は [さくらインターネット Advent Calendar 2019](https://qiita.com/advent-calendar/2019/sakura) の17日目です。

Dockerホスティングサービス [Arukas](https://arukas.io/) の課金システムを担当している鷲野です。
残念ながらArukasは、2020年1月31日にサービス提供を終了する事になりましたが
せっかくなのでArukas課金システムの開発/運用を通して得た知見を共有していきたいと思います。
今回は「ArukasにStripeのAlipay決済を導入してみた」経験で得た知見を纏めさせていただきます。


## はじめに
Arukasは月間の使用量・時間に応じた料金を翌月請求する`従量制後払い`の請求フローとなっています。
いわゆるサブスクリプション方式のサービスです。

Arukasでは決済処理に[Stripe](https://stripe.com/)を利用していますが、カード登録や決済部分だけの最小限の利用に留めており
[Stripe Billing](https://stripe.com/jp/billing)は利用していません。
料金計算や、決済の実行（請求フローの制御）などは自前で開発したシステムで行っています。
（理由としてはなるべく自前でコントロールする事でサービス設計/システム設計の自由度を取ったという感じです）

Arukasはサービスを利用するにあたってクレジットカード登録を必須としており、料金の請求はカード決済にのみ対応していました。
Arukasは海外からの利用にも対応していますが、中国ではクレジットカードを持っていない人が多いらしく、Arukasを使いたいニーズはあるのに登録できないという問題がありました。
そこで中国の人も利用できるように、別の決済方法を導入できないか検討したところ
Stripeはカード決済以外にも様々な決済方法に対応しており、その中に中国でメジャーな[Alipay決済](https://stripe.com/docs/sources/alipay)がありました。
このAlipay決済で「カード決済を前提とした現状の請求フロー」を変えずに済むようなやり方
（後述する`reusable`）が可能な事がわかったので、Alipay導入する事にしました。

（そもそもプリペイド的な先払い方式をサービスに導入できれば決済方法の自由度はグッと上がるのですが、そうなると資金決済法が絡んできて話が大きくなりすぎるため断念しました）

ちなみにArukas課金システムはPHP7.2で作成しStripeのPHPライブラリを使用しています。
本記事のコードはPHPで紹介していますが、Stripeは`curl` `Ruby` `Python` `PHP` `Java` `Node` `Go` `.NET`などに対応しており、文中のリンク先のStripeドキュメントでは各言語のコードが記載されているので適宜好きなコードに読み替えてください。

## StripeのAlipay決済 `single-use`と`reusable`
StripeのAlipay決済には`single-use`と`reusable`があります。
（Alipay自体の説明はここでは省きます）
https://stripe.com/docs/sources/alipay
https://stripe.com/docs/sources#single-use-or-reusable

ざっくり説明すると`single-use`は1回限りの支払い方式で
「ユーザーに支払いリクエスト」→「ユーザーに承諾を得る」→「承諾された事を確認し決済」
といった一連の手続きを決済する時に毎回行います。

それに対して`reusable`では初回だけ
「ユーザーに支払いリクエスト」→「ユーザーに承諾を得る」
を行って登録し、その後は登録済みのAlipayを使って何度も決済を行うことができます。
クレジットカードのように、登録したカードで毎回決済を行うのと同じ動きが可能になります。

そのため、Arukasでは既存の請求フローを変えずに済む`reusable`でAlipay決済を導入することにしました。

ちなみにAlipay決済を有効にするにはStripeのダッシュボード上で有効に設定する必要があり、
さらに`reusable`を使えるようにするにはStripeのサポートに申請する必要があります。
また、StripeのAlipay決済は中国本土のAlipayアカウントのみ利用可能となっているため、中国以外の人のAlipay決済には使えません。

ではAlipay決済についてStripeドキュメントと照らし合わせて詳細に説明していきます。

## Alipay決済の手順
### STEP1 AlipayのSourceオブジェクトを作成し、承諾ページへユーザーをリダイレクト
https://stripe.com/docs/sources/alipay#create-source

```php
// 1. Create Source
$source = \Stripe\Source::create([
            "type" => "alipay"
            , "currency" => "jpy"
            , "amount" => 50   // ← reusable時はコメントアウトする
            , "usage" => "reusable" // ← single-use時はコメントアウトする
            , "redirect" => [
                "return_url" => "Arukasエンドポイント" // ユーザー承諾後に戻ってくるURLを指定
            ]
        ]);
// $source["redirect"]["url"] がユーザーの承諾ページURLとなっているのでリダイレクトする
// この時点では $source["status"] = "pending" となっている
```
 
### STEP2　承諾ページでユーザーに承諾してもらう
https://stripe.com/docs/sources/alipay#customer-action
リダイレクト先のページで、ユーザーはAlipayにログインし承諾 or 拒否します。
承諾された場合Sourceのstatus=`chargeable`に、拒否された場合`failed`になります。
あるいは承諾も拒否もせず放置された場合は`pending`のままですが、1時間経過すると`canceled`になります。

それと`single-use`では`chargeable`になった後、決済実行せず6時間経過した場合または23:45中国標準時（GMT+8）より前に決済しなかった場合は`canceled`になります。
https://stripe.com/docs/sources/alipay#single-use-sources-expiration

Stripeのテスト環境で実施した場合はテスト画面が用意されており、
$source["redirect"]["url"] にそのURLが入っているため、ブラウザで開いて承諾 or 拒否することができます。
![スクリーンショット 2019-12-13 23.07.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313462/7f1c27bc-0f7c-8dff-f63f-5e2319fc3d0f.png)

本番環境で実施した場合はこんなような案内画面がでます。
![alipay.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313462/ce4d9058-1c2d-a6bf-1b26-7dfb493b5f92.png)
説明の機械翻訳
> 1.ワンクリック支払いは、Alipayが開始した便利な支払い方法であり、パスワードなしで支払いを完了できます。
2.ワンクリック支払い条件を開きます。Alipayアカウントを持ち、本名認証を実行します。 Alipayアカウントをお持ちでない場合は、最初に登録してください。
3. Alipayのワンクリック支払いが有効になった後、Alipayは取引が発生すると自動的に引き落とし、SMSで通知します。

ちなみにこの先に進むと中国本土のAlipayアカウントではない場合はこんな感じのエラーになります。
![alipay-error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/313462/25e5ccd1-90f9-82b8-8646-25ba27053b33.png)


### STEP3　Sourceオブジェクトのstatus=`chargeable`を確認し、決済
https://stripe.com/docs/sources/alipay#charge-request
STEP1でSource作成時に指定したArukasエンドポイントで行います。
（あるいはより厳密にやるなら[Webhook](https://stripe.com/docs/sources/alipay#using-webhooks)を使う）

#### STEP3.1 Sourceオブジェクトの確認
GETパラメータで`source`と`client_secret`が取得できるので`source`の値からStripeAPIでSourceオブジェクトを取得します。

ここで念のためセキュリティを考慮して`client_secret`の値を確認します。
Sourceオブジェクトの`client_secret`とGETパラメータの`client_secret`のそれぞれの値を比較して一致するか確認する。
一致していない場合は不審なリクエストなので処理中断しましょう。

Sourceオブジェクトのstatus=`chargeable`**以外**であれば決済できないのでこれも処理中断。
`chargeable`であれば処理続行する。

```php
// StripeAPIでSourceオブジェクト取得
$source = \Stripe\Source::retrieve('GETパラメータ`source`の値');
// client_secretの比較確認
if (strcmp($source["client_secret"], 'GETパラメータ`client_secret`の値') != 0) {
    return;
}
// status = chargeable 確認
if (strcmp($source["status"], "chargeable") != 0) {
    return;
}
// 処理続行する
```

#### STEP3.2 `reusable`の時はCustomerにattachする
`single-use`ではCustomerにattachしないためSTEP3.2は飛ばします。
https://stripe.com/docs/sources/customers#single-use-sources

`reusable`では、ここでSourceを[Customer](https://stripe.com/docs/api/customers)にattachします。
https://stripe.com/docs/sources/alipay#attaching-a-reusable-source-to-a-customer
https://stripe.com/docs/sources/customers
ちなみにCustomerとはStripe内でユーザーを管理するオブジェクトで、CardやAlipayなどのSourceを紐付ける事ができます。

##### [Customerを新規作成しつつSourceをattachする場合](https://stripe.com/docs/sources/customers#attaching-a-source-to-a-new-customer-object)

```php
$customer = \Stripe\Customer::create([
  "email" => "paying.user@example.com",
  "source" => $source["id"],    // STEP3.1で取得/確認した$source
]);
```

新規作成したCustomerなので、ここでattachしたSourceが[default_source](https://stripe.com/docs/api/customers/object#customer_object-default_source)となります。
default_sourceが設定されたCustomerでは、後述するchargeリクエストにsourceパラメータを指定せずcustomerパラメータを指定する事で、自動的にdefault_sourceで決済されます。

##### [既存CustomerにSourceをattachする場合](https://stripe.com/docs/sources/customers#attaching-a-source-to-an-existing-customer-object)

```php
$createdSource = \Stripe\Customer::createSource(
  'cus_GMABrqbZ1mdxe7',        // 既存Customerのid
  [
    'source' => $source["id"], // STEP3.1で取得/確認した$source
  ]
);
```

上のやり方の場合、既に別のSourceがdefault_sourceとして存在する場合はそのままで、attachしたSourceは追加される動きになります。

default_sourceを置き換えてattachするには以下のようにします。
この時、既存のdefault_sourceは自動的にdetachされます。

```php
\Stripe\Customer::update(
  'cus_GMABrqbZ1mdxe7',        // 既存Customerのid
  [
    'default_source' => $source["id"], // STEP3.1で取得/確認した$source
  ]
);
```


#### STEP3.3 決済実行する
##### `single-use`ではsourceパラメータを指定してchargeリクエストを行う
https://stripe.com/docs/sources/alipay#make-a-charge-request-using-the-source

```php
// single-useでの決済はsourceパラメータを指定する
try {
    $charge = \Stripe\Charge::create([
                "amount" => 50,      // STEP1のamountと一致してないとエラーになるよ
                "currency" => "jpy",
                "description" => "Alipay Payment Test",
                "source" => $source["id"],    // STEP3.1で取得/確認した$source
    ]);
    var_dump($charge);  // $charge["paid"]がtrueなら決済成功。決済失敗時はfalseだがExceptionが飛ぶのでfalseになる事はまず無い
} catch (\Stripe\Error\Card | \Stripe\Error\InvalidRequest $e) {
    // Alipay決済失敗時はこの２つのExceptionをキャッチする
    var_dump($e);
}
```
決済後はSourceのstatus=`consumed`になります。
もしここで`reusable`のSourceをSTEP3.2のCustomerへのattachを行わず上の方法で決済した場合は、
`single-use`と同様にstatus=`consumed`となり、再び決済するにはSTEP1から行う必要があります。

##### `reusable`ではcustomerパラメータを指定してchargeリクエストを行う
`reusable`ではchargeリクエストを何度も行えます。
STEP1でSource作成する際にamountパラメータを指定していませんでしたが、毎回のchargeリクエスト時にamountを指定することになります。

```php
// reusableでの決済はcustomerパラメータを指定する
try {
    $charge = \Stripe\Charge::create([
                "amount" => 50,      // STEP1ではamount指定してない
                "currency" => "jpy",
                "description" => "Alipay Payment Test",
                "customer" => 'cus_GMABrqbZ1mdxe7',     // STEP3.2のcustomer
    ]);
    var_dump($charge);  // $charge["paid"]がtrueなら決済成功。決済失敗時はfalseだがExceptionが飛ぶのでfalseになる事はまず無い
} catch (\Stripe\Error\Card | \Stripe\Error\InvalidRequest $e) {
    // Alipay決済失敗時はこの２つのExceptionをキャッチする
    var_dump($e);
}
```

上記のchargeリクエストを何度か続けて実行しても決済できます。
決済後のSourceをStripeダッシュボードで確認するとstatus=`chargeable`, usage=`reusable`であることから
`reusable`の動きとなっている事が確認できます。

##### Alipay決済時のエラーハンドリング
https://stripe.com/docs/api/php#error_handling

`single-use`でも`reusable`でも共通した話となりますが
Alipay決済では決済失敗するほとんどの要因で`\Stripe\Error\InvalidRequest`となります。
具体的には「ユーザーが承諾してない」「amount値がおかしい」などです。

`\Stripe\Error\Card`については「Alipay決済なのにCardなのか」という感じですが
レアケースで発生し、自分の知りうる中では`reusable`で登録した承諾済みユーザーが決済時にAlipay残高が足りない時に発生しました。

月次バッチ処理などで全ユーザーを決済する事を想定した場合、
発生しうるExceptionの中でAlipay決済時に個別にcatchすべきは
`\Stripe\Error\InvalidRequest`と`\Stripe\Error\Card`の２つです。
（ちなみにカード決済時に個別にcatchすべきは`\Stripe\Error\Card`だけ）
それ以外は個別にcatchするというよりはバッチ処理自体を中断すべきExceptionとなります。

## `reusable`特有のリスク
Alipay決済の流れは以上のようになりますが、`reusable`においては`single-use`には無い特有のリスクがあります。
それは`reusable`で承諾したユーザーが、後でAlipay内の操作によって承諾を取り消し可能な事です。
ユーザーがAlipay内で承諾を取り消した場合、Stripeでは決済してみる以外にそれを検知する方法がありません。
Sourceのstatusは一度`chargeable`になった後は変化せず、決済を試みた際には
"The customer has revoked authorization for this source"（顧客は認証を失効しました）というエラーになります。
（Arukasで実体験済み＆Stripe社にも確認済み）

この時、Alipay側からすればユーザー承諾のないサービスから勝手な決済が来て拒否したに過ぎないので
中国における信用スコアへの影響は無いと思われます。
（信用スコアの詳細は不明なのであくまで想像に過ぎませんが）

そのため、悪意のあるユーザーが１ヶ月間存分にサービスを利用し、高額な利用料を取り立てる事ができないと言った事態が起こり得ます。
このリスクに対応するには、サービス側で利用料を一定金額ごとに小まめに取り立てるなどの工夫が必要になります。


## その他、カード決済とAlipay決済の比較
- カードには有効期限が存在するが、`reusable`のAlipay Sourceには有効期限はなく、何事も無ければずっと使える（Stripe社に確認済み）
- 決済後の[Chargeオブジェクト](https://stripe.com/docs/api/charges/object)のidはカード決済では`ch_hoge`, Alipay決済では`py_fuga`となる
- 決済失敗時、カード決済ではChargeオブジェクトが作成されるが、Alipay決済では作成されない
- [Cardオブジェクト](https://stripe.com/docs/api/cards/object)では国情報が取得できるが、AlipayのSourceからは国情報は取得できない
  - StripeでAlipay決済できるのは中国本土のAlipayアカウントだけだから中国に決まってると思うが、後で日本に在住したケースなどがありえる
  - そのためサービス利用料に消費税を含めるかは、Alipay払いのユーザーに関してはArukasに登録時に入力した国情報で判定した
- [最低決済金額はJPYでは50円](https://stripe.com/docs/currencies#minimum-and-maximum-charge-amounts)となっており、カード決済やAlipay決済に関係なく共通
  - 50円未満を指定した場合は決済エラーとなる
- カード決済ではchargeリクエストにcaptureパラメータをfalseで渡す事でオーソリする事ができる
  - https://support.stripe.com/questions/jp-does-stripe-support-authorize-and-capture
  - https://stripe.com/docs/api/charges/create#create_charge-capture
  - ちなみにオーソリ成功したらすぐ[refund](https://stripe.com/docs/api/refunds/create)しましょう
- Alipay決済ではchargeリクエストにcaptureパラメータを渡すことはできない。渡すとエラーになる

## ArukasでのAlipay決済(reusable)の導入
従来では
①カード登録　→　②サービスを使用　→　③使用量に応じた料金を翌月請求

Alipay導入後
①カード or Alipay(reusable)登録　→　②サービスを使用　→　③使用量に応じた料金を翌月請求

という流れになります。
①にて上述のSTEP1〜3.2を行う。
③にてSTEP3.3の`customerパラメータを指定したchargeリクエスト`を行う。
customerパラメータを指定したchargeリクエストでは、customerのdefault_sourceで決済されるため、
カード決済でもAlipay決済でも同じやり方で決済実行できる。

## ArukasにAlipay決済(reusable)を導入してみた結果
- Arukasへの中国人ユーザー登録数がすごく伸びた
- ごく一部のAlipay決済ユーザーは[`reusable`特有のリスク](https://qiita.com/h-washino/private/1979dd47f83148e6d9cd#reusable%E7%89%B9%E6%9C%89%E3%81%AE%E3%83%AA%E3%82%B9%E3%82%AF)に書いたような事態が発生した
- 中国人のメンタルなのか、サービスを信用して承諾状態を保つのに抵抗があるらしく、一度取り消しておく自衛メンタルらしい。利用料の督促をすると再度登録して決済させてくれるユーザーもいた
- そういうわけで`reusable`だからと言ってカード同様に信用して同じ請求フローにするのはあまり良くなく、何らかの対策が必要
  - 一定金額を使用するごとに、月中でも小まめに取り立てる請求フローにするとか
  - その際、請求残額が最低決済金額50円を下回らないようにしないといけない。請求残額を翌月に繰り越す処理を考えないといけなくなる
  - 使用量で増減する`従量制後払い`だから難しいのであって、そもそも`定額制先払い`なら話は簡単なのでAlipay決済だけそうするとかサービス設計で回避するという手も

## 終わりに
以上となりますが、ArukasにAlipay導入した際に得た知見を纏め直して本記事とさせていただきました。
リスクや手間を鑑みても海外対応のサービスならAlipay導入する価値はあるのではないでしょうか。

こういった記事を公開することはこれまでほとんど無かったのですが
Arukasがサービス終了ということになり、せっかく得た知見をこのまま纏め直さずに置いたら勿体ない気がしたので
この機会に記事にさせていただきました。

また何かネタがあれば記事にしたいと思います。