前言 本篇將會分享,如何使用 PayPal REST API,來做到以下的動作:
建立授權訂單
授權
請款
退款
部分款項凍結
本篇屬於個人學習筆記,所以可能會參雜一些個人的專案內容,請選擇性參考。
安裝 PayPal REST API 官方 SDK 本篇使用的為目前 PayPal 最新釋出的 SDK 版本
安裝 composer require paypal/paypal-checkout-sdk
設定 個人設定
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 ) { $request = new OrdersCreateRequest (); $request ->headers["prefer" ] = "return=representation" ; $request ->body = self ::buildRequestBody ($toBeSavedInfo , $recipient ); $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" ; } echo json_encode ($response ->result, JSON_PRETTY_PRINT), "\n" ; } foreach (($response ->result->links) as $link ) { if ($link ->rel === 'approve' ) { $linkForApproval = $link ->href; break ; } } $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 ) { $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 ++; } 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_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裡面的功能自己寫一個! 這才是我認為的最佳解!
public static function authorizeOrder ($orderId , $amount = null , $debug = false ) { $request = new OrdersAuthorizeRequest ($orderId ); $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" ; } 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 (); $request = new AuthorizationsCaptureRequest ($newPayPal ->authorization_id); $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" ; } 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" ; } 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 的金流深入的研究了一下,當然還有許多比較細緻的功能因為時間的關係還沒有去接觸到,待之後有時間有機會再來好好研究,再把心得過程都記錄下來分享給大家!
歡迎轉載,但麻煩請註明出處,感謝!
留言