CakephpからPaypalへジャンプさせ、決済してもらう

新しい記事([Cakephp]PaypalエクスプレスチェックアウトAPIを使う)があります。


CakephpでPaypalを使用して決済してもらうサンプル。
Cakephp1.2(古い)ですが、1.3でも使えると思います。2.x系は分かりません。

1.Paypal Sandboxに登録する
2.テスト用アカウントを取得する(売り、買いの両方あった方がいいと思う)
 Via:http://d.hatena.ne.jp/hrendoh/20110516/1305548398
3.Cakephpを設定する
4.テストする

こんな感じの流れになります。
Paypalのテストアカウントは「売り」の方はビジネスネームを入れて登録すると決済ページに飛んだ際に「店舗名」のような感じで表示されます。
「買い」の方はお客様になるので、テスト用のクレジットカード番号を控えます。このクレカで決済のテストを行い、プログラムからジャンプしたり、決済後にプログラムに戻ったりします。


■Cakephpの設定
Via:CakePHP+Paypal決済モジュール

参考サイトをまるコピーでプログラム上はOKですが、ちょっと補足を入れつつソースなどを。


●モデル

<?php
class Paypal extends AppModel {
	var $name = 'Paypal';
	var $useTable = false;

	var $apiServer = PAYPAL_API_SERVER;
	var $expressCheckoutUrl = PAYPAL_EXPRESS_CHECKOUT_URL;

	var $version = '63.0';
	var $username = PAYPAL_API_USERNAME;
	var $password = PAYPAL_API_PASSWORD;
	var $signature = PAYPAL_API_SIGNATURE;

	var $errorMsg = array();

	function __construct() {
		App::import('Vendor', 'Request', array('file'=>'HTTP/Request.php'));
	}

	// 注文のセットアップ
	function setExpressCheckout ($item_amount, $request='Sale', $options=array()) {
		$params = array(
			'METHOD' => 'SetExpressCheckout',
			'VERSION' => $this->version,
			'USER' => $this->username,
			'PWD' =>$this->password,
			'SIGNATURE' => $this->signature,
			'PAYMENTREQUEST_0_AMT' => $item_amount,
			'PAYMENTREQUEST_0_CURRENCYCODE' => 'JPY',
			'PAYMENTREQUEST_0_ITEMAMT' => $item_amount,
			'PAYMENTREQUEST_0_PAYMENTACTION' => $request,
			'LOCALECODE' => 'JP'
		);

		$params = am($params, $options);
		foreach ($params as &$p) $p = h($p);
		$nvpStr = http_build_query($params);
		$req = new HTTP_Request($this->apiServer.'?'.$nvpStr);
		$req->setMethod(HTTP_REQUEST_METHOD_GET);

		if (!PEAR::isError($req->sendRequest())) {
			$rawResult = $req->getResponseBody();
			parse_str($rawResult, $result);

			if ($result['ACK'] == 'Success') {
				$this->expressCheckoutUrl .= "&token={$result['TOKEN']}";
				$this->expressCheckoutUrl .= '&useraction=commit';
				return $result;
			} else {
				for ($i=0; $i<=9; $i++) {
					if (!empty($result['L_ERRORCODE'.$i])) {
						$this->errorMsg[$i] = $result['L_LONGMESSAGE'.$i];
						$this->log("Set - {$result['CORRELATIONID']} - {$result['L_ERRORCODE'.$i]}", 'paypal');
					}
				}
				return false;
			}
		}
	}

	// 決済後のデータ取得
	function getExpressCheckoutDetails($token=null, $options=array()) {
		$params = array(
			'METHOD' => 'GetExpressCheckoutDetails',
			'VERSION' => $this->version,
			'USER' => $this->username,
			'PWD' =>$this->password,
			'SIGNATURE' => $this->signature,
			'TOKEN' => $token
		);

		$params = am($params, $options);
		foreach ($params as &$p) $p = h($p);
		$nvpStr = http_build_query($params);

		$req = new HTTP_Request($this->apiServer.'?'.$nvpStr);
		$req->setMethod(HTTP_REQUEST_METHOD_GET);

		if (!PEAR::isError($req->sendRequest())) {
			$rawResult = $req->getResponseBody();
			parse_str($rawResult, $result);
			if ($result['ACK'] == 'Success') {
				return $result;
			} else {
				for ($i=0; $i<=9; $i++) {
					if (!empty($result['L_ERRORCODE'.$i])) {
						$this->errorMsg[$i] = $result['L_LONGMESSAGE'.$i];
						$this->log("Get - {$result['CORRELATIONID']} - {$result['L_ERRORCODE'.$i]}", 'paypal');
					}
				}
				return false;
			}
		}
	}

