PayPal REST API 串接金流好簡單

前言

本篇將會分享,如何使用 PayPal REST API,來做到以下的動作:

  1. 建立授權訂單
  2. 授權
  3. 請款
  4. 退款
  5. 部分款項凍結

本篇屬於個人學習筆記,所以可能會參雜一些個人的專案內容,請選擇性參考。

安裝 PayPal REST API 官方 SDK

本篇使用的為目前 PayPal 最新釋出的 SDK 版本

安裝

composer require paypal/paypal-checkout-sdk

設定

個人設定

  • 安裝完成之後,可在 SDK 的資料夾底下,找到範例,如下圖:

  • 設定 PayPalClient.php 檔案,如下: 1. 申請開發者帳號並登入 2. 建立一個App 3. 取得 Client ID 以及 Secret
    4. 將取得的 Client ID 以及 Secret 填入, Ray 是設在環境變數

public static function environment()
{
$clientId = env('PAYPAL_SANDBOX_API_ClientID');
$clientSecret = env('PAYPAL_SANDBOX_API_SECRET');
return new SandboxEnvironment($clientId, $clientSecret);
}

開始

在上面提到的範例資料夾中,可以找到幾乎所有會用到的範例。這邊可以根據每個人的需求不同來客制,以下是 Ray 自己的版本
任何疑惑,請參考 sample 裡頭的範例,以及官方文件如下:
order
payment

order 跟 payment 的差異

主要的差異如下:

  • order: 只支援 PayPal 的會員。可以延後付款,並且視乎貨物的狀態做部分的請款
  • payment: 可以延後付款,但不可以分批請款。

詳細的介紹可以參考原文解說

建立訂單 ( order )

public function createOrder($toBeSavedInfo, Recipient $recipient, $debug = false)
{
// 引用SDK
$request = new OrdersCreateRequest();
$request->headers["prefer"] = "return=representation";

// 這邊的RequestBody等等會貼在下面
$request->body = self::buildRequestBody($toBeSavedInfo, $recipient);

// 這邊引用剛剛設定好的 PayPalClient
$client = PayPalClient::client();
$response = $client->execute($request);
if ($debug)
{
print "Status Code: {$response->statusCode}\n";
print "Status: {$response->result->status}\n";
print "Order ID: {$response->result->id}\n";
print "Intent: {$response->result->intent}\n";
print "Links:\n";
foreach ($response->result->links as $link)
{
print "\t{$link->rel}: {$link->href}\tCall Type: {$link->method}\n";
}
// To toggle printing the whole response body comment/uncomment below line
echo json_encode($response->result, JSON_PRETTY_PRINT), "\n";
}

// 建立建立完成後,我只取讓使用者用來確認的連結,預設 PayPal 提供了很多的連結,但是其他的我們都可以靠 API 來達成。
foreach (($response->result->links) as $link)
{
if ($link->rel === 'approve')
{
$linkForApproval = $link->href;
break;
}
}

// 這邊取得建立訂單之後的一些會用到的資訊,然後 return
$toBeSavedInfo['payment_id'] = $response->result->id;
$toBeSavedInfo['statusCode'] = $response->statusCode;
$toBeSavedInfo['custom_id'] = $response->result->purchase_units[0]->custom_id;
$toBeSavedInfo['PayPal_total_amount'] = $response->result->purchase_units[0]->amount->value;
$toBeSavedInfo['orderStatus'] = $response->result->status;
$toBeSavedInfo['linkForApproval'] = $linkForApproval;

return $toBeSavedInfo;
}

下面是建立訂單功能會用到的 RequestBody

