Skip to content

Commit

Permalink
feat(paypal): add webhook support (#434)
Browse files Browse the repository at this point in the history
* feat(paypal): add webhook support
  • Loading branch information
thahao authored Dec 11, 2024
1 parent 9940221 commit 0225abe
Show file tree
Hide file tree
Showing 4 changed files with 358 additions and 0 deletions.
9 changes: 9 additions & 0 deletions paypal/constant.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,13 @@ const (

// 物流相关
addTrackingNumber = "/v2/checkout/orders/%s/track" // order_id 授权物流信息 POST

// webhook 相关
createWebhook = "/v1/notifications/webhooks" // 创建webhook POST
listWebhook = "/v1/notifications/webhooks" // 获取webhook列表 GET
showWebhookDetail = "/v1/notifications/webhooks/%s" // webhook_id 获取webhook详情 GET
updateWebhook = "/v1/notifications/webhooks/%s" // webhook_id 更新webhook PATCH
deleteWebhook = "/v1/notifications/webhooks/%s" // webhook_id 删除webhook DELETE
verifyWebhookSignature = "/v1/notifications/verify-webhook-signature" //webhook消息验签
showWebhookEventDetail = "/v1/notifications/webhooks-events/%s" // webhook_id 获取webhook-event详情 GET
)
74 changes: 74 additions & 0 deletions paypal/model.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package paypal

import "encoding/json"

type AccessToken struct {
Scope string `json:"scope"`
AccessToken string `json:"access_token"`
Expand Down Expand Up @@ -1055,3 +1057,75 @@ type AddTrackingNumberRsp struct {
ErrorResponse *ErrorResponse `json:"-"`
Response *OrderDetail `json:"response,omitempty"`
}

type CreateWebhookRsp struct {
Code int `json:"-"`
Error string `json:"-"`
ErrorResponse *ErrorResponse `json:"-"`
Response *Webhook `json:"response,omitempty"`
}

type WebhookEventType struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Status string `json:"status,omitempty"`
ResourceVersions []string `json:"resource_versions,omitempty"`
}

type Webhook struct {
Id string `json:"id"`
Url string `json:"url"`
EventTypes []*WebhookEventType `json:"event_types"`
Links []*Link `json:"links,omitempty"`
}

type ListWebhook struct {
Webhooks []*Webhook `json:"webhooks"`
}

type ListWebhookRsp struct {
Code int `json:"-"`
Error string `json:"-"`
ErrorResponse *ErrorResponse `json:"-"`
Response *ListWebhook `json:"response,omitempty"`
}

type WebhookDetailRsp struct {
Code int `json:"-"`
Error string `json:"-"`
ErrorResponse *ErrorResponse `json:"-"`
Response *Webhook `json:"response,omitempty"`
}

type WebhookEventDetailRsp struct {
Code int `json:"-"`
Error string `json:"-"`
ErrorResponse *ErrorResponse `json:"-"`
Response json.RawMessage `json:"response,omitempty"`
}

type VerifyWebhookSignatureRequest struct {
AuthAlgo string `json:"auth_algo,omitempty"`
CertURL string `json:"cert_url,omitempty"`
TransmissionID string `json:"transmission_id,omitempty"`
TransmissionSig string `json:"transmission_sig,omitempty"`
TransmissionTime string `json:"transmission_time,omitempty"`
WebhookID string `json:"webhook_id,omitempty"`
Event json.RawMessage `json:"webhook_event,omitempty"`
}

type VerifyWebhookResponse struct {
VerificationStatus string `json:"verification_status,omitempty"`
}

type WebhookEvent struct {
Id string `json:"id"`
CreateTime string `json:"create_time"`
ResourceType string `json:"resource_type"`
EventType string `json:"event_type"`
Summary string `json:"summary"`
Resource json.RawMessage `json:"resource,omitempty"`
Links []*Link `json:"links,omitempty"`
EventVersion string `json:"event_version"`
ResourceVersion string `json:"resource_version"`
}
149 changes: 149 additions & 0 deletions paypal/webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package paypal

import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/go-pay/gopay"
"net/http"
)