	// 決済後に注文IDなどを取得
	function doExpressCheckout($token, $payerId, $amount, $request='Sale', $options=array()) {
		$params = array(
			'METHOD' => 'DoExpressCheckoutPayment',
			'VERSION' => $this->version,
			'USER' => $this->username,
			'PWD' =>$this->password,
			'SIGNATURE' => $this->signature,
			'TOKEN' => $token,
			'PAYERID' => $payerId,
			'PAYMENTREQUEST_0_AMT' => $amount,
			'PAYMENTREQUEST_0_CURRENCYCODE' => 'JPY',
			'PAYMENTREQUEST_0_PAYMENTACTION' => $request
		);

		$params = am($params, $options);
		foreach ($params as &$p) $p = h($p);
		$nvpStr = http_build_query($params);

		$req = new HTTP_Request($this->apiServer.'?'.$nvpStr);
		$req->setMethod(HTTP_REQUEST_METHOD_GET);

		if (!PEAR::isError($req->sendRequest())) {
			$rawResult = $req->getResponseBody();
			parse_str($rawResult, $result);
			if ($result['ACK'] == 'Success') {
				return $result;
			} else {
				for ($i=0; $i<=9; $i++) {
					if (!empty($result['L_ERRORCODE'.$i])) {
						$this->errorMsg[$i] = $result['L_LONGMESSAGE'.$i];
						$this->log("Do - {$params['PAYERID']} - {$result['CORRELATIONID']} - {$result['L_ERRORCODE'.$i]}", 'paypal');
					}
				}
				return false;
			}
		}
	}

	// 発送後に支払い?
	function doCapture($transactionId, $amount=0, $complete=false, $options=array()) {
		$params = array(
			'METHOD' => 'DoCapture',
			'VERSION' => $this->version,
			'USER' => $this->username,
			'PWD' =>$this->password,
			'SIGNATURE' => $this->signature,
			'AUTHORIZATIONID' => $transactionId,
			'AMT' => $amount,
			'CURRENCYCODE' => 'JPY',
			'COMPLETETYPE' => ($complete) ? 'Complete' : 'NotComplete'
		);

		$params = am($params, $options);
		foreach ($params as &$p) $p = h($p);
		$nvpStr = http_build_query($params);

		$req = new HTTP_Request($this->apiServer.'?'.$nvpStr);
		$req->setMethod(HTTP_REQUEST_METHOD_GET);

		if (!PEAR::isError($req->sendRequest())) {
			$rawResult = $req->getResponseBody();
			parse_str($rawResult, $result);
			if ($result['ACK'] == 'Success') {
				return $result;
			} else {
				for ($i=0; $i<=9; $i++) {
					if (!empty($result['L_ERRORCODE'.$i])) {
						$this->errorMsg[$i] = $result['L_LONGMESSAGE'.$i];
						$this->log('DoCap - '.$result['CORRELATIONID'].' - '.$result['L_ERRORCODE'.$i], 'paypal');
					}
				}
				return false;
			}
		}
	}

	// 注文キャンセル
	function doVoid($transactionId, $options=array()) {
		$params = array(
			'METHOD' => 'DoVoid',
			'VERSION' => $this->version,
			'USER' => $this->username,
			'PWD' =>$this->password,
			'SIGNATURE' => $this->signature,
			'AUTHORIZATIONID' => $transactionId,
			'CURRENCYCODE' => 'JPY',
		);

		$params = am($params, $options);
		foreach ($params as &$p) $p = h($p);
		$nvpStr = http_build_query($params);

		$req = new HTTP_Request($this->apiServer.'?'.$nvpStr);
		$req->setMethod(HTTP_REQUEST_METHOD_GET);

		if (!PEAR::isError($req->sendRequest())) {
			$rawResult = $req->getResponseBody();
			parse_str($rawResult, $result);
			if ($result['ACK'] == 'Success') {
				return $result;
			} else {
				for ($i=0; $i<=9; $i++) {
					if (!empty($result['L_ERRORCODE'.$i])) {
						$this->errorMsg[$i] = $result['L_LONGMESSAGE'.$i];
						$this->log('DoVoid - '.$params['AUTHORIZATIONID'].' - '.$result['L_ERRORCODE'.$i], 'paypal');
					}
				}
				return false;
			}
		}
	}

}
?>