public static function buildRequestBody($toBeSavedInfo, Recipient $recipient)
{
// 這邊的設定,使得我們可以在 PayPal 的付款頁面,看到多個商品的明細
$item = [];
$i = 1;
foreach ($toBeSavedInfo['orders'] as $order)
{
$item[] = [
'name' => $order->item_name,
'description' => $order->item_description,
'sku' => $i,
'unit_amount' => [
'currency_code' => $toBeSavedInfo['mc_currency'],
'value' => $order->unit_price,
],
'quantity' => $order->quantity,
];
$i ++;
}

// 這邊我們指定 intent ,我設在環境變數,
return [
'intent' => env('PAYPAL_SANDBOX_INTENT_OF_CREATED_ORDERS'),
'application_context' =>
[
'return_url' => env('PAYPAL_SANDBOX_RETURN_URL'),
'cancel_url' => env('PAYPAL_SANDBOX_CANCEL_URL'),
'brand_name' => env('APP_NAME'),
'locale' => env('PAYPAL_SANDBOX_LOCALE'),
'landing_page' => env('PAYPAL_SANDBOX_LANDING_PAGE'),
'shipping_preferences' => env('PAYPAL_SANDBOX_SHIPPING_PREFERENCES'),
'user_action' => env('PAYPAL_SANDBOX_USER_ACTION'),
],
// 這邊可以設定 purchase_unit ,一個 purchase_unit 裡面可以設定税、運費、等等,這邊省略
'purchase_units' =>
[
[
'custom_id' => $toBeSavedInfo['merchant_trade_no'],
'amount' =>
[
'currency_code' => $toBeSavedInfo['mc_currency'],
'value' => $toBeSavedInfo['total_amount'],
'breakdown' =>
[
'item_total' =>
[
'currency_code' => $toBeSavedInfo['mc_currency'],
'value' => $toBeSavedInfo['total_amount'],
],
],
],

'items' => $item,
// 這邊可以指定收件人
'shipping' =>
array(
'name' =>
array(
'full_name' => $recipient->name,
),
'address' =>
array(
'address_line_1' => $recipient->others,
'admin_area_2' => $recipient->district,
'admin_area_1' => $recipient->city,
'postal_code' => $recipient->postcode,
'country_code' => $recipient->country_code,
),
),
],
],
];
}

授權 ( authorization )

接下來,我們要使用 REST API 中的特別功能, Authorization 。

  • 授權之後,我們有29天的時間可以使用 capture 來從使用者的帳戶裡面扣錢。 不過呢,雖然有效期限是29天,但是 PayPal 只能保證給予單次授權開始計算的三天內,使用者的帳戶裡頭會有足夠的金額。
  • 意思就是說呢,在 authorization 開始計算的三天, PayPal 會暫時性的在付款方的 PayPal 帳戶中,凍結申請的款項,記住只有三天哦! 這三天稱為 honor period 。
  • 在首次的 authorization 之後,我們可以申請多次,最多10次的 authorization ,稱為 reauthorize 。
    如果你覺得這樣次數還是太少,可以通過跟 PayPal 客服聯絡的方式,將次數提升到最多99次!
  • 其實只要將時間算好,10次的授權應該很夠用了。平均三天授權一次,10次也一個月了,海運都到了!
  • 授權可以更改金額,最高可以授權115% 或 不超過 75 USD 的金額,如果運費或者稅務方面,或是其他原因造成費用有些許變動的話,可以透過重新授權來更改費用。
  • 細節可以參考官方文件

授權的範例如下:

記住這是 Ray 自己的版本,大家可以參考官方的範例,再依照自己的需求作更改,或者乾脆取SDK裡面的功能自己寫一個! 這才是我認為的最佳解!

/**
* This function can be used to perform authorization on the approved order.
* Valid Approved order id should be passed as an argument.
*/
// 這邊我們可以變更授權的金額,根據你的需求
public static function authorizeOrder($orderId, $amount = null, $debug = false)
{
$request = new OrdersAuthorizeRequest($orderId);
// RequestBody 跟上面提到的差不多,可以參考官方的範例!
$request->body = self::buildRequestBodyForAuthorizeOrder($amount);

$client = PayPalClient::client();
$response = $client->execute($request);
if ($debug)
{
print "Status Code: {$response->statusCode}\n";
print "Status: {$response->result->status}\n";
print "Order ID: {$response->result->id}\n";
print "Authorization ID: {$response->result->purchase_units[0]->payments->authorizations[0]->id}\n";
print "Links:\n";
foreach ($response->result->links as $link)
{
print "\t{$link->rel}: {$link->href}\tCall Type: {$link->method}\n";
}
print "Authorization Links:\n";
foreach ($response->result->purchase_units[0]->payments->authorizations[0]->links as $link)
{
print "\t{$link->rel}: {$link->href}\tCall Type: {$link->method}\n";
}
// To toggle printing the whole response body comment/uncomment below line
echo json_encode($response->result, JSON_PRETTY_PRINT), "\n";
}

return $response;
}

授權之後呢,我們需要驗證授權是否成功,所以我利用回傳的response,寫一個驗證的 function,如下:

