PHP
CakePHP
PayPal
REST-API
ExpressCheckoutAPI

[CakePHP2.x]PayPalプラグインでREST API決済

More than 1 year has passed since last update.

株式会社ネクステージが開発・運用を行っている演劇パスというサービスではPayPal IPNを利用して決済を行っていましたが、この度セキュリティ強化と、タイムアウトの実装のため、PayPal REST API(ExpressCheckout)に切り替えを行いました。
開発の際の注意点などを含め導入方法について、解説していきたいと思います。

今回は、タイムアウトの処理を足したいというだけなので、PayPal公式や他サイトで紹介している、自サイトに戻ってきた後、ユーザーによる決済の確認を行うという動作は省略し、Paypal側がOKであれば自動的に決済を完了する形で実装しました。

環境

  • PHP 7.0
  • CakePHP 2.2.5

使用したプラグイン

Paypal Plugin for CakePHP 2.x https://github.com/robmcvey/cakephp-paypal

IPNとREST APIの違い

最初に結論を言うと、今回の実装ではユーザーからの見た目はほとんど変わりません。
PayPalにデーターを送信する方法と決済が完了するタイミングが変わるだけです。
PayPalの決済手段と違いについては以下のサイトが綺麗にまとまっているので参照してみてください。
http://akiyoko.hatenablog.jp/entry/2016/08/22/082539

使い方

1.プラグインのインストール

https://github.com/robmcvey/cakephp-paypal からプラグインをダウンロードして
app/Plugin/Paypal/にコピー

bootstrap.php に以下を追記

bootstrap.php
CakePlugin::load('Paypal');

使用する前もしくは、AppModel、AppControllerなどに以下を追記

$paypal_params = [
    'sandboxMode' => false, //falseだと本番(Live)、trueだとテスト(sandbox)
    'nvpUsername' => 'username', //ユーザーネーム
    'nvpPassword' => 'password', //APIのパスワード
    'nvpSignature' => 'signature' //APIのシグネーチャー
];
App::uses('Paypal', 'Paypal.Lib');
$this->Paypal = new Paypal($paypal_parms);

これで準備完了

2.オーダーを送信

PayPalに移動する前にPayPalに決済する情報を送信します。

setExpressCheckout
$items[] = [
    'name' => '商品名',
    'description' => '商品詳細',
    'subtotal' => '値段',
    'tax' => '税金(外税)',
    'shipping' => '商品別送料',
    'qty' => 1//個数
];
$order = [
    'description' => '決済名',
    'currency' => 'JPY',//日本円
    'return' => '決済処理を行うURL',
    'cancel' => '決済を中断した時戻るURL',
    'custom' => 'カスタムMessage',
    'items' => $items,
    'shipping' => '送料'
];
$this->Session->write('order',$order);
try {
    $setExpressCheckout = $this->Paypal->setExpressCheckout($order);//PayPalにリダイレクトするためのURLが返ってくる
    $this->redirect($setExpressCheckout); //PayPalに進む
} catch (Exception $e) {
    //debug($e->getMessage());
}

注意点

$itemsは、複数のアイテムが指定できる。
taxは外税で計算されて、PayPalで表示される時は内税になるので、紛らわしい。
subtotalはトータルと書いてあるけど、単価
請求金額=(subtotal+tax+shipping)×qtyなので注意
tax、shippingの指定は任意

$orderのcustomには、任意の文字列を入れて、決済完了用ページにPayPalからリダイレクトされる際に渡されるもののはずだけど、返ってこない。
customと、shippingは任意
決済を完了する時に完全に一致する$orderを送信する必要があるので、今回はsessionに書き込んでおく。

3.決済情報を受け取り決済を完了

PayPal側でユーザーの認証と、準備が完了したら、setExpressCheckoutの$order['return']で指定したURLに
トークンとPlayerIDと一緒にリダイレクトされる。

http://example.com/callbackpaypal/?token=EC-44V10330880299912&PayerID=FCT3VE4Y4Q18V

こんな感じのGETパラメーターが付加されてリダイレクトしてくるので、決済を完了させる。