参考サイトからとりあえずコピーしました。
PEARのHTTP_Requestを使用しますので、その設定が必要です。Vendorに入れてください。
また、VersionでPaypalAPIのバージョンを指定しますが、63でも問題なく使用できます。Paypalのマニュアルでは89が最新のようですが、バージョン間の違いがわかりません。


●PaypalAPIのキーをBootstrapに追記

// テスト用
define('PAYPAL_API_SERVER', 'https://api-3t.sandbox.paypal.com/nvp');
define('PAYPAL_EXPRESS_CHECKOUT_URL', 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout');
define('PAYPAL_API_USERNAME', 'xxxxxxxxxxxxxxxx');
define('PAYPAL_API_PASSWORD', 'xxxxxxxxxxxxxxxx');
define('PAYPAL_API_SIGNATURE', 'xxxxxxxxxxxxxxxx');
// 本番用
/*
define('PAYPAL_API_SERVER', 'https://api-3t.paypal.com/nvp');
define('PAYPAL_EXPRESS_CHECKOUT_URL', 'https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout');
define('PAYPAL_API_USERNAME', 'xxxxxxxxxxxxxxxx');
define('PAYPAL_API_PASSWORD', 'xxxxxxxxxxxxxxxx');
define('PAYPAL_API_SIGNATURE', 'xxxxxxxxxxxxxxxx');
*/

if文で切り替えでもいいのですが、手動切替えにしています。
本番用のAPIキーはエクスプレスチェックアウトに登録すると設定ページがあるので、そこで取得します。


■コントローラ

1.Paypalモデルをusesで指定する
2.決済リンクからPaypalが選ばれた時に商品名や金額、決済完了かキャンセルされた時に戻るURLなどをセット
3.Paypalサイトにジャンプさせる
4.Paypalから戻った時の処理をする

var $uses = array('Paypal', 'xxxxxx', 'xxxxxx');
・
・
・
function paypal() {
	if (!empty($this->data)) {
		// Paypal手続き
		$setOptions = array(
			'NOSHIPPING' => '1',
			'ALLOWNOTE' => '0',
			'L_PAYMENTREQUEST_0_NAME0' => '商品名を入れる',
			'L_PAYMENTREQUEST_0_DESC0' => '商品説明を入れる',
			'L_PAYMENTREQUEST_0_AMT0' => 商品金額を入れる,
			'RETURNURL' => 'http://xxxxxxxxxxx.com/コントローラ/paypalok',     // 決済完了時の戻りURL
			'CANCELURL' => 'http://xxxxxxxxxxx.com/コントローラ/paypalcancel'  // 決済キャンセル時の戻りURL
		);
		$amount = 合計金額が入る;
		if ($setResult = $this->Paypal->setExpressCheckout($amount, 'Sale', $setOptions)) {
			$express_checkout_url = $this->Paypal->expressCheckoutUrl;
		} else {
			$this->set('ppErrors', $this->Paypal->errorMsg);
			var_dump($this->Paypal->errorMsg);
			exit();
		}

		// Paypalにジャンプする
		$this->redirect($express_checkout_url);

		exit();
	}
}

// 決済完了時の処理
function paypalok() {
	// Paypalの戻り値をチェック
	if ($getResult = $this->Paypal->getExpressCheckoutDetails($this->params['url']['token'])) {
		// 決済完了時の金額が戻ってくるのでDBなどに入れたりの処理をする

		// 注文詳細
		if ($doResult = $this->Paypal->doExpressCheckout($getResult['TOKEN'], $getResult['PAYERID'], $getResult['AMT'])) {
			// 商品の詳細が戻ってくるので注文IDなどをDBに入れたりの処理をする
		}
	} else {
		// エラー
		die('error unknown');
	}
}

// 決済キャンセル時の処理
function paypalcancel() {
	//echo 'paypal cancel !!!!!!!!!!';
}

今回は1商品のみの決済なので、L_PAYMENTREQUEST_0_NAME0だけですが、マニュアルによれば複数もOKのようです。
「L_PAYMENTREQUEST_n_NAMEn」で数字を増やすとPaypalの購入リストに表示されるらしい。

また、paypalokとpaypalcancelのビューは単純に「決済が完了しました」「決済がキャンセルされました」ぐらいしか表示していません。

sandboxでのテストIDを登録したパソコンであれば、決済ページにジャンプしますが、他のPCだと「Please login to use the PayPal Sandbox features.」というようなエラーが表示されます。「ログインして!」と言っています。
テストなので他PCでも素直にログインしてテストしてください。
製作する人とチェックする人が違うと、ちょっと迷うと思いますが、チェックする人にsandboxのログイン情報を教えましょう。


Via:[PDF]エクスプレスチェックアウト サービス導入ガイド (日本語)
Via:[PDF]NVP (Name-Value Pair) API デベロッパーガイド (日本語)


追記(2013/1//20)
ご質問をいただきましたので追記します。

Refund(返金)処理ですが、基本的には
Via:RefundTransaction NVP example
ここと同じ処理です。

引数の意味等は「[PDF]NVP (Name-Value Pair) API デベロッパーガイド (日本語)」のP166あたりに書いてあります。

■モデル
上に書いたモデルに「doRefund」として追加する

// 返金処理
function doRefund($transactionId, $options=array()) {
	$params = array(
		'METHOD' => 'DoVoid',
		'VERSION' => $this->version,
		'USER' => $this->username,
		'PWD' =>$this->password,
		'SIGNATURE' => $this->signature,
		'TRANSACTIONID' => $transactionId,
		'CURRENCYCODE' => 'JPY',
	);

	$params = am($params, $options);
	foreach ($params as &$p) $p = h($p);
	$nvpStr = http_build_query($params);

	$req = new HTTP_Request($this->apiServer.'?'.$nvpStr);
	$req->setMethod(HTTP_REQUEST_METHOD_GET);

	if (!PEAR::isError($req->sendRequest())) {
		$rawResult = $req->getResponseBody();
		parse_str($rawResult, $result);
		if ($result['ACK'] == 'Success') {
			return $result;
		} else {
			for ($i=0; $i<=9; $i++) {
				if (!empty($result['L_ERRORCODE'.$i])) {
					$this->errorMsg[$i] = $result['L_LONGMESSAGE'.$i];
					$this->log('DoVoid - '.$params['AUTHORIZATIONID'].' - '.$result['L_ERRORCODE'.$i], 'paypal');
				}
			}
			return false;
		}
	}
}

■コントローラ
コントローラは管理者側での処理になります。
決済後の戻り値にある注文ID(transactionid)をDBに格納しておいて、返金を行う際に引数に入れて渡します。

if (!empty($this->data)) {
	// Paypal
	$setOptions = array(
		'REFUNDTYPE' => 'FULL',  // FULL or Partial
		//'NOTE' => '',          // required if Partial
		//'AMT' => ''            // required if Partial
	);
	$result = $this->Paypal->doRefund($this->data['Model']['transactionId'], $setOptions);
}

REFUNDTYPEに全購入返金(FULL)か一部返金(Partial)かを入れます。NOTE(メモ)とAMT(合計金額)はPartialの時は必須になります。

テストはしていませんが、たぶんこれでいいと思います。

「CakephpからPaypalへジャンプさせ、決済してもらう」への4件のフィードバック

  1. 大変参考になりました。

    これのRefundコードもモデル・コントローラーに記載いただいたりはできないでしょうか!?

  2. Refundコード誠にありがとうございます。大変助かります。

    もう一つ質問があるのですが、Paypalモデルにて、

    var $useTable = false;

    となっていますが、コントローラーで

    // 決済完了時の金額が戻ってくるのでDBなどに入れたりの処理をする

    を実施するには、当然上記はfalseではダメですよね?

    なのでテーブル名を指定してDB保存を試みているのですが、
    なぜか一向に保存できず下記のようなエラーになります。

    The eventKey variable is required
    An Internal Error Has Occurred

    これは何か特有の問題がありますでしょうか?

  3. 何度もすみません。saveできない件ですが、

    function __construct() {
    App::import(‘Vendor’, ‘Request’, array(‘file’=>’HTTP/Request.php’));
    }

    parent::__construct();

    を追加して解決しました。

  4. このモデルはPaypal決済をしてもらうだけになるので、DBはfalseです。実際の運用は他のコントローラから呼ばれて決済するようになります。

    ネットショップの注文の最後の画面で「銀行振込はこちら」「ネットバンク決済はこちら」とかにしてネットバンクならページをジャンプさせて即決済とかにしているところもあると思います。それのPaypal版になるので、carts_controller.phpとかに組み込んで使用します。
    このような仕様なので、注文IDは別モデルで保存します。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)