// CreateWebhook 创建Webhook
func (c *Client) CreateWebhook(ctx context.Context, bm gopay.BodyMap) (ppRsp *CreateWebhookRsp, err error) {
if err = bm.CheckEmptyError("url", "event_types"); nil != err {
return nil, err
}
res, bs, err := c.doPayPalPost(ctx, bm, createWebhook)
if nil != err {
return nil, err
}
ppRsp = &CreateWebhookRsp{Code: Success}
ppRsp.Response = new(Webhook)
if err = json.Unmarshal(bs, &ppRsp.Response); nil != err {
return nil, fmt.Errorf("json.Unmarshal(%s):%w", string(bs), err)
}
if res.StatusCode != http.StatusCreated {
ppRsp.Code = res.StatusCode
ppRsp.Error = string(bs)
ppRsp.ErrorResponse = new(ErrorResponse)
_ = json.Unmarshal(bs, ppRsp.ErrorResponse)
}
return ppRsp, nil
}

// ListWebhook 查询Webhook列表
func (c *Client) ListWebhook(ctx context.Context) (ppRsp *ListWebhookRsp, err error) {
res, bs, err := c.doPayPalGet(ctx, listWebhook)
if nil != err {
return nil, err
}
ppRsp = &ListWebhookRsp{Code: Success}
if err = json.Unmarshal(bs, &ppRsp.Response); nil != err {
return nil, fmt.Errorf("json.Unmarshal(%s):%w", string(bs), err)
}
if res.StatusCode != http.StatusOK {
ppRsp.Code = res.StatusCode
ppRsp.Error = string(bs)
ppRsp.ErrorResponse = new(ErrorResponse)
_ = json.Unmarshal(bs, ppRsp.ErrorResponse)
}
return ppRsp, nil
}

// ShowWebhookDetail 查询Webhook
func (c *Client) ShowWebhookDetail(ctx context.Context, webhookId string) (ppRsp *WebhookDetailRsp, err error) {
url := fmt.Sprintf(showWebhookDetail, webhookId)
res, bs, err := c.doPayPalGet(ctx, url)
if nil != err {
return nil, err
}
ppRsp = &WebhookDetailRsp{Code: Success}
if err = json.Unmarshal(bs, &ppRsp.Response); nil != err {
return nil, fmt.Errorf("json.Unmarshal(%s):%w", string(bs), err)
}
if res.StatusCode != http.StatusOK {
ppRsp.Code = res.StatusCode
ppRsp.Error = string(bs)
ppRsp.ErrorResponse = new(ErrorResponse)
_ = json.Unmarshal(bs, ppRsp.ErrorResponse)
}
return ppRsp, nil
}

// UpdateWebhook 更新Webhook消息
func (c *Client) UpdateWebhook(ctx context.Context, webhookId string, patchs []*Patch) (ppRsp *WebhookDetailRsp, err error) {
url := fmt.Sprintf(updateWebhook, webhookId)
res, bs, err := c.doPayPalPatch(ctx, patchs, url)
if nil != err {
return nil, err
}
ppRsp = &WebhookDetailRsp{Code: Success}
if err = json.Unmarshal(bs, &ppRsp.Response); nil != err {
return nil, fmt.Errorf("json.Unmarshal(%s):%w", string(bs), err)
}
if res.StatusCode != http.StatusOK {
ppRsp.Code = res.StatusCode
ppRsp.Error = string(bs)
ppRsp.ErrorResponse = new(ErrorResponse)
_ = json.Unmarshal(bs, ppRsp.ErrorResponse)
}
return ppRsp, nil
}