DoExpress
//決済に必要なデータの所得
$token = $this->request->query('token');
$payerId = $this->request->query('PayerID');
$order = $this->Session->read('order');
$this->Session->delete('order');
if (empty($token) || empty($payerId || empty($order))) {
    //必要なデータが不足している
    return;
}
try {
    //決済情報が有効かどうかPayPalに問い合わせる
    $getExpressCheckoutDetails = $this->Paypal->getExpressCheckoutDetails($token);
    if ($getExpressCheckoutDetails['PAYMENTREQUESTINFO_0_ERRORCODE'] != 0) {
        //決済を続けられない
        throw new Exception("getExpressCheckoutDetailsError", $getExpressCheckoutDetails['PAYMENTREQUESTINFO_0_ERRORCODE']);
    }

    # ここにDB書き込みなどサイト内部の処理を書く

    //決済を確定する
    $doExpressCheckoutPayment = $this->Paypal->doExpressCheckoutPayment($order, $token, $payerId);
    if (!$doExpressCheckoutPayment) {
        //決済に失敗
        throw new Exception("doExpressCheckoutPayment",1);
    }

} catch (Exception $e) {
    //debug($e->getMessage());
}

注意点

$getExpressCheckoutDetails['PAYMENTREQUESTINFO_0_ERRORCODE']に0以外の数字が入っている場合何らかの理由で決済ができないトークンであることになる。トークンの有効期限切れや、すでに決済が行われたトークン、トークンが見つからない時などがこの場合に当たる。
getExpressCheckoutDetailsで決済できないトークンの場合でも、APIの処理自体はsuccessが返ってくるため、例外は発生しないので注意。通信エラーなどはちゃんと例外になる。
doExpressCheckoutPaymentで決済ができなかった場合、使用してみた感じでは必ず例外が発生するので、直下のifまで処理されず全て例外になる。
この場合、例外は PaypalExceptionか、PaypalRedirectExceptionが発生する。

まとめ

以上でとりあえず、CakePHPでのPayPalプラグインを使用したREST APIのテストはできるはずです。
最初に述べた通り今回は、callbackされてきたページでの決済の確認をユーザーに行わず、Paypal側がOKであれば自動的に決済を完了する形で実装しました。
もちろん、お好みで、ユーザーへの確認画面などを挟むこともできます。
実際の決済にはDBへの書き込みや、決済に失敗した時の処理などが必要ですが、参考になれば幸いです。
では

資料・付録

実装の際にLogを取るためにREST APIの戻り値を調べたので、付録として参考までに一覧をつけておきたいと思います。
調査を行ったのは2016年12月です。実際に返ってきたパラメーターをなるべく写していますが、トークンやIDなどは、加工しています。
Paypalの仕様が変わることもあるので、こんなことが受け取れるんだと参考にしてください。
一覧を見るとわかると思いますが、customの変数は返ってきません。(なぜかわからない)
今回使用したプラグインでは機能がないものが多く、決め打ちの値が返ってくるのもかなりあります。(商品のサイズとかは全て0)

getExpressCheckoutDetailsの戻り値

