Paypalの定期支払導入の覚書

Paypalを利用して定期支払を実装したのだけど、まとまった日本語の資料がなかったり、資料の内容が古かったリして困ったので、実装方法などをまとめておきます。

Paypalの定期支払

Paypalの定期支払には

  • リカーリングペイメント
  • リファレンストランザクション

の2種類があります。リカーリングペイメントの特徴は

  • 定期的に決まった金額を決済できる
  • 導入の際に審査は不要
  • 買い手のPaypalアカウントに有効なクレジットカード情報登録されている必要がある
  • 決済する金額は180日ごとに最大20%しか増額できない

で、リファレンストランザクションの特徴は

  • 任意の額を任意のタイミングで決済できる
  • 導入するには審査が必要(3ヶ月間の合計売上が9万ドルを超えている)

です。今回は、リファレンストランザクションの審査をパスできそうになかったので、リカーリングペイメントを導入することにしました。

リカーリングペイメントの処理の流れ

リカーリングペイメントの処理の流れは以下の図のようになっています。

RecurringPaymentsFlow

  1. 1)のSetExpressCheckoutで、請求処理規約を設定。このとき、定期支払とは別に一回限りの請求があれば、その請求内容も含めます。
  2. 2)で受け取ったTokenを利用してPaypalにリダイレクト。この後、Payapalでログインや支払の確認がされます。
  3. 5)のGetExpressCheckoutDetailsで、定期支払プロフィールを作成するのに必要な情報(PAYERID)を取得します。
  4. 6)のDoExpressCheckoutPaymentは、一回限りの請求がある場合のみ発行します。なければこの処理は不要です。
  5. 6)のCreateRecurringPaymentsProfileは、定期支払プロフィールを作成します。ここで、毎回の支払金額や期間、トライアルの有無などを設定します。
  6. 7)で受けっとったPROFILEIDは、定期支払プロフィール情報の取得や更新、またIPN受信の際にも利用しますので、DBなどに保存しておきます。

参考)
ExpressCheckout IntegrationGuide
エクスプレスチェックアウト応用機能ガイド

SetExpressCheckoutのパラメータの例(一回限りの請求なし)

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のパラメータの詳細

GetExpressCheckoutDetailsのパラメータの例

VERSION=76.0
&PWD=APIパスワード
&USER=APIユーザ名
&SIGNATURE=API署名
&METHOD=GetExpressCheckoutDetails
&TOKEN=図の2)で受け取ったToken

このAPIのレスポンスにはPAYERIDという、Paypal内でユニークな買い手のIDが含まれます。

参考)
GetExpressCheckoutDetailsのパラメータの詳細

CreateRecurringPaymentsProfileのパラメータの例

例えば、

  • 商品名はメルマガ定期購読
  • 月額300円
  • 販売物はデジタルコンテンツ
  • 購入日は2013-7-8 午前4時(東京時間)
  • トライアル期間は1ヶ月
  • トライアル料金は0円
  • トライアル期間は1サイクル
  • 初回支払い分0円
  • 請求が失敗した場合は、次回の請求に加算する
  • 請求が5回失敗した場合は、定期支払を停止する

のような定期支払の場合、パラメータは

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

のようになります。この場合、

  • トライアル期間は購入日の2013-7-8から1ヶ月間。
  • 1ヶ月後の2013-8-8に300円が請求処理される。
  • 以降1ヶ月ごとに300円が請求処理される。

という処理がされます。上記の処理中、何らかの理由で請求処理が失敗した場合

  • 例えば、2013-8-8に300円が請求処理されるが、この処理が失敗した場合は、翌月の2013-9-8に通常請求分300円と未払い分300円を合算した600円が請求される。

のような処理が行われます。

また、例えば、支払金額と期間を

  • 2ヶ月分の金額は300円
  • トライアル期間は3週間

のようにしたい場合は、

&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上で定期支払に関する処理がされたことを知るには

Paypal上で定期支払に関する処理がされたことを知るには、IPNという仕組みを利用します。現状、設定は、PayPalアカウントにログインし、「個人設定」⇒「販売ツール」⇒「即時支払い通知」の更新よりする必要があります。(2013-9-1時点)

テクニカルサポートに問い合わせたところ、「API経由でIPN通知URLを設定するには、SetExpressCheckoutのパラメータに『PAYMENTREQUEST_0_NOTIFYURL=IPN通知URL』を含めればよい」という回答を頂きましたが、2013-9-1時点では、このパラメータは利用できません。
また、DoExpressCheckoutPaymentでPAYMENTREQUEST_0_NOTIFYURLの設定ができるようなので、これも問い合わせたところ、「DoExpressCheckoutPaymentで設定したPAYMENTREQUEST_0_NOTIFYURLがCreateRecurringPaymentsProfileで作成したプロファイルに影響を与えないのでダメ」との回答をもらいました。

参考)
Introducing IPN

定期支払の際のIPNのタイミング

定期支払のIPNのタイミングとしては以下の場合があります。

  • 定期支払が入金成功の場合(txn_type=recurring_payment)
  • 定期支払が入金失敗の場合(txn_type=recurring_payment_skipped)
  • 3回連続失敗後(txn_type=recurring_payment_failed)
  • 定期支払の期間が正常に切れる場合(txn_type=recurring_payment_expired)
  • 定期支払が立ち上がる時(txn_type=recurring_payment_profile_created)
  • paypalサイトで買い手側がキャンセルした場合(txn_type=recurring_payment_profile_cancel)
  • API経由でキャンセルした場合(txn_type=recurring_payment_profile_cancel)
  • 売り手側が定期支払を一時的に停止する場合(txn_type=recurring_payment_suspended)
  • 決済失敗がMAXFAILEDPAYMENTSに達した場合(txn_type=recurring_payment_suspended_due_to_max_failed_payment)

定期支払が入金成功の場合も、回収(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秒⇒・・・のように増加していきます。

エクスプレスチェックアウト応用機能ガイドの56ページの表を見ると、

・paypal.comのインターフェイスによって個人設定がキャンセル IPNあり
・APIを使用してプロフィールステータスを変更 IPNなし
・APIを使用してプロフィールを更新 IPNなし

となっているが、2013-9-1時点では、ManageRecurringPaymentsProfileStatus API経由でキャンセルした際にもIPNは通知されます。
定期支払の期間を一週間に設定したような場合に定期支払が失敗すると、再回収日が次の定期支払日を乗り越えるということが起こります。この場合、乗り越える分の再回収は行われず、txn_type=recurring_payment_failedの通知がきます。(MAXFAILEDPAYMENTSに達した場合は、txn_type=recurring_payment_failedではなく、txn_type=recurring_payment_suspended_due_to_max_failed_paymentの通知がきます)

IPNで受信するパラメータ

定期支払プロフィールを作成した際、削除した際、定期支払に成功した際に受信する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
)

パラメータの詳細については以下の参考を見てください。

参考)
IPN and PDT Variables

定期支払の際のIPNを受信するスクリプトの例

<?php
if(function_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の実装例1
IPNの実装例2

※IPNでなぜかエラーが出る場合、文字コードが原因の可能性があります。こちらのページを参考に設定を確認してみると解消するかもしれません。

※「You are not signed up to accept payment for digitally delivered goods.」というエラーが出る場合は参考にしてください。

その他

Paypalテクニカルサポート

投稿日:
カテゴリー: php タグ:

2件のコメント

コメントは受け付けていません。