// DeleteWebhook 删除Webhook消息
func (c *Client) DeleteWebhook(ctx context.Context, webhookId string) (ppRsp *WebhookDetailRsp, err error) {
url := fmt.Sprintf(deleteWebhook, webhookId)
res, bs, err := c.doPayPalDelete(ctx, url)
if nil != err {
return nil, err
}
ppRsp = &WebhookDetailRsp{Code: Success}
if res.StatusCode != http.StatusNoContent {
ppRsp.Code = res.StatusCode
ppRsp.Error = string(bs)
ppRsp.ErrorResponse = new(ErrorResponse)
_ = json.Unmarshal(bs, ppRsp.ErrorResponse)
}
return ppRsp, nil
}

// VerifyWebhookSignature 验证Webhook签名
// 文档:https://developer.paypal.com/docs/api/webhooks/v1/#verify-webhook-signature_post
func (c *Client) VerifyWebhookSignature(ctx context.Context, bm gopay.BodyMap) (verifyRes *VerifyWebhookResponse, err error) {
if err = bm.CheckEmptyError("auth_algo", "cert_url", "transmission_id", "transmission_sig", "transmission_time", "webhook_id", "webhook_event"); err != nil {
return nil, err
}
res, bs, err := c.doPayPalPost(ctx, bm, verifyWebhookSignature)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return verifyRes, errors.New("request paypal url[verify-webhook-signature_post] error")
}
verifyRes = &VerifyWebhookResponse{}
if err = json.Unmarshal(bs, verifyRes); err != nil {
return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
}
return verifyRes, nil
}

// ShowWebhookEventDetail 查询Webhook-event消息
func (c *Client) ShowWebhookEventDetail(ctx context.Context, eventId string) (ppRsp *WebhookEventDetailRsp, err error) {
url := fmt.Sprintf(showWebhookEventDetail, eventId)
res, bs, err := c.doPayPalGet(ctx, url)
if nil != err {
return nil, err
}
ppRsp = &WebhookEventDetailRsp{Code: Success}
if err = json.Unmarshal(bs, &ppRsp.Response); nil != err {
return nil, fmt.Errorf("json.Unmarshal(%s):%w", string(bs), err)
}
if res.StatusCode != http.StatusOK {
ppRsp.Code = res.StatusCode
ppRsp.Error = string(bs)
ppRsp.ErrorResponse = new(ErrorResponse)
_ = json.Unmarshal(bs, ppRsp.ErrorResponse)
}
return ppRsp, nil
}
126 changes: 126 additions & 0 deletions paypal/webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package paypal

import (
"github.com/go-pay/gopay"
"github.com/go-pay/xlog"
"testing"
)

var (
webhookId = ""
replaceUrl = ""
)

func TestClient_CreateWebhook(t *testing.T) {
url := "https://thahao-test.mynatapp.cc/pay_order/paypal"
bm := make(gopay.BodyMap)

var eventTypes []*WebhookEventType
item := &WebhookEventType{
Name: "PAYMENT.CAPTURE.REFUNDED",
}
eventTypes = append(eventTypes, item)
bm.Set("url", url).
Set("event_types", eventTypes)
ppRsp, err := client.CreateWebhook(client.ctx, bm)
if err != nil {
xlog.Error(err)
return
}
if ppRsp.Code != Success {
xlog.Debugf("ppRsp.Code: %+v", ppRsp.Code)
xlog.Debugf("ppRsp.Error: %+v", ppRsp.Error)
xlog.Debugf("ppRsp.ErrorResponse: %+v", ppRsp.ErrorResponse)
return
}
xlog.Debugf("ppRsp.Response: %+v", ppRsp.Response)
}

func TestClient_ListWebhook(t *testing.T) {
ppRsp, err := client.ListWebhook(client.ctx)
if err != nil {
xlog.Error(err)
return
}
if ppRsp.Code != Success {
xlog.Debugf("ppRsp.Code: %+v", ppRsp.Code)
xlog.Debugf("ppRsp.Error: %+v", ppRsp.Error)
xlog.Debugf("ppRsp.ErrorResponse: %+v", ppRsp.ErrorResponse)
return
}
xlog.Debugf("ppRsp.Response: %+v", ppRsp.Response)
}

