Help us understand the problem. What is going on with this article?

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

この記事は さくらインターネット Advent Calendar 2019 の17日目です。

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

はじめに

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

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

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

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

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

StripeのAlipay決済 single-usereusable

StripeのAlipay決済にはsingle-usereusableがあります。
(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

// 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

本番環境で実施した場合はこんなような案内画面がでます。
alipay.png
説明の機械翻訳

1.ワンクリック支払いは、Alipayが開始した便利な支払い方法であり、パスワードなしで支払いを完了できます。
2.ワンクリック支払い条件を開きます。Alipayアカウントを持ち、本名認証を実行します。 Alipayアカウントをお持ちでない場合は、最初に登録してください。
3. Alipayのワンクリック支払いが有効になった後、Alipayは取引が発生すると自動的に引き落とし、SMSで通知します。

ちなみにこの先に進むと中国本土のAlipayアカウントではない場合はこんな感じのエラーになります。
alipay-error.png

STEP3 Sourceオブジェクトのstatus=chargeableを確認し、決済

https://stripe.com/docs/sources/alipay#charge-request
STEP1でSource作成時に指定したArukasエンドポイントで行います。
(あるいはより厳密にやるならWebhookを使う)

STEP3.1 Sourceオブジェクトの確認

GETパラメータでsourceclient_secretが取得できるのでsourceの値からStripeAPIでSourceオブジェクトを取得します。

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

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

// 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に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する場合
$customer = \Stripe\Customer::create([
  "email" => "paying.user@example.com",
  "source" => $source["id"],    // STEP3.1で取得/確認した$source
]);

新規作成したCustomerなので、ここでattachしたSourceがdefault_sourceとなります。
default_sourceが設定されたCustomerでは、後述するchargeリクエストにsourceパラメータを指定せずcustomerパラメータを指定する事で、自動的にdefault_sourceで決済されます。

既存CustomerにSourceをattachする場合
$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されます。

\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

// 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決済失敗時はこの2つの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を指定することになります。

// 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決済失敗時はこの2つの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の2つです。
(ちなみにカード決済時に個別に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側からすればユーザー承諾のないサービスから勝手な決済が来て拒否したに過ぎないので
中国における信用スコアへの影響は無いと思われます。
(信用スコアの詳細は不明なのであくまで想像に過ぎませんが)

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

その他、カード決済とAlipay決済の比較

  • カードには有効期限が存在するが、reusableのAlipay Sourceには有効期限はなく、何事も無ければずっと使える(Stripe社に確認済み)
  • 決済後のChargeオブジェクトのidはカード決済ではch_hoge, Alipay決済ではpy_fugaとなる
  • 決済失敗時、カード決済ではChargeオブジェクトが作成されるが、Alipay決済では作成されない
  • Cardオブジェクトでは国情報が取得できるが、AlipayのSourceからは国情報は取得できない
    • StripeでAlipay決済できるのは中国本土のAlipayアカウントだけだから中国に決まってると思うが、後で日本に在住したケースなどがありえる
    • そのためサービス利用料に消費税を含めるかは、Alipay払いのユーザーに関してはArukasに登録時に入力した国情報で判定した
  • 最低決済金額はJPYでは50円となっており、カード決済やAlipay決済に関係なく共通
    • 50円未満を指定した場合は決済エラーとなる
  • カード決済ではchargeリクエストにcaptureパラメータをfalseで渡す事でオーソリする事ができる
  • 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特有のリスクに書いたような事態が発生した
  • 中国人のメンタルなのか、サービスを信用して承諾状態を保つのに抵抗があるらしく、一度取り消しておく自衛メンタルらしい。利用料の督促をすると再度登録して決済させてくれるユーザーもいた
  • そういうわけでreusableだからと言ってカード同様に信用して同じ請求フローにするのはあまり良くなく、何らかの対策が必要
    • 一定金額を使用するごとに、月中でも小まめに取り立てる請求フローにするとか
    • その際、請求残額が最低決済金額50円を下回らないようにしないといけない。請求残額を翌月に繰り越す処理を考えないといけなくなる
    • 使用量で増減する従量制後払いだから難しいのであって、そもそも定額制先払いなら話は簡単なのでAlipay決済だけそうするとかサービス設計で回避するという手も

終わりに

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

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

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

h-washino
開発エンジニア。Arukasというサービスの課金システムを担当してます。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした