Paypalを利用して定期支払を実装したのだけど、まとまった日本語の資料がなかったり、資料の内容が古かったリして困ったので、実装方法などをまとめておきます。
Paypalの定期支払には
の2種類があります。リカーリングペイメントの特徴は
で、リファレンストランザクションの特徴は
です。今回は、リファレンストランザクションの審査をパスできそうになかったので、リカーリングペイメントを導入することにしました。
リカーリングペイメントの処理の流れは以下の図のようになっています。
参考)
ExpressCheckout IntegrationGuide
エクスプレスチェックアウト応用機能ガイド
VERSION=76.0 &PWD=APIパスワード &USER=APIユーザ名 &SIGNATURE=API署名 &METHOD=SetExpressCheckout &RETURNURL=Paypalの処理後、リダイレクトされるURL &CANCELURL=Paypalの処理をキャンセルしたときにリダイレクトされるURL &LOCALECODE=ja_JP &NOSHIPPING=1 &L_BILLINGTYPE0=RecurringPayments &L_BILLINGAGREEMENTDESCRIPTION0=商品名 &PAYMENTREQUEST_0_PAYMENTACTION=Sale &PAYMENTREQUEST_0_CURRENCYCODE=JPY &PAYMENTREQUEST_0_AMT=0 &PAYMENTREQUEST_0_ITEMAMT=0
参考)
SetExpressCheckoutのパラメータの詳細
VERSION=76.0 &PWD=APIパスワード &USER=APIユーザ名 &SIGNATURE=API署名 &METHOD=GetExpressCheckoutDetails &TOKEN=図の2)で受け取ったToken
このAPIのレスポンスにはPAYERIDという、Paypal内でユニークな買い手のIDが含まれます。
参考)
GetExpressCheckoutDetailsのパラメータの詳細
例えば、
のような定期支払の場合、パラメータは
VERSION=76.0 &PWD=APIパスワード &USER=APIユーザ名 &SIGNATURE=API署名 &METHOD=CreateRecurringPaymentsProfile &COUNTRYCODE=JP &TOKEN=図の2)で受け取ったToken &PAYERID=GetExpressCheckoutDetailsのレスポンスのPAYERID &PROFILESTARTDATE=2013-07-08T04:00:00+900 &DESC=メルマガ定期購読 &BILLINGPERIOD=Month &BILLINGFREQUENCY=1 &AMT=300 &CURRENCYCODE=JPY &TOTALBILLINGCYCLES=0 &TRIALBILLINGPERIOD=Month &TRIALBILLINGFREQUENCY=1 &TRIALTOTALBILLINGCYCLES=1 &TRIALAMT=0 &L_PAYMENTREQUEST_0_ITEMCATEGORY0=Digital &L_PAYMENTREQUEST_0_NAME0=メルマガ定期購読 &L_PAYMENTREQUEST_0_AMT0=300 &L_PAYMENTREQUEST_0_QTY0=1 &AUTOBILLOUTAMT=AddToNextBilling &MAXFAILEDPAYMENTS=5
のようになります。この場合、
という処理がされます。上記の処理中、何らかの理由で請求処理が失敗した場合
のような処理が行われます。
また、例えば、支払金額と期間を
のようにしたい場合は、
&BILLINGPERIOD=Month &BILLINGFREQUENCY=1 &TRIALBILLINGPERIOD=Month &TRIALBILLINGFREQUENCY=1
の部分を
&BILLINGPERIOD=Month &BILLINGFREQUENCY=2 &TRIALBILLINGPERIOD=Week &TRIALBILLINGFREQUENCY=3
のようにします。
参考)
CreateRecurringPaymentsProfileのパラメータの詳細
トライアル有り、無しの定期支払プロフィールを動的に作成するには、SetExpressCheckoutの後、GetExpressCheckoutDetailsで取得できるPAYERID(購入者ID、Paypal内でユニーク)を利用します。
このPAYERIDが以前に、同じ商品購入に利用されたかどうかをチェックし、購入がなければ、トライアルがある定期支払プロフィールを作成する。購入があれば、トライアルのない定期支払プロフィールを作成するようにしたらよいです。
定期支払プロフィール情報の取得や更新をするには、CreateRecurringPaymentsProfileを発行した際のレスポンスに含まれるPROFILEID(定期支払プロフィールID、Paypal内でユニーク)を利用します。
定期支払プロフィールの情報を取得するにはGetRecurringPaymentsProfileDetailsを利用します。
定期支払のキャンセル、一時停止、再開はどはManageRecurringPaymentsProfileStatusを利用します。
例えば、キャンセルの場合は
VERSION=76.0 &PWD=APIパスワード &USER=APIユーザ名 &SIGNATURE=API署名 &METHOD=ManageRecurringPaymentsProfileStatus &ACTION=Cancel &PROFILEID=CreateRecurringPaymentsProfileのレスポンスのPROFILEID
のようなパラメータになります。
定期支払プロフィールの更新や、未払いの対応はUpdateRecurringPaymentsProfileを利用します。
Paypal上で定期支払に関する処理がされたことを知るには、IPNという仕組みを利用します。現状、設定は、PayPalアカウントにログインし、「個人設定」⇒「販売ツール」⇒「即時支払い通知」の更新よりする必要があります。(2013-9-1時点)
定期支払のIPNのタイミングとしては以下の場合があります。
定期支払が入金成功の場合も、回収(payment_status=Completed)と保留(payment_status=Pending)の場合があります。保留(payment_status=Pending)の場合は、状況が改善すれば、回収(payment_status=Completed)の通知がきます。改善しないようであれば、txn_type=recurring_payment_skippedの通知がきます。
定期支払が失敗した場合、txn_type=recurring_payment_skippedの通知が来ます。失敗すると5日間隔で再回収(再決済)がされます。再回収に3回連続して失敗した場合、txn_type=recurring_payment_failedの通知がきます。
CreateRecurringPaymentsProfileでMAXFAILEDPAYMENTS(最大失敗数)を指定している場合、txn_type=recurring_payment_failedの通知を受けた回数が、MAXFAILEDPAYMENTSに達すると、txn_type=recurring_payment_suspended_due_to_max_failed_paymentが通知され、定期支払プロフィール自体が一時的停止状態となります。
IPN通知は送信後、HTTPレスポンス200を得ないとエラーが発生したと判断し、最大16回まで再送信されます。従って、なんらかの事情で処理を中断したい場合は、HTTPレスポンス500などを返信するとよいです。なお、IPN通知の時間の間隔は、10秒⇒20秒⇒40秒⇒80秒⇒・・・のように増加していきます。
定期支払プロフィールを作成した際、削除した際、定期支払に成功した際に受信するIPNのパラメータの例です。
●定期支払が立ち上がる時(txn_type=recurring_payment_profile_created)
POST: Array
(
[payment_cycle] => Weekly
[txn_type] => recurring_payment_profile_created
[last_name] =>
[next_payment_date] => 03:00:00 Sep 06, 2013 PDT
[residence_country] => JP
[initial_payment_amount] => 0
[currency_code] => JPY
[time_created] => 21:49:27 Sep 05, 2013 PDT
[verify_sign] => AwHaEGczPmr8aGypRLasxdoQxeffALHLT4Maxrb3oFAnyDD6tbYisP3B
[period_type] => Trial
[payer_status] => verified
[test_ipn] => 1
[tax] => 0
[payer_email] => 購入者のメルアド
[first_name] =>
[receiver_email] => 売り手のメルアド
[payer_id] => 購入者ID(PAYERID)
[product_type] => 1
[shipping] => 0
[amount_per_cycle] => 0
[profile_status] => Active
[charset] => windows-1252
[notify_version] => 3.7
[amount] => 0
[outstanding_balance] => 0
[recurring_payment_id] => 定期支払プロフィールID(PROFILEID)
[product_name] =>
[ipn_track_id] => f2bc9f72bc8a7
) ●paypalサイトで買い手側がキャンセルした場合(txn_type=recurring_payment_profile_cancel)
POST: Array
(
[payment_cycle] => Weekly
[txn_type] => recurring_payment_profile_cancel
[last_name] => c
[next_payment_date] => N/A
[residence_country] => JP
[initial_payment_amount] => 0
[currency_code] => JPY
[time_created] => 23:12:44 Sep 04, 2013 PDT
[verify_sign] => AdiVRNf5OTV9pPAxyKig6GO0FXccA08KQM8AmxczibJpudrNr5fFaTCh
[period_type] => Regular
[payer_status] => verified
[test_ipn] => 1
[tax] => 0
[payer_email] => 購入者のメルアド
[first_name] =>
[receiver_email] => 売り手のメルアド
[payer_id] => 購入者ID(PAYERID)
[product_type] => 1
[shipping] => 0
[amount_per_cycle] => 1000
[profile_status] => Cancelled
[charset] => Shift_JIS
[notify_version] => 3.7
[amount] => 1000
[outstanding_balance] => 0
[recurring_payment_id] => 定期支払プロフィールID(PROFILEID)
[product_name] => L}KeXgij
[ipn_track_id] => 29050a5b83f38
) ●定期支払が入金成功の場合(txn_type=recurring_payment)
POST: Array
(
[mc_gross] => 1000
[outstanding_balance] => 0
[period_type] => Regular
[next_payment_date] => 03:00:00 Sep 12, 2013 PDT
[protection_eligibility] => Ineligible
[payment_cycle] => Weekly
[tax] => 0
[payer_id] => 購入者ID(PAYERID)
[payment_date] => 09:48:01 Sep 05, 2013 PDT
[payment_status] => Completed
[product_name] => L}KeXgij
[charset] => Shift_JIS
[recurring_payment_id] => 定期支払プロフィールID(PROFILEID)
[first_name] =>
[mc_fee] => 57
[notify_version] => 3.7
[amount_per_cycle] => 1000
[payer_status] => verified
[currency_code] => JPY
[business] => 売り手のメルアド
[verify_sign] => AP07VK1pC2eZJkq.-nvebqbuZLGIAxSFQhGKZnZS5ENGfLvsiX0Zh4Wt
[payer_email] => 購入者のメルアド
[initial_payment_amount] => 0
[profile_status] => Active
[amount] => 1000
[txn_id] => 2AM81146YH885684V
[payment_type] => instant
[last_name] => c
[receiver_email] => 売り手のメルアド
[payment_fee] =>
[receiver_id] => 2D7WLQ3ES5ZAJ
[txn_type] => recurring_payment
[mc_currency] => JPY
[residence_country] => JP
[test_ipn] => 1
[transaction_subject] => L}KeXgij
[payment_gross] =>
[shipping] => 0
[product_type] => 1
[time_created] => 23:12:44 Sep 04, 2013 PDT
[ipn_track_id] => 89cfcdf06d4dc
) パラメータの詳細については以下の参考を見てください。
<?php
if(_exists('get_magic_quotes_gpc')) {
$get_magic_quotes_exists = true;
}
//POSTされてきたデータをpaypalのチェック用に加工する。
$req = 'cmd=_notify-validate';
foreach ($_POST as $key => $value) {
if($get_magic_quotes_exists == true && get_magic_quotes_gpc() == 1) {
$value = urlencode(stripslashes($value));
} else {
$value = urlencode($value);
}
$req .= "&$key=$value";
}
//POSTされてきたデータの正当性をpaypalでチェック
$ch = curl_init('https://www.sandbox.paypal.com/webscr');
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, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Connection: Close'));
if( !($res = curl_exec($ch)) ) {
//curl実行中にエラー発生
curl_close($ch);
echo 'Error: '.curl_error($ch);
exit;
}
curl_close($ch);
if (strcmp ($res, "VERIFIED") != 0) {
//POSTされてきたデータは正当でない
echo 'Error: invalid post data';
exit;
}
$data=array();
$data['txn_id']=isset($_POST['txn_id'])?$_POST['txn_id']:'';
$data['txn_type']=$_POST['txn_type'];
$data['receiver_email']=$_POST['receiver_email'];
$data['payer_email']=$_POST['payer_email'];
$data['payer_id']=$_POST['payer_id'];
$data['recurring_payment_id']=$_POST['recurring_payment_id'];
$data['currency_code']=isset($_POST['currency_code'])?$_POST['currency_code']:(isset($_POST['mc_currency'])?$_POST['mc_currency']:'');
$data['amount']=isset($_POST['amount'])?$_POST['amount']:(isset($_POST['mc_gross'])?$_POST['mc_gross']:'');
$data['payment_status']=isset($_POST['payment_status'])?$_POST['payment_status']:'';
$data['pending_reason']=isset($_POST['pending_reason'])?$_POST['pending_reason']:'';
$data['reason_code']=isset($_POST['reason_code'])?$_POST['reason_code']:'';
$txn_type=strtoupper($_POST['txn_type']);
$payment_status=strtoupper($_POST['payment_status']);
//$txn_typeで処理を分岐します。
//定期支払プロフィールの特定には$data['recurring_payment_id']の値を利用します。
if($txn_type=='RECURRING_PAYMENT_PROFILE_CANCEL'){
//paypalサイトで買い手側がキャンセルした
}else if($txn_type=='RECURRING_PAYMENT'){
//定期決済を行った
if($payment_status=='COMPLETED'){
//回収成功
//txn_idは、決済ごとに設定されるIDで、決済が行われた際にのみ設定される。
//この値を利用して、決済後の処理を重複しないようにする。
}else{
//pending。何らかの理由により回収できなかった。
//pending_reasonやreason_codeを確認する。
}
}else if($txn_type=='RECURRING_PAYMENT_SKIPPED'){
//定期決済でpendingになった後、状況が改善しない場合。
//状況が改善するまで再回収を行う。期間は5日。
//3回連続で再回収できな場合は、RECURRING_PAYMENT_FAILEDがくる。
}else if($txn_type=='RECURRING_PAYMENT_FAILED'){
//RECURRING_PAYMENT_SKIPPEDが3回続いた場合。
//MAXFAILEDPAYMENTSで設定する失敗数として1回カウントされる。
}else if($txn_type=='RECURRING_PAYMENT_SUSPENDED_DUE_TO_MAX_FAILED_PAYMENT'){
//RECURRING_PAYMENT_FAILEDを受け取った回数がMAXFAILEDPAYMENTSを超えた
//プロファイル自体が一時的停止状態となる。
}
echo 'SUCCESS<br />';
print_r($data);
exit; ※IPNでなぜかエラーが出る場合、文字コードが原因の可能性があります。こちらのページを参考に設定を確認してみると解消するかもしれません。
※「You are not signed up to accept payment for digitally delivered goods.」というエラーが出る場合は参考にしてください。