public static function checkIfAuthorizedSuccessfully($response)
{
$newPayPal = (new NewPayPal())->where('payment_id', request()->token)->first();
// 確認授權是否完成
if (($response->result->status) !== 'COMPLETED')
return 'Authorization isn\'t completed';

// 確認授權是否已開始
if (($response->result->purchase_units[0]->payments->authorizations[0]->status) !== 'CREATED')
return 'Authorization was not created';

// 確認幣別是否一致
if (($response->result->purchase_units[0]->payments->authorizations[0]->amount->currency_code) !== ($newPayPal->mc_currency))
return 'The currency is mismatched';

// 確認授權金額是否正確。這邊是我自己的版本,有需要變動授權金額的話,這邊可以變一下。
if (intval($response->result->purchase_units[0]->payments->authorizations[0]->amount->value) !== ($newPayPal->total_amount))
return 'The total amount is not correct';
}

提款 ( capture )

  • 就像上面提到的,成功授權之後,我們可以在29天內隨時向買家請款。
  • 不過 PayPal 只有保證三天買家帳戶裡頭會有足夠的金額,又稱 honor period
  • 一般來說,我們只要把時間算好,可以透過 授權 以及 再次授權 ,將 這筆金額臨時凍結30天。

以下是提款 ( capture ) 的範例:

public static function captureAuthorization(NewPayPal $newPayPal, $final_capture = false, $debug = false)
{
$NewPayPal = (new NewPayPal)->where('merchant_trade_no', $newPayPal->merchant_trade_no)->first();
// 提款功能,需要帶入授權id
$request = new AuthorizationsCaptureRequest($newPayPal->authorization_id);
// 這邊帶入要提款的金額,如上所敘,提款金額是可以分批次的! Final_capture 如果設定為true的話,會結束此次授權,此次授權之後無法再進行提款,若要提款需要再重新授權。
$request->body = self::buildRequestBodyForCaptureAuthorization($NewPayPal->to_be_captured_amount, $final_capture, $newPayPal->mc_currency);
$client = PayPalClient::client();
$response = $client->execute($request);

if ($debug)
{
print "Status Code: {$response->statusCode}\n";
print "Status: {$response->result->status}\n";
print "Capture ID: {$response->result->id}\n";
print "Links:\n";
foreach ($response->result->links as $link)
{
print "\t{$link->rel}: {$link->href}\tCall Type: {$link->method}\n";
}
// To toggle printing the whole response body comment/uncomment below line
echo json_encode($response->result, JSON_PRETTY_PRINT), "\n";
}
return $response;
}

以下是提款 ( capture ) 的 RequestBody

public static function buildRequestBodyForCaptureAuthorization($amount = null, $final_capture = false, $currency = 'USD')
{
if ($amount != null)
{
// 指定提款的金額與幣別,需要跟授權的一致
return [
"amount" => [
'currency_code' => $currency,
'value' => $amount,
],
'final_capture' => $final_capture
];
}

return "{}";
}

以下是提款的邏輯

Ray 的邏輯是設定一個提款期限來決定什麼時候提款,因為每次退款都會扣掉手續費,所以 Ray 的想法是利用金額凍結取代退款,在提款期限之前,如果買家申請退款的話, Ray 這邊只需要去更改要提款的最終數字,這樣就可以避免掉手續費的部分。 Ray 自己設定的容許退款期限為七天,所以 Ray 會將這筆金額凍結七天,在七天後等到一切都確定了再依照最終的提款金額一次提款。
所以下面的function每天都會跑一次,如果已經過了提款期限就會真正執行提款,並更新該訂單在所有資料庫裡頭相對應的狀態。

public static function dailyCaptureAuthorization()
{
$toBeCapturedPayments = NewPayPal::whereNotNull('authorization_id')->whereNull('capture_id')->where('to_be_captured_date', '<', Carbon::now()->toDateTimeString())->get();
foreach ($toBeCapturedPayments as $toBeCapturedPayment)
{
$response = NewPayPal::captureAuthorization($toBeCapturedPayment);
if (($response->result->status) === 'COMPLETED')
{
$toBeCapturedPayment->update(['capture_id' => $response->result->id, 'status' => 7]);
foreach ($toBeCapturedPayment->orderRelations as $orderRelation)
{
if (($orderRelation->status == 5) || ($orderRelation->status == 6))
{
$orderRelation->order->update(['status' => 7]);
$orderRelation->update(['status' => 7]);
}
}
}
}
}

