前言
有嘗試串接過 PayPal 的人都知道, PayPal 提供了好幾種方式供使用者串接使用。
本篇記錄了:
- 如何使用 PayPal 付款標準版 (PayPal Payment Standard) 來付款結帳。
- 如用使用 PayPal 即時付款通知 (PayPal IPN) 來驗證付款結果。
- 如何在 PayPal 付款標準版 (PayPal Payment Standard) 中,自定義多個商品以及每個商品的明細,包含名稱、單價、數量。
在本文章中,我使用的是PHP的框架,Laravel
因為此篇文章主要紀錄我這個專案大概的一個流程,雖說主題是金流部分,但難免會記錄到一些跟金流無關的部分。
可以直接從’建金流訂單’的部分開始看即可。
驗證
此節是整個流程的一個程序,跟金流較無關係,可以跳過。
$toBeValidatedCondition = [ |
收集必要資訊
因為我在做這個專案時,前端的時間比較吃緊一點,所以後端這邊決定除必要資訊之外,後端這邊將所有資訊搞定,盡量讓前端帶最少的資料,做最多的事。
$toBeSavedInfo = [ |
分流點
因為這個專案接了兩家金流,所以會需要一個地方來判定金流服務商
switch ($thirdPartyPaymentService->id) |
建金流訂單
在使用者按下付款之後,一張臨時的金流訂單會被建立。此訂單只介於你與你與 PayPal 之間,使用者不會接觸到這張訂單。
因為會一次性的寫入兩張 table ,所以這邊會特別使用 Laravel 的 Transaction 來將資料處理,如果對 Laravel Transaction 有興趣的,可以參考我在另外一篇文章中,有一小段針對 Laravel Transaction 的解說
public function make(Array $toBeSavedInfo, Request $request, Recipient $recipient) |
建立提交付款申請的 URL
這邊會用到很多 PayPal 付款標準版 (PayPal Payment Standard) 的 變量 (variable),各種變量的使用可以參考這篇文章
另外,因為 Ray 在做這個案子時,前端的時間上比較吃緊,所以 Ray 將所以非必要的資料全部由後端這邊處理,前端只帶入先前已建立的使用者訂單,後端從資料庫內調出所有的資料並提供給 PayPal
public function send(Array $toBeSavedInfo, Request $request, Recipient $recipient) |
使用者付款
- 使用者經由我們上面產出的 URL 到達 PayPal 付款頁面,這邊請先去申請測試帳號
到達付款頁面
這邊可以看到我們指定的商品明細,以及金額,選擇繼續
這邊可以看到我們指定的住址
交易成功,這邊可以看到全部的細節
驗證付款狀態
使用者完成付款程序後, PayPal 會發一封 IPN 給我們,有關於 IPN 的規格可以參考官方文件
首先,我們先安裝 PayPal 的 官方 IPN CODE SAMPLES
git clone https://github.com/paypal/ipn-code-samples
進到
php
的資料夾內cd ipn-code-samples/php
接下來,我們複製
PaypalIPN.php
到我們的專案內, Ray 是把它放到App
底下。接著,在把
ipn-code-samples
裡頭的cert
整個資料夾也放到App
底下,大概如下圖再來,我們到
composer.json
檔案中,在autoload-dev
底下的files
加入PaypalIPN.php
這個檔案,如果沒有files
的可能要自己建一個"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
},
"files": [
"app/Helpers.php",
"app/AllPay.Payment.Integration.php",
"app/PaypalIPN.php"
]
},重新執行
composer
,在terminal
的專案資料夾底下,執行composer install
萬事俱備,只欠東風! 接下來讓我們將
example_usage_advanced.php
的檔案裡頭的內容複製到你想要的地方,可以是你的Controller,也可以是你的某個class下面的一個function,如下:
下面的 code 有點長,可以不必全看,除了我中文有特別解釋的地方之外,大概跟原本的sample一樣。public function listen(Request $request)
{
// 因為官方sample是去接 $_POST,所以這邊直接將Laravel的輸入轉成 POST ,想要自己改的人也可以哦
$_POST = $request->post();
// 這個資訊代表是否該交易已付款
$payment_status = $_POST['payment_status'];
// 還記得我們之前帶過去的金流訂單號碼嗎?
$merchant_trade_no = $_POST['custom'];
// 很重要,等等會用到
$txn_id = $_POST['txn_id'];
$txn_type = $_POST['txn_type'];
// 付款時間
$payment_date = Carbon::parse($_POST['payment_date'])->setTimezone('UTC');
// 總付款金額
$mc_gross = $_POST['mc_gross'];
// 幣別
$mc_currency = $_POST['mc_currency'];
$enable_sandbox = env('PAYPAL_SANDBOX_ENABLESANDBOX');
// 這邊表示收款人的email,如果IPN裡頭的收款人不在這個清單裡面的話,驗證將會失敗
// Use this to specify all of the email addresses that you have attached to paypal:
$my_email_addresses = array(env('PAYPAL_SANDBOX_MAIL'));
// Set this to true to send a confirmation email:
$send_confirmation_email = env('PAYPAL_SANDBOX_SEND_CONFIRMATION_EMAIL');
$confirmation_email_address = "buybuybuygogo@gmail.com";
$from_email_address = "test@gmail.com";
// 選true的話,會自動記log
// Set this to true to save a log file:
$save_log_file = env('PAYPAL_SANDBOX_SAVE_LOG_FILE');
$log_file_dir = storage_path() . "/app/payment_logs";
// Here is some information on how to configure sendmail:
// http://php.net/manual/en/function.mail.php#118210
// 這邊就是主要驗證的function
$ipn = new PaypalIPN();
if ($enable_sandbox)
{
$ipn->useSandbox();
}
$verified = $ipn->verifyIPN();
$data_text = "";
foreach ($_POST as $key => $value)
{
$data_text .= $key . " = " . $value . "\r\n";
}
$test_text = "";
if ($_POST["test_ipn"] == 1)
{
$test_text = "Test ";
}
// 上面提到的mail,就在這邊確認
// Check the receiver email to see if it matches your list of paypal email addresses
$receiver_email_found = false;
foreach ($my_email_addresses as $a)
{
if (strtolower($_POST["receiver_email"]) == strtolower($a))
{
$receiver_email_found = true;
break;
}
}
date_default_timezone_set("America/Los_Angeles");
list($year, $month, $day, $hour, $minute, $second, $timezone) = explode(":", date("Y:m:d:H:i:s:T"));
$date = $year . "-" . $month . "-" . $day;
$timestamp = $date . " " . $hour . ":" . $minute . ":" . $second . " " . $timezone;
$dated_log_file_dir = $log_file_dir . "/" . $year . "/" . $month;
$paypal_ipn_status = "VERIFICATION FAILED";
if ($verified)
{
// 進到下面的if之後,表示已經驗證成功了,我們可以在驗證成功之後做一些該做的事
$paypal_ipn_status = "RECEIVER EMAIL MISMATCH";
if ($receiver_email_found)
{
$paypal_ipn_status = "Completed Successfully";
$PayPal = (new PayPal())->where('merchant_trade_no', $merchant_trade_no)->first();
// 這邊檢查了幾項,大概如下:
// 1. 檢查txn_id,為了避免這筆交易之前就已經有處理過。所以資料庫裡面如果已經有這個txn_id,將不予理會
// 2. 檢查mc_gross,總金額必須與我們金流訂單裡頭的金額相等
// 3. 檢查幣別,幣別必須與我們金流訂單裡頭的幣別相同
// 4. 檢查payment_status,該交易必需已經付款完成
if ((!PayPal::checkIfTxnIdExists($txn_id)) && ($mc_gross == $PayPal->total_amount) && ($mc_currency == $PayPal->mc_currency) && ($payment_status == 'Completed'))
{
// 將該txd_id新增到該金流訂單內,並更新該訂單數據,主要可被識別為已結單。
$PayPal->update(['txn_id' => $txn_id, 'txn_type' => $txn_type, 'payment_date' => $payment_date, 'status' => 1, 'expiry_time' => null]);
$recipient = $PayPal->recipient;
$orderRelations = $PayPal->orderRelations->where('payment_service_id', 2);
// 更新完金流訂單後,根據該金流訂單取得相對應的使用者訂單,並更新使用者訂單狀態。
Order::updateStatus($orderRelations, $recipient);
// 付款完成,寄mail通知使用者
Helpers::mailWhenPaid($PayPal, $orderRelations);
}
}
} elseif ($enable_sandbox)
{
if ($_POST["test_ipn"] != 1)
{
$paypal_ipn_status = "RECEIVED FROM LIVE WHILE SANDBOXED";
}
} elseif ($_POST["test_ipn"] == 1)
{
$paypal_ipn_status = "RECEIVED FROM SANDBOX WHILE LIVE";
}
if ($save_log_file)
{
// Create log file directory
if (!is_dir($dated_log_file_dir))
{
if (!file_exists($dated_log_file_dir))
{
mkdir($dated_log_file_dir, 0777, true);
if (!is_dir($dated_log_file_dir))
{
$save_log_file = false;
}
} else
{
$save_log_file = false;
}
}
// Restrict web access to files in the log file directory
$htaccess_body = "RewriteEngine On" . "\r\n" . "RewriteRule .* - [L,R=404]";
if ($save_log_file && (!is_file($log_file_dir . "/.htaccess") || file_get_contents($log_file_dir . "/.htaccess") !== $htaccess_body))
{
if (!is_dir($log_file_dir . "/.htaccess"))
{
file_put_contents($log_file_dir . "/.htaccess", $htaccess_body);
if (!is_file($log_file_dir . "/.htaccess") || file_get_contents($log_file_dir . "/.htaccess") !== $htaccess_body)
{
$save_log_file = false;
}
} else
{
$save_log_file = false;
}
}
if ($save_log_file)
{
// Save data to text file
file_put_contents($dated_log_file_dir . "/" . $test_text . "paypal_ipn_" . $date . ".txt", "paypal_ipn_status = " . $paypal_ipn_status . "\r\n" . "paypal_ipn_date = " . $timestamp . "\r\n" . $data_text . "\r\n", FILE_APPEND);
}
}
if ($send_confirmation_email)
{
// Send confirmation email
mail($confirmation_email_address, $test_text . "PayPal IPN : " . $paypal_ipn_status, "paypal_ipn_status = " . $paypal_ipn_status . "\r\n" . "paypal_ipn_date = " . $timestamp . "\r\n" . $data_text, "From: " . $from_email_address);
}
// Reply with an empty 200 response to indicate to paypal the IPN was received correctly
header("HTTP/1.1 200 OK");
}
結語
以上大概就是整個利用 PayPal 付款標準版 (Payment Standard) 來付款 ,然後經由 IPN 驗證的流程,
流程大概如下:
- 買家完成付款
- PayPal 發送 IPN Message 到 Listener
- Listener 回饋 HTTP 200 Response 到 PayPal
- Listener 將剛剛收到的 IPN Message 原封不動的回傳到 PayPal
- PayPal 驗證無誤後,回傳 Verified ,若驗證失敗,回傳 Invalid
留言