PayPal checkout 金流串接實現自動履行訂單

最近幫專題增加金流功能,使用了PayPal提供的方案,順便做點心得紀錄

PayPal提供了多種API供選擇,可惜文件寫得有點分散且不夠詳盡,讓第一次接觸金流API這部分的我吃了不少苦頭

欲實現目的
使用者在網站付款後自動實現充值

環境與方案
前端付款方案:PayPal Checkout (https://developer.paypal.com/docs/checkout/)
收款通知方案:Instant Payment Notificatio(IPN)(https://developer.paypal.com/docs/classic/products/instant-payment-notification/)
前端:ReactJS
後端:Laravel

這邊不得不提一下PayPal提供了多種的前端付款方案除了本文使用的PayPal Checkout還有Html form方案目的都是幫助開法者快速建立付款按鈕,似乎還有單純使用API建立帳單的方式,看文件的時候沒注意真的會很容易被搞混...

在收到款項通知部分本以為可以有API去打,查閱開發文件後似乎是沒有這種選項,必須自己在伺服器建立IPN聆聽器來接收付款通知

流程邏輯
使用者發起付款(包含一組識別用invoice)->在自己伺服器建立紀錄
->在paypal建立帳單->引導至paypal付款->paypal將付款結果通知IPN
->將付款通知中的invoice與自己記錄的invoce進行match並且更新資料庫
->完成

建立付款按鈕
開始首要建立付款按鈕,在React部分我參考了這篇,若非使用React的則可參考官方文件,整體來說差異不大

而checkout button所需的token則可以在https://developer.paypal.com/找到
※DASHBOARD>My Apps & Credentials>REST API apps>Create App或是點擊已存在App

在React建立好的checkout button大概長這樣
-
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import scriptLoader from 'react-async-script-loader';
class PayPalBtn extends Component {
constructor(props) {
super(props);
this.state = {
showButton: false
}
window.React = React;
window.ReactDOM = ReactDOM;
}
componentDidMount() {
const {
isScriptLoaded,
isScriptLoadSucceed
} = this.props;
if (isScriptLoaded && isScriptLoadSucceed) {
this.setState({ showButton: true });
}
}
componentWillReceiveProps(nextProps) {
const {
isScriptLoaded,
isScriptLoadSucceed,
} = nextProps;
const isLoadedButWasntLoadedBefore =
!this.state.showButton &&
!this.props.isScriptLoaded &&
isScriptLoaded;
if (isLoadedButWasntLoadedBefore) {
if (isScriptLoadSucceed) {
this.setState({ showButton: true });
}
}
}
render() {
const {
total,
currency,
env,
commit,
client,
onSuccess,
onError,
onCancel,
invoice
} = this.props;
const {
showButton,
} = this.state;
const payment = () =>{
//create payment here
//http post to my api with unique "invoice"
//...
//make a new payment
return paypal.rest.payment.create(env, client, {
transactions: [
{
amount: {
total,
currency,
},
invoice_number:invoice, //we can track tranction with invoice
},
],
});
}
const onAuthorize = (data, actions) =>{
console.log('data',data);
actions.payment.execute()
.then(() => {
const onSuccessPayment = {
paid: true,
cancelled: false,
payerID: data.payerID,
paymentID: data.paymentID,
paymentToken: data.paymentToken,
returnUrl: data.returnUrl,
};
onSuccess(onSuccessPayment);
});
}
return (
<div>
{showButton && <paypal.Button.react
env={env}
client={client}
commit={commit}
payment={payment}
onAuthorize={onAuthorize}
onCancel={onCancel}
onError={onError}
/>}
</div>
);
}
}
export default connect()(scriptLoader('https://www.paypalobjects.com/api/checkout.js')(PayPalBtn));
-

元件使用。此例中使用亂數字串當作發票號碼
-
import React, { Component } from 'react';
import './index.css';
import vip from 'AppSrc/img/vip.png'
import PropTypes from 'prop-types';
import PaypalButton from './PaypalBtn.jsx';
const CLIENT = {
sandbox: 'xxx',
production: 'xxx',
};
const ENV = process.env.NODE_ENV === 'production'
? 'production'
: 'sandbox';
class Index extends Component {
constructor(props) {
super(props);
this.state = {}
this.makeid = this.makeid.bind(this)
}
makeid() {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < 50; i++){
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
render() {
const onSuccess = (payment) =>
console.log('Successful payment!', payment);
const onError = (error) =>
console.log('Erroneous payment OR failed to load script!', error);
const onCancel = (data) =>
console.log('Cancelled payment!', data);
let invoice1 = this.makeid();
return (
<div>
<br />
<div className="row">
<div className="col-md-4 col-12">
<div className="card">
<div className="card-header text-center">
TWD $50
</div>
<div className="text-center">
<img src={vip} className="card-img-top w-75" alt="" />
<br />
<br />
<h5 className="card-title">額外旅遊規劃次數(3次)</h5>
</div>
<div className="card-body text-center">購買額外的次數來使用旅遊規劃服務</div>
<div className="card-footer text-muted text-center">
<PaypalButton
client={CLIENT}
env={ENV}
commit={true}
currency={'TWD'}
total={50}
onSuccess={onSuccess}
onError={onError}
onCancel={onCancel}
invoice={invoice1}
/>
</div>
</div>
</div>
</div>
</div>
);
}
}
Index.propTypes = {
dispatch: PropTypes.func
}
export default (Index);
-

訂單追蹤
接著看回React checkout button範例中的line:62,我在這邊示意你要在發起paypal訂單前紀錄自記產生的發票號碼(invoice)與購買資訊,當然也必須將這組發票號碼交由paypal(line:74),

我這邊使用較為簡單的做法(但是可能會產生竄改風險),直接在前端call paypal create payment api,由於我的專題僅賣一種商品,所以我只需知道有帳單進來,並且更新資料庫即可,若有多種商品與數量則建議參考官方文件在後端執行這個動作

建立IPN
官方有提供範例程式

自己在測試的時候曾經嘗試整合在laravel中,但是在IPN simulator中會發生錯誤,我沒找到解決方法,但是估計是laravel中間處理了太多的東西導致資訊變動所以失敗,最我將sample code放置在public資料夾下成功收到IPN訊息,在以call api方式更新資料庫

由於IPN程式無法藉由頁面debug,所以必須察看或是自己寫log
linux & apache下 PHP的錯誤會被記錄在/var/log/apache2

然後我們可以將收到的IPN訊息寫入log,linux下記得將讀寫權限開放chmod -R 777 your_directory_name

修改後的ipn(PHP)
-
<?php namespace Listener;
use PaypalIPN;
require('PaypalIPN.php');
function write_log($str,$status,$data_array) //傳入資料夾名 想寫近的狀態 資料
{
$textname = $str.date("Ymd").".txt"; //檔名 filename
$URL = "./log/".$str."/"; //路徑 Path
if(!is_dir($URL)) // 路徑中的$str 資料夾是否存在 Folder exists in the path
mkdir($URL,0700);
$URL .= $textname; //完整路徑與檔名 The full path and filename
$time = $str.$status.":".date("H:i:s"); //時間 Time
$writ_tmp = '';
foreach ($data_array as $key => $value) //將陣列資料讀出 To read array data
{
$writ_tmp .= ",".$key."=".$value;
}
$write_data = $time.$writ_tmp."\n";
$fileopen = fopen($URL, "a+");
fseek($fileopen, 0);
fwrite($fileopen,$write_data); //寫資料進去 write data
fclose($fileopen);
}
$ipn = new PaypalIPN();
// Use the sandbox endpoint during testing.
$ipn->usePHPCerts();
$ipn->useSandbox();
$verified = $ipn->verifyIPN();
if ($verified) {
/*
* Process IPN
* A list of variables is available here:
* https://developer.paypal.com/webapps/developer/docs/classic/ipn/integration-guide/IPNandPDTVariables/
*/
write_log('payapl','test',[
'item_name'=>$_POST["item_name"],
'payment_status'=>$_POST['payment_status'],
'pending_reason'=>$_POST['pending_reason'],
'invoice'=>$_POST['invoice'],
'txn_id'=>$_POST['txn_id'],
]);
if($_POST['payment_status']=='Completed'){
//call api 履行訂單
}
}
// Reply with an empty 200 response to indicate to paypal the IPN was received correctly.
header("HTTP/1.1 200 OK");
view raw myIPN.php hosted with ❤ by GitHub
-
注意line:30 有時候ipn發生錯誤是沒辦法使用正確的驗證檔案,我在這邊調用了usePHPCerts()解決這個問題(官方範例預設未使用)

更多變數參考https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNandPDTVariables/

之後若有通知進來則會寫入log/paypal下

IPN付款通知
要打開付款通知需要先前往https://www.sandbox.paypal.com或是https://www.paypal.com
設定>我的銷售工具>即時付款通知>你的IPN網址>Turn on IPN

收尾
最後可以使用payapl官方提供的IPN simulator進行測試(需登入),然後在收到IPN訊息且狀態為成功的狀況下比對invoice(自訂的)來履行訂單

留言

這個網誌中的熱門文章

[Arduino]電子秤平 重量感測條+HX711AD模組

cpe練習筆記 UVa401 Palindromes

[PHP,MySQL]圖片上傳和讀取 使用base64_encode & base64_decode