func TestClient_ShowWebhookDetail(t *testing.T) {
ppRsp, err := client.ShowWebhookDetail(client.ctx, webhookId)
if err != nil {
xlog.Error(err)
return
}
if ppRsp.Code != Success {
xlog.Debugf("ppRsp.Code: %+v", ppRsp.Code)
xlog.Debugf("ppRsp.Error: %+v", ppRsp.Error)
xlog.Debugf("ppRsp.ErrorResponse: %+v", ppRsp.ErrorResponse)
return
}
xlog.Debugf("ppRsp.Response: %+v", ppRsp.Response)
}

func TestClient_UpdateWebhook(t *testing.T) {
var ps []*Patch
item := &Patch{
Op: "replace",
Path: "/url", // reference_id is yourself set when create order
Value: replaceUrl,
}
ps = append(ps, item)
ppRsp, err := client.UpdateWebhook(client.ctx, webhookId, ps)
if err != nil {
xlog.Error(err)
return
}
if ppRsp.Code != Success {
xlog.Debugf("ppRsp.Code: %+v", ppRsp.Code)
xlog.Debugf("ppRsp.Error: %+v", ppRsp.Error)
xlog.Debugf("ppRsp.ErrorResponse: %+v", ppRsp.ErrorResponse)
return
}
xlog.Debugf("ppRsp.Response: %+v", ppRsp.Response)
}

func TestClient_DeleteWebhook(t *testing.T) {
ppRsp, err := client.DeleteWebhook(client.ctx, webhookId)
if err != nil {
xlog.Error(err)
return
}
if ppRsp.Code != Success {
xlog.Debugf("ppRsp.Code: %+v", ppRsp.Code)
xlog.Debugf("ppRsp.Error: %+v", ppRsp.Error)
xlog.Debugf("ppRsp.ErrorResponse: %+v", ppRsp.ErrorResponse)
return
}
xlog.Debugf("ppRsp.Response: %+v", ppRsp.Response)
}

func TestClient_VerifyWebhookSignature(t *testing.T) {
bm := make(gopay.BodyMap)
bm.Set("auth_algo", "SHA256withRSA").
Set("cert_url", "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-ad47cb8d").
Set("transmission_id", "b9d46480-2162-11ee-a2ae-61fbe51a886c").
Set("transmission_sig", "NcbK6Mxok1iu12VU2bEgXUiFhifdX9eYlJJLtfc0etlVPgbigCZiQq3+Z8z7uNnCMh9S9rKjGr5eTscIHvUmB3jnPqUeLlGI3d670lXUkATH+p6Q/HI33ZidDAFTsgc3kZizqlONsPvmu5fdSA9UmKsaDmBEbACZXH/P4hTY4/pdAmk9OOPdySAhXj7gDwSz4ChMM0H+nSwXdyQC5IrjFQdoGABNoEPtRDUI7n0RCphu/kaZmQl7BtDXhoJAKYKmUS0pw4DhVW8hGoxBNrwizSW9eFE5tDhYO5WdGuWraGPKS5X/FD5JVfA2Kxj83rFvxHgyfKuYiMtnvevZVDp3Xg==").
Set("transmission_time", "2023-07-13T09:50:40Z").
Set("webhook_id", "3WA07241VT312694T").
SetBodyMap("webhook_event", func(b gopay.BodyMap) {
b.Set("event_version", "1.0").
Set("resource_version", "2.0")
})

xlog.Debug("bm:", bm.JsonBody())
verifyRes, err := client.VerifyWebhookSignature(ctx, bm)
if err != nil {
xlog.Error(err)
return
}
xlog.Debugf("verifyRes: %+v", verifyRes)
}

0 comments on commit 0225abe

Please sign in to comment.