退款 ( refund )

  • 退款的規則是,可以針對特定授權,進行一次性或者分批次的退款。
  • 若屬於分批次,可以指定退款金額
  • 若想要一次性,可以將整個 RequestBody 留空,像官方範例那樣
    public static function refundOrder($captureId, $amount, $currency, $debug = false)
    {
    $request = new CapturesRefundRequest($captureId);
    // 這邊帶入指定的退款金額以及幣別,幣別必須要跟授權的一樣哦
    $request->body = self::buildRequestBodyForRefundOrder($amount, $currency);
    $client = PayPalClient::client();
    $response = $client->execute($request);

    if ($debug)
    {
    print "Status Code: {$response->statusCode}\n";
    print "Status: {$response->result->status}\n";
    print "Order ID: {$response->result->id}\n";
    print "Links:\n";
    foreach ($response->result->links as $link)
    {
    print "\t{$link->rel}: {$link->href}\tCall Type: {$link->method}\n";
    }
    // To toggle printing the whole response body comment/uncomment below line
    echo json_encode($response->result, JSON_PRETTY_PRINT), "\n";
    }

    return $response;
    }

以下是退款的 RequestBody

public static function buildRequestBodyForRefundOrder($amount = null, $currency = 'USD', $final_capture = false)
{
// 若金額有指定就為指定值,若無指定便為預設格式
if ($amount != null)
{
return [
"amount" => [
'currency_code' => $currency,
'value' => $amount,
],
'final_capture' => $final_capture
];
}

return "{}";
}

退款的邏輯

  • 相對應提款時所做的操作,在商家真正對買家做出提款之前,買家所申請的退款請求都只是去更改資料庫的數字。
  • 如果過了七天,但實屬特殊案例,買家也還是可以申請退款,不過到時候就會有手續費產生
  • 以上邏輯只適用於 PayPal ,因為本專案整合兩家金流,所以上面的邏輯並不適用於歐付寶,不過總體來說,對買家來說都是沒有影響的。
    public static function refund(Order $order, NewPayPal $paymentServiceInstance, OrderRelations $orderRelation)
    {
    // 當該申請訂單為已授權,但尚未請款
    if (($paymentServiceInstance->capture_id === null) && ($paymentServiceInstance->authorization_id !== null))
    {
    // 如上所敘,我們只更新資料庫的請款金額
    $paymentServiceInstance->update([
    'to_be_captured_amount' => $paymentServiceInstance->to_be_captured_amount - $order->total_amount,
    'total_amount' => $paymentServiceInstance->total_amount - $order->total_amount
    ]);
    $order->update(['status' => 4]);
    $orderRelation->update(['status' => 4]);
    }

    // 當該訂單已經請款了
    if ($paymentServiceInstance->capture_id !== null)
    {
    // 真正執行退款 API ,將款項退給買家
    $response = self::refundOrder($paymentServiceInstance->capture_id, $order->total_amount, $paymentServiceInstance->mc_currency);
    // 如果退款確定成功,更新訂單狀態
    if ($response->result->status == 'COMPLETED')
    {
    $order->update(['status' => 4]);
    $orderRelation->update(['status' => 4]);
    $paymentServiceInstance->update([
    'total_amount' => $paymentServiceInstance->total_amount - $order->total_amount
    ]);
    }
    }
    }

取消授權

取消方法非常簡單,只要使用官方範例,並且依照格式帶入授權的id就可以,這邊就不特別舉例!
授權id在你成功授權的時候會回傳,在那時記得把它存起來!

取得授權資料

取得授權方法非常簡單,只要使用官方範例,並且依照格式帶入授權的id就可以,這邊就不特別舉例!
授權id在你成功授權的時候會回傳,在那時記得把它存起來!

取得提款資料

取得提款方法非常簡單,只要使用官方範例,並且依照格式帶入提款的id就可以,這邊就不特別舉例!
提款id在你成功提款的時候會回傳,在那時記得把它存起來!

總結

依照官方文件, PayPal REST API 是可以搭配 JavaScript 的 Smart Button 一起使用的,不過 Ray 負責的是後端的角色,所以這一部分就沒有深究。
看起來還蠻有趣的!有興趣的可以花時間研究一下!
PayPal 不愧是國際的金流系統,各項的支援都十分全面以及功能也十分多樣,可惜已經退出台灣了!不過據了解應該是因為相關法令的關係,退出在另一個角度來說也是在捍衛台灣的稅法,未嘗不是一件好事,這邊就不多做評論。
這陣子算是針對 PayPal 的金流深入的研究了一下,當然還有許多比較細緻的功能因為時間的關係還沒有去接觸到,待之後有時間有機會再來好好研究,再把心得過程都記錄下來分享給大家!

歡迎轉載,但麻煩請註明出處,感謝!

伸縮自如的 git flow 利用 PayPal 付款標準版 (PayPal Payment Standard) 以及 PayPal 即時付款通知 (PayPal IPN) 方式結帳付款

留言

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×