有別於綠界支付的收款流程,Paypal 相對對於開發者與使用者來說都很友善,但是機制也與綠界有很多的不同,以此篇記錄 2021 進行串接的流程。
建立訂單收付流程
整體步驟是:
- 建立開發者帳號,註冊 My Apps & Credentials 取得 ClientID, Secret
- 使用第三方或自己建造 Access Token
- 建立所有需要的參數,送出請求得到允許結帳的 URL ,測試帳號在 Sandbox > Account (點進去帳號詳細可以改密碼),也可以用 CreditCard Generator。
- 使用者接受 Continue 後,會用 GET 跳轉到一個 Returtn URL,此時 Server 可以調用 Capture Order 完成付款。
- 使用者付款後, Paypal 會幕後通知你的伺服器,稱呼是 IPN,測試上可以用 IPN Simulator。
- 正式要設定 IPN 通知的 Server URL 不是在 Developer,是在 Paypal Business Account 去設定,詳情請參考: [連結]
- 收到 IPN 回傳後,請把資料送回去 Paypal 再次檢查驗證資料正確性: [連結] ,使用 POST 回傳驗證後,會收到 INVALID 或是 VERIFIED 這兩種 Body 代表正常或不正常。
*測試帳號的 Return 回傳設定,並不是藏在 Application 裡面。
在後台搜尋 IPN,設定 Instant Payment Notification (IPN) 的網址。
注意要先進去 Account Settings 搜尋 IPN,按下 Update 就可以找到設定 IPN 網址的欄位。
文件資料
資訊可能會隨著時間變化而失效,整體而言是以 Accept Payment 和 Create Order 下去看,會找出建立訂單作收付的方法。
接收付款 Accept Payments [連結]
跳轉到收付頁面的做法
- 使用前端 Calls [連結]
- 使用後端 Calls : [建立 Access Token] 要有 Token 才能呼叫 API (沒有用 Plugin 就需要自己生) | GoDoc | HTML Form
- IPN 付款結果通知說明 [連結]
測試指南 [連結]
測試支付方法 (含產生測試信用卡號碼) [連結]
設定基本的支付方法 [連結]
建立 Order 的參數及請求回傳範例說明 [連結]
Server Side - Create Order 流程
使用 Server Side 建立 Order,並不是只要呼叫 Create Order 得到網址就可以給使用者付款,你所得到的 URL 只是讓使用者接受你的訂單,接受完之後,你才可以從 Server Side 執行付款。
總結流程是:
- 發送建立訂單請求
* 範例 Code 中 "接受完成跳轉的網址" 請填上 2. 的 GET 網址,因為使用者 approve 完會跳轉到這個網址,接收後才可以用 Server Side 請款。 - 伺服器端請款
- 接收 IPN 驗證
範例 Code - 1. 發送建立訂單請求
func PaypalPaymentHTML(product_name string, amount string, trade_description string, choosePayment string, trade_no string) string {
// Create a client instance
paypalClient, err := paypal.NewClient("CLIENT_ID", "SECRET", paypal.APIBaseSandBox)
if err != nil {
fmt.Println("DEAD WITH ERROR 1")
return "ERROR 1"
}
// Retrieve access token
_, err = paypalClient.GetAccessToken(context.Background())
if err != nil {
fmt.Println("DEAD WITH ERROR 2")
return "ERROR 2"
}
// Purchase Information
purchase := []paypal.PurchaseUnitRequest{
paypal.PurchaseUnitRequest{
Amount: &paypal.PurchaseUnitAmount{
Currency: "TWD",
Value: amount,
},
CustomID: trade_no,
},
}
orderPayer := &paypal.CreateOrderPayer{}
appContext := &paypal.ApplicationContext{
BrandName: "公司名稱",
Locale: "zh-TW",
ShippingPreference: paypal.ShippingPreferenceNoShipping,
UserAction: paypal.UserActionContinue,
ReturnURL: "接受完成跳轉的網址 (填上前往 /paypalApproval Router 的 full url)",
CancelURL: "取消跳轉回去的網址",
}
order, err := paypalClient.CreateOrder(context.Background(), "CAPTURE", purchase, orderPayer, appContext) //.CreatePayout(context.Background(), payout)
if err != nil {
fmt.Println("DEAD WITH ERROR 3")
return "ERROR 3"
}
// 回傳眾多的 URL 中,找到 Approval URL ,讓使用者接受你的要求付款請求。
var trueLink string = "None"
for _, v := range order.Links {
if v.Method == "GET" && v.Rel == "approve" {
trueLink = v.Href
}
}
// 取得網址 Token Query String, 製作到表單中
u, _ := url.Parse(trueLink)
m, _ := url.ParseQuery(u.RawQuery)
token := m["token"][0]
return `<form name="myform" method="get" action="` + trueLink + `"><input type="text" name="token" value="` + token + `"/><input type="submit" value="submit"/></form>`
}
範例 Code - 2. 伺服器端請款
// 使用 Golang - Gin 作為 Server
//token=6UP57614AS899474A&PayerID=F5R7FFUHN64PA
r.GET("/paypalApproval", func(c *gin.Context) {
// Create a client instance
paypalClient, err := paypal.NewClient(config.Paypal.ClientID, config.Paypal.Secret, paypal.APIBaseSandBox)
if err != nil {
fmt.Println("DEAD WITH ERROR 1")
c.String(200, "ERROR 1")
return
}
// Retrieve access token
_, err = paypalClient.GetAccessToken(context.Background())
if err != nil {
fmt.Println("DEAD WITH ERROR 2")
c.String(200, "ERROR 2")
return
}
// token 在這裡是訂單編號 ID 的意思
token, _ := c.GetQuery("token")
// 對訂單做請款
paypalClient.CaptureOrder(c, token, paypal.CaptureOrderRequest{})
/*
檢查訂單狀態:
1. 使用者接受訂單 &{0E555560CU5359715 APPROVED CAPTURE 0xc00008d400 [{default 0xc0002459b0 }] [{https://api.sandbox.paypal.com/v2/checkout/orders/0E555560CU5359715 self GET }
2. 完成請款 &{5AG9353242852454J COMPLETED CAPTURE 0xc0003b6960 [{default 0xc000198720 0xc00000f100}] [{https://api.sandbox.paypal.com/v2/checkout/orders/5AG9353242852454J self GET }]
3. ....
o, _ := paypalClient.GetOrder(context.Background(), token)
fmt.Println(o)
*/
c.String(200, "OK")
})
範例 Code - 接收 IPN 資料後驗證
func PaypalPaymentHOOK(c *gin.Context) {
c.String(200, "")
// Paypal Server Request 呼叫的資料
ipnBody, _ := ioutil.ReadAll(c.Request.Body)
// 根據 Paypal 的要求對伺服器做驗證檢查
/*
Send response messages back to PayPal:
https://ipnpb.sandbox.paypal.com/cgi-bin/webscr (for Sandbox IPNs)
https://ipnpb.paypal.com/cgi-bin/webscr (for live IPNs)
*/
_, body, errs := gorequest.New().Post(config.Paypal.IPNCheckServer).
Set("User-Agent", `PHP-IPN-VerificationScript`).
Send(`cmd=_notify-validate&` + string(ipnBody)).
EndBytes()
if errs != nil {
fmt.Println("Failed to request validation.")
}
if string(body) == "VERIFIED" {
fmt.Println("GOT THE RIGHT RETURN CALLS.")
} else {
fmt.Println("THIS IS NOT A VAILD IPN RETURN CALLS.")
}
}
Reference:
https://developer.paypal.com/
https://godoc.org/github.com/logpacker/PayPal-Go-SDK
https://tn710617.github.io/zh-tw/PayPalRestAPI/#%E5%BB%BA%E7%AB%8B%E8%A8%82%E5%96%AE-order
https://thecodingday.blogspot.com/2018/11/paypal-checkout.html
https://stackoverflow.com/questions/4298117/paypal-ipn-always-return-payment-status-pending-on-sandbox
https://pkg.go.dev/github.com/plutov/paypal/[email protected]#PaymentCaptureRequest
https://www.paypal.com/apex/developer/expressCheckout/executeApprovedPayment
https://developer.paypal.com/docs/archive/checkout/how-to/server-integration/
https://developer.paypal.com/docs/api/orders/v2#orders_authorize
https://developer.paypal.com/demo/checkout/#/pattern/server
沒有留言:
張貼留言