2021年1月31日 星期日

簡易使用 Paypal API 建立收付

有別於綠界支付的收款流程,Paypal 相對對於開發者與使用者來說都很友善,但是機制也與綠界有很多的不同,以此篇記錄 2021 進行串接的流程。


建立訂單收付流程


整體步驟是:

  1. 建立開發者帳號,註冊 My Apps & Credentials 取得 ClientID, Secret


  2.  使用第三方或自己建造 Access Token
  3.  建立所有需要的參數,送出請求得到允許結帳的 URL ,測試帳號在 Sandbox > Account (點進去帳號詳細可以改密碼),也可以用 CreditCard Generator。


  4. 使用者接受 Continue 後,會用 GET 跳轉到一個 Returtn URL,此時 Server 可以調用 Capture Order 完成付款。
  5. 使用者付款後, Paypal 會幕後通知你的伺服器,稱呼是 IPN,測試上可以用 IPN Simulator。



  6.   正式要設定 IPN 通知的 Server URL 不是在 Developer,是在 Paypal Business Account 去設定,詳情請參考: [連結]
  7.  收到 IPN 回傳後,請把資料送回去 Paypal 再次檢查驗證資料正確性: [連結] ,使用 POST 回傳驗證後,會收到 INVALID 或是 VERIFIED 這兩種 Body 代表正常或不正常。


*測試帳號的 Return 回傳設定,並不是藏在 Application 裡面。


根據這篇 [文章] 指出,你應該現在 Developer 後台的 SANDBOX -> Accounts 拿到 Business 的帳號密碼,然後在這個網站登入: https://www.sandbox.paypal.com/

在後台搜尋 IPN,設定 Instant Payment Notification (IPN) 的網址。


注意要先進去 Account Settings 搜尋 IPN,按下 Update 就可以找到設定 IPN 網址的欄位。



文件資料


資訊可能會隨著時間變化而失效,整體而言是以 Accept Payment 和 Create Order 下去看,會找出建立訂單作收付的方法。

接收付款 Accept Payments [連結]

跳轉到收付頁面的做法

測試指南 [連結]
測試支付方法 (含產生測試信用卡號碼) [連結]

設定基本的支付方法 [連結]
建立 Order 的參數及請求回傳範例說明 [連結]


Server Side - Create Order 流程


使用 Server Side 建立 Order,並不是只要呼叫 Create Order 得到網址就可以給使用者付款,你所得到的 URL 只是讓使用者接受你的訂單,接受完之後,你才可以從 Server Side 執行付款。

總結流程是: 

  1. 發送建立訂單請求
     * 範例 Code 中 "接受完成跳轉的網址" 請填上 2. 的 GET 網址,因為使用者 approve 完會跳轉到這個網址,接收後才可以用 Server Side 請款。
  2. 伺服器端請款
  3. 接收 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]4.0.0#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


沒有留言:

張貼留言

© Mac Taylor, 歡迎自由轉貼。
Background Email Pattern by Toby Elliott
Since 2014