パラメータ名 説明 定数一覧
TOKEN EC-2096785279340000W 決済トークン
BILLINGAGREEMENTACCEPTEDSTATUS 0 定期支払の受け入れ 0(受け入れていない)
1(受け入れた)
CHECKOUTSTATUS PaymentActionNotInitiated 決済の状況 PaymentActionNotInitiated
(支払いが開始されていない)
PaymentActionFailed
(決済失敗)
PaymentActionInProgress
(決済処理中)
PaymentActionCompleted
(決済完了済み)
TIMESTAMP 2016-12-14T03:48:54Z タイムスタンプ
CORRELATIONID 39fb8b76ad8nn レスポンスID
ACK Success リクエストのステータス
VERSION 104 APIバージョン
BUILD 0 サブバージョン
EMAIL sample@example.com 買い手のアドレス
PAYERID XXXXXXXXXXXXX 買い手のユーザーID
PAYERSTATUS verified ユーザーのステータス verified
unverified
FIRSTNAME Test ユーザーの名前
LASTNAME User 苗字
COUNTRYCODE JP ユーザーの所属国
CURRENCYCODE JPY 支払い通貨
AMT 1200 合計
ITEMAMT 1200 商品価格の合計
SHIPPINGAMT 0 送料合計
HANDLINGAMT 0 梱包費合計
TAXAMT 0 税金合計
DESC 決済名 決済の詳細
INSURANCEAMT 0 出荷保険
SHIPDISCAMT 0 送料の割引
L_NAME0 商品名 商品名
L_QTY0 1 商品数
L_TAXAMT0 0 消費税(外税)
L_AMT0 1200 商品の値段
L_DESC0 商品の詳細 商品名の詳細
L_ITEMWEIGHTVALUE0 0 商品の重さ(単位無)
L_ITEMLENGTHVALUE0 0 商品の長さ(単位無)
L_ITEMWIDTHVALUE0 0 商品の幅(単位無)
L_ITEMHEIGHTVALUE0 0 商品の高さ(単位無)
L_ITEMCATEGORY0 Physical デジタルコンテンツかどうか Digital
Physical
PAYMENTREQUEST_0_
CURRENCYCODE
JPY 通貨
PAYMENTREQUEST_0_AMT 1200 支払い合計
PAYMENTREQUEST_0_ITEMAMT 1200 購入金額の合計(送料等を除く)
PAYMENTREQUEST_0_SHIPPINGAMT 0 送料合計
PAYMENTREQUEST_0_HANDLINGAMT 0 梱包費
PAYMENTREQUEST_0_TAXAMT 0 税金合計
PAYMENTREQUEST_0_DESC 決済の説明 支払いの説明
PAYMENTREQUEST_0_
INSURANCEAMT
0 出荷保険費用
PAYMENTREQUEST_0_
SHIPDISCAMT
0 送料の割引料金
PAYMENTREQUEST_0_
INSURANCEOPTIONOFFERED
FALSE 保険適用の有無
PAYMENTREQUEST_0_
ADDRESSNORMALIZATIONSTATUS
None 住所正規化ステータス?
L_PAYMENTREQUEST_0_NAME0 商品名 商品名
L_PAYMENTREQUEST_0_QTY0 1 商品数
L_PAYMENTREQUEST_0_TAXAMT0 0 消費税
L_PAYMENTREQUEST_0_AMT0 1200 値段
L_PAYMENTREQUEST_0_DESC0 商品の詳細 商品の詳細
L_PAYMENTREQUEST_0_
ITEMWEIGHTVALUE0
0 商品の重さ(単位無)
L_PAYMENTREQUEST_0_
ITEMLENGTHVALUE0
0 長さ
L_PAYMENTREQUEST_0_
ITEMWIDTHVALUE0
0
L_PAYMENTREQUEST_0_
ITEMHEIGHTVALUE0
0 高さ
L_PAYMENTREQUEST_0_
ITEMCATEGORY0
Physical デジタルコンテンツかどうか Digital
Physical
PAYMENTREQUESTINFO_0_
ERRORCODE
0 エラーコード(0はエラーなし)

doExpressCheckoutPaymentの戻り値

パラメータ名 説明 定数一覧
TOKEN EC-7GA30889WP510000A 決済トークン
SUCCESSPAGEREDIRECTREQUESTED FALSE 取引完了後にPayPalにリダイレクトするか
TIMESTAMP 2016-12-14T03:59:35Z タイムスタンプ
CORRELATIONID e39075c57fnn レスポンスID
ACK Success レスポンスステータス
VERSION 104 AIPバージョン
BUILD 0 APIのサブバージョン
INSURANCEOPTIONSELECTED FALSE 保険の使用
SHIPPINGOPTIONISDEFAULT FALSE デフォルトの配送方法を選択したか
PAYMENTINFO_0_TRANSACTIONID 9AH12342KL1211703 トランザクションID
APIでキャンセルなどをする際に必要
PAYMENTINFO_0_TRANSACTIONTYPE cart トランザクションタイプ
PAYMENTINFO_0_PAYMENTTYPE instant 決済のタイミング none(支払いなし)
echeck(後払い)
instant(即時)
PAYMENTINFO_0_ORDERTIME 2016-12-14T03:59:34Z 決済の時間
PAYMENTINFO_0_AMT 1200 決済された金額
PAYMENTINFO_0_FEEAMT 67 決済から引かれた手数料(内)
PAYMENTINFO_0_TAXAMT 0 税金
PAYMENTINFO_0_CURRENCYCODE JPY 通貨
PAYMENTINFO_0_PAYMENTSTATUS Completed 決済のステータス
PAYMENTINFO_0_PENDINGREASON None 支払いが保留になった理由
PAYMENTINFO_0_REASONCODE None トランザクションタイプが一致しない場合の説明
PAYMENTINFO_0_PROTECTIONELIGIBILITY Ineligible 決済がPaypalの保護対象かどうか
PAYMENTINFO_0_PROTECTIONELIGIBILITYTYPE None 保護の種類
PROTECTIONELIGIBILITY 3GFRTLTJUAJA4 公式マニュアルでは
PAYMENTINFO_n_PROTECTIONELIGIBILITYと同じ
PAYMENTINFO_0_ERRORCODE 0 エラーコード なしの場合は0
PAYMENTINFO_0_ACK Success 結果のステータス