Digitra

LINUXサーバの設定やプログラムのことなどを中心にブログを書いています。

paypal月額課金のIPN Listenerの実装

Paypalの月額課金(定期購読)は、管理画面上でボタンのHTMLを作って、それを設置するだけで一応、ユーザから月額課金を実現することは出来るのだが、サービス側でこのユーザがこのコンテンツに月額課金してるよってってのは、IPN Listenerというサーバ側でPaypalトランザクションを受け取る仕組みを実装してやらないといけない。


Paypal上にこのIPNの仕様のドキュメントはあるはあるのだが、ドキュメントがあまり更新されてなかったり、リンク切れのドキュメントがあったりと、やれやれな状態だが、実装した方法を紹介。

定期購読とIPNのおおまかな流れ

  1. ユーザが設置した定期購読ボタンを押下する
  2. Paypalに遷移し、ユーザがPaypal上で決済を行う
  3. Paypal上の決済完了画面が表示される(自サービスのページには戻されない)
  4. 非同期でPaypalから自サーバのIPN ListenerのAPIがコールされ、トランザクションを受け取る(トランザクションの種類については後述)
  5. 自前のIPN Listenerでは、受けたトランザクションPaypalからのものであるかどうかを確認するためにPaypalに確認のコールを行う
  6. 妥当性の確認が取れたらユーザの決済が完了したトランザクションを確認したらユーザに課金のサービスを提供できる状態にする(DBの更新とか)
  7. 定期的(ボタンで設定した決済間隔)にユーザの決済処理が行われて、その結果がトランザクションで飛んで来るので、決済されていればサービス延長する。
  8. 定期購読の契約がキャンセルされたら同じくキャンセルのトランザクションが飛んでくるので当該サービスを停止処理を行う。

 

定期購読ボタン作成時の注意

  • ボタンを作成する際には提供サービスごとに課金額を変えるような場合は、購読IDを指定するか、カスタム値にサービスのIDを入れる。
  • また、Paypalからのトランザクションの戻りがどのユーザの決済かを判断するためにカスタム値にユーザIDなどを入れておく。

サービスIDとユーザIDをボタンのカスタム値としていれる例

<input TYPE="hidden" name="custom" value="<?php echo $serviceid;?>,<?php echo $userid">
※何個もカスタム値を入れるなら、わかりやすくするために、serviceid=1,userid=hoge のような入れ方のほうが良いかも。

IPNトランザクションの種類

subscr_signup(購読の契約が行われた)

例えば、初月無料という定期購読にした場合は、このトランザクションを受けたらサービスを有効にしてやる。

subscr_payment(決済が行われた)

上述の通り、無料期間中にはこのトランザクションは流れてこない。1週間無料なら1週間後に初めて決済処理が行われてこのトランザクションが流れてくる。

subscr_cancel(購読の契約が解除された)

Paypal上で購読の解除が行われるとこのトランザクションが流れてくるので、提供しているサービスの停止処理を行う。
 

subscr_failed(決済が失敗した)

クレジットカードの有効期限が切れていたりなどで、決済日に決済が行われないとこのトランザクションが流れてくる。

こいつがくせ者で、決済に失敗したからといって購読の契約が解除されたわけではない。提供サービスを停止して、再度、定期購読が申し込める画面にアクセスできるようにしてしまうと、ユーザが2重の契約をしてしまう可能性がある。

クレジットカードを登録しなおしたりで決済が可能になると、subscr_paymentが流れてくるので、failed中はサービスを停止ではなくサスペンド状態という扱いにするなどして、契約がキャンセルされたわけじゃないということを意識しておく必要がある。
subscr_failの状態がどれだけ続いたとしても契約は残っている。契約が解除されたら、必ずsubscr_cancelが流れてくるのでそれまではサスペンドする。
 

subscr_eot(購読の有効期限切れ)

正直、このトランザクションが流れてくるタイミングが良くわからない。購読の有効期限の管理は、自サービス側で行うだろうから、無視しても大丈夫?
StackOverflowでは、以下の様なことやり取りしているが、うーん、わからん。

TLS1.2対応が必須になりそう

2016年1月時点では、Paypalの本番環境ではTLS1.0でのコール(phpcurl関数でトランザクションの確認のコール)ができていたが、今年になって?からテスト環境にはTLS1.0でコールするとエラーが返ってきた。
どうもPHP5.3のcurlではTLS1.2のプロトコルに対応していないようなので、サーバのcurlコマンドをアップデートして、exec関数でコールするようにしてみた。
 

Paypalから受け取ったデータの妥当性のチェック通信を行なう

トランザクションの確認は以下のようにcurlコマンドで以下のように接続した。
function confirmPaypalTransaction($posts) {

        $paypal_domain = "www.paypal.com"; //テスト環境はsandbox.paypal.com

        $req = 'cmd=_notify-validate'; 
    
        foreach ($posts as $key => $value) {
            $value = urlencode(stripslashes($value));
            $req  .= "&".$key."=".$value;
        }
    
        $url = 'https://'.$paypal_domain.'/cgi-bin/webscr';        
        $cmd = '/usr/bin/curl '.$url.' --tlsv1.2 -d "'.$req.'"';
        exec($cmd, $res, $ret);
/* 以下のcurl関数では叩けなくなった(PHP5.3)
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
        curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1);
        curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, 'rsa_aes_128_sha');
        curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Connection: Close'));
        if ( !$res = curl_exec($ch) ) {
            $this->writeLog("FATAL", "CURL ERROR:". curl_error($ch));
        }
        curl_close($ch);*/
        return $res;
}