Skip to content

Commit 0225abe

Browse files
authored
feat(paypal): add webhook support (#434)
* feat(paypal): add webhook support
1 parent 9940221 commit 0225abe

File tree

4 files changed

+358
-0
lines changed

4 files changed

+358
-0
lines changed

paypal/constant.go

+9
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,13 @@ const (
6262

6363
// 物流相关
6464
addTrackingNumber = "/v2/checkout/orders/%s/track" // order_id 授权物流信息 POST
65+
66+
// webhook 相关
67+
createWebhook = "/v1/notifications/webhooks" // 创建webhook POST
68+
listWebhook = "/v1/notifications/webhooks" // 获取webhook列表 GET
69+
showWebhookDetail = "/v1/notifications/webhooks/%s" // webhook_id 获取webhook详情 GET
70+
updateWebhook = "/v1/notifications/webhooks/%s" // webhook_id 更新webhook PATCH
71+
deleteWebhook = "/v1/notifications/webhooks/%s" // webhook_id 删除webhook DELETE
72+
verifyWebhookSignature = "/v1/notifications/verify-webhook-signature" //webhook消息验签
73+
showWebhookEventDetail = "/v1/notifications/webhooks-events/%s" // webhook_id 获取webhook-event详情 GET
6574
)

paypal/model.go

+74
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package paypal
22

3+
import "encoding/json"
4+
35
type AccessToken struct {
46
Scope string `json:"scope"`
57
AccessToken string `json:"access_token"`
@@ -1055,3 +1057,75 @@ type AddTrackingNumberRsp struct {
10551057
ErrorResponse *ErrorResponse `json:"-"`
10561058
Response *OrderDetail `json:"response,omitempty"`
10571059
}
1060+
1061+
type CreateWebhookRsp struct {
1062+
Code int `json:"-"`
1063+
Error string `json:"-"`
1064+
ErrorResponse *ErrorResponse `json:"-"`
1065+
Response *Webhook `json:"response,omitempty"`
1066+
}
1067+
1068+
type WebhookEventType struct {
1069+
Name string `json:"name"`
1070+
Description string `json:"description,omitempty"`
1071+
Status string `json:"status,omitempty"`
1072+
ResourceVersions []string `json:"resource_versions,omitempty"`
1073+
}
1074+
1075+
type Webhook struct {
1076+
Id string `json:"id"`
1077+
Url string `json:"url"`
1078+
EventTypes []*WebhookEventType `json:"event_types"`
1079+
Links []*Link `json:"links,omitempty"`
1080+
}
1081+
1082+
type ListWebhook struct {
1083+
Webhooks []*Webhook `json:"webhooks"`
1084+
}
1085+
1086+
type ListWebhookRsp struct {
1087+
Code int `json:"-"`
1088+
Error string `json:"-"`
1089+
ErrorResponse *ErrorResponse `json:"-"`
1090+
Response *ListWebhook `json:"response,omitempty"`
1091+
}
1092+
1093+
type WebhookDetailRsp struct {
1094+
Code int `json:"-"`
1095+
Error string `json:"-"`
1096+
ErrorResponse *ErrorResponse `json:"-"`
1097+
Response *Webhook `json:"response,omitempty"`
1098+
}
1099+
1100+
type WebhookEventDetailRsp struct {
1101+
Code int `json:"-"`
1102+
Error string `json:"-"`
1103+
ErrorResponse *ErrorResponse `json:"-"`
1104+
Response json.RawMessage `json:"response,omitempty"`
1105+
}
1106+
1107+
type VerifyWebhookSignatureRequest struct {
1108+
AuthAlgo string `json:"auth_algo,omitempty"`
1109+
CertURL string `json:"cert_url,omitempty"`
1110+
TransmissionID string `json:"transmission_id,omitempty"`
1111+
TransmissionSig string `json:"transmission_sig,omitempty"`
1112+
TransmissionTime string `json:"transmission_time,omitempty"`
1113+
WebhookID string `json:"webhook_id,omitempty"`
1114+
Event json.RawMessage `json:"webhook_event,omitempty"`
1115+
}
1116+
1117+
type VerifyWebhookResponse struct {
1118+
VerificationStatus string `json:"verification_status,omitempty"`
1119+
}
1120+
1121+
type WebhookEvent struct {
1122+
Id string `json:"id"`
1123+
CreateTime string `json:"create_time"`
1124+
ResourceType string `json:"resource_type"`
1125+
EventType string `json:"event_type"`
1126+
Summary string `json:"summary"`
1127+
Resource json.RawMessage `json:"resource,omitempty"`
1128+
Links []*Link `json:"links,omitempty"`
1129+
EventVersion string `json:"event_version"`
1130+
ResourceVersion string `json:"resource_version"`
1131+
}

paypal/webhook.go

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package paypal
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"github.com/go-pay/gopay"
9+
"net/http"
10+
)
11+
12+
// CreateWebhook 创建Webhook
13+
func (c *Client) CreateWebhook(ctx context.Context, bm gopay.BodyMap) (ppRsp *CreateWebhookRsp, err error) {
14+
if err = bm.CheckEmptyError("url", "event_types"); nil != err {
15+
return nil, err
16+
}
17+
res, bs, err := c.doPayPalPost(ctx, bm, createWebhook)
18+
if nil != err {
19+
return nil, err
20+
}
21+
ppRsp = &CreateWebhookRsp{Code: Success}
22+
ppRsp.Response = new(Webhook)
23+
if err = json.Unmarshal(bs, &ppRsp.Response); nil != err {
24+
return nil, fmt.Errorf("json.Unmarshal(%s):%w", string(bs), err)
25+
}
26+
if res.StatusCode != http.StatusCreated {
27+
ppRsp.Code = res.StatusCode
28+
ppRsp.Error = string(bs)
29+
ppRsp.ErrorResponse = new(ErrorResponse)
30+
_ = json.Unmarshal(bs, ppRsp.ErrorResponse)
31+
}
32+
return ppRsp, nil
33+
}
34+
35+
// ListWebhook 查询Webhook列表
36+
func (c *Client) ListWebhook(ctx context.Context) (ppRsp *ListWebhookRsp, err error) {
37+
res, bs, err := c.doPayPalGet(ctx, listWebhook)
38+
if nil != err {
39+
return nil, err
40+
}
41+
ppRsp = &ListWebhookRsp{Code: Success}
42+
if err = json.Unmarshal(bs, &ppRsp.Response); nil != err {
43+
return nil, fmt.Errorf("json.Unmarshal(%s):%w", string(bs), err)
44+
}
45+
if res.StatusCode != http.StatusOK {
46+
ppRsp.Code = res.StatusCode
47+
ppRsp.Error = string(bs)
48+
ppRsp.ErrorResponse = new(ErrorResponse)
49+
_ = json.Unmarshal(bs, ppRsp.ErrorResponse)
50+
}
51+
return ppRsp, nil
52+
}
53+
54+
// ShowWebhookDetail 查询Webhook
55+
func (c *Client) ShowWebhookDetail(ctx context.Context, webhookId string) (ppRsp *WebhookDetailRsp, err error) {
56+
url := fmt.Sprintf(showWebhookDetail, webhookId)
57+
res, bs, err := c.doPayPalGet(ctx, url)
58+
if nil != err {
59+
return nil, err
60+
}
61+
ppRsp = &WebhookDetailRsp{Code: Success}
62+
if err = json.Unmarshal(bs, &ppRsp.Response); nil != err {
63+
return nil, fmt.Errorf("json.Unmarshal(%s):%w", string(bs), err)
64+
}
65+
if res.StatusCode != http.StatusOK {
66+
ppRsp.Code = res.StatusCode
67+
ppRsp.Error = string(bs)
68+
ppRsp.ErrorResponse = new(ErrorResponse)
69+
_ = json.Unmarshal(bs, ppRsp.ErrorResponse)
70+
}
71+
return ppRsp, nil
72+
}
73+
74+
// UpdateWebhook 更新Webhook消息
75+
func (c *Client) UpdateWebhook(ctx context.Context, webhookId string, patchs []*Patch) (ppRsp *WebhookDetailRsp, err error) {
76+
url := fmt.Sprintf(updateWebhook, webhookId)
77+
res, bs, err := c.doPayPalPatch(ctx, patchs, url)
78+
if nil != err {
79+
return nil, err
80+
}
81+
ppRsp = &WebhookDetailRsp{Code: Success}
82+
if err = json.Unmarshal(bs, &ppRsp.Response); nil != err {
83+
return nil, fmt.Errorf("json.Unmarshal(%s):%w", string(bs), err)
84+
}
85+
if res.StatusCode != http.StatusOK {
86+
ppRsp.Code = res.StatusCode
87+
ppRsp.Error = string(bs)
88+
ppRsp.ErrorResponse = new(ErrorResponse)
89+
_ = json.Unmarshal(bs, ppRsp.ErrorResponse)
90+
}
91+
return ppRsp, nil
92+
}
93+
94+
// DeleteWebhook 删除Webhook消息
95+
func (c *Client) DeleteWebhook(ctx context.Context, webhookId string) (ppRsp *WebhookDetailRsp, err error) {
96+
url := fmt.Sprintf(deleteWebhook, webhookId)
97+
res, bs, err := c.doPayPalDelete(ctx, url)
98+
if nil != err {
99+
return nil, err
100+
}
101+
ppRsp = &WebhookDetailRsp{Code: Success}
102+
if res.StatusCode != http.StatusNoContent {
103+
ppRsp.Code = res.StatusCode
104+
ppRsp.Error = string(bs)
105+
ppRsp.ErrorResponse = new(ErrorResponse)
106+
_ = json.Unmarshal(bs, ppRsp.ErrorResponse)
107+
}
108+
return ppRsp, nil
109+
}
110+
111+
// VerifyWebhookSignature 验证Webhook签名
112+
// 文档:https://developer.paypal.com/docs/api/webhooks/v1/#verify-webhook-signature_post
113+
func (c *Client) VerifyWebhookSignature(ctx context.Context, bm gopay.BodyMap) (verifyRes *VerifyWebhookResponse, err error) {
114+
if err = bm.CheckEmptyError("auth_algo", "cert_url", "transmission_id", "transmission_sig", "transmission_time", "webhook_id", "webhook_event"); err != nil {
115+
return nil, err
116+
}
117+
res, bs, err := c.doPayPalPost(ctx, bm, verifyWebhookSignature)
118+
if err != nil {
119+
return nil, err
120+
}
121+
if res.StatusCode != http.StatusOK {
122+
return verifyRes, errors.New("request paypal url[verify-webhook-signature_post] error")
123+
}
124+
verifyRes = &VerifyWebhookResponse{}
125+
if err = json.Unmarshal(bs, verifyRes); err != nil {
126+
return nil, fmt.Errorf("[%w]: %v, bytes: %s", gopay.UnmarshalErr, err, string(bs))
127+
}
128+
return verifyRes, nil
129+
}
130+
131+
// ShowWebhookEventDetail 查询Webhook-event消息
132+
func (c *Client) ShowWebhookEventDetail(ctx context.Context, eventId string) (ppRsp *WebhookEventDetailRsp, err error) {
133+
url := fmt.Sprintf(showWebhookEventDetail, eventId)
134+
res, bs, err := c.doPayPalGet(ctx, url)
135+
if nil != err {
136+
return nil, err
137+
}
138+
ppRsp = &WebhookEventDetailRsp{Code: Success}
139+
if err = json.Unmarshal(bs, &ppRsp.Response); nil != err {
140+
return nil, fmt.Errorf("json.Unmarshal(%s):%w", string(bs), err)
141+
}
142+
if res.StatusCode != http.StatusOK {
143+
ppRsp.Code = res.StatusCode
144+
ppRsp.Error = string(bs)
145+
ppRsp.ErrorResponse = new(ErrorResponse)
146+
_ = json.Unmarshal(bs, ppRsp.ErrorResponse)
147+
}
148+
return ppRsp, nil
149+
}

paypal/webhook_test.go

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package paypal
2+
3+
import (
4+
"github.com/go-pay/gopay"
5+
"github.com/go-pay/xlog"
6+
"testing"
7+
)
8+
9+
var (
10+
webhookId = ""
11+
replaceUrl = ""
12+
)
13+
14+
func TestClient_CreateWebhook(t *testing.T) {
15+
url := "https://thahao-test.mynatapp.cc/pay_order/paypal"
16+
bm := make(gopay.BodyMap)
17+
18+
var eventTypes []*WebhookEventType
19+
item := &WebhookEventType{
20+
Name: "PAYMENT.CAPTURE.REFUNDED",
21+
}
22+
eventTypes = append(eventTypes, item)
23+
bm.Set("url", url).
24+
Set("event_types", eventTypes)
25+
ppRsp, err := client.CreateWebhook(client.ctx, bm)
26+
if err != nil {
27+
xlog.Error(err)
28+
return
29+
}
30+
if ppRsp.Code != Success {
31+
xlog.Debugf("ppRsp.Code: %+v", ppRsp.Code)
32+
xlog.Debugf("ppRsp.Error: %+v", ppRsp.Error)
33+
xlog.Debugf("ppRsp.ErrorResponse: %+v", ppRsp.ErrorResponse)
34+
return
35+
}
36+
xlog.Debugf("ppRsp.Response: %+v", ppRsp.Response)
37+
}
38+
39+
func TestClient_ListWebhook(t *testing.T) {
40+
ppRsp, err := client.ListWebhook(client.ctx)
41+
if err != nil {
42+
xlog.Error(err)
43+
return
44+
}
45+
if ppRsp.Code != Success {
46+
xlog.Debugf("ppRsp.Code: %+v", ppRsp.Code)
47+
xlog.Debugf("ppRsp.Error: %+v", ppRsp.Error)
48+
xlog.Debugf("ppRsp.ErrorResponse: %+v", ppRsp.ErrorResponse)
49+
return
50+
}
51+
xlog.Debugf("ppRsp.Response: %+v", ppRsp.Response)
52+
}
53+
54+
func TestClient_ShowWebhookDetail(t *testing.T) {
55+
ppRsp, err := client.ShowWebhookDetail(client.ctx, webhookId)
56+
if err != nil {
57+
xlog.Error(err)
58+
return
59+
}
60+
if ppRsp.Code != Success {
61+
xlog.Debugf("ppRsp.Code: %+v", ppRsp.Code)
62+
xlog.Debugf("ppRsp.Error: %+v", ppRsp.Error)
63+
xlog.Debugf("ppRsp.ErrorResponse: %+v", ppRsp.ErrorResponse)
64+
return
65+
}
66+
xlog.Debugf("ppRsp.Response: %+v", ppRsp.Response)
67+
}
68+
69+
func TestClient_UpdateWebhook(t *testing.T) {
70+
var ps []*Patch
71+
item := &Patch{
72+
Op: "replace",
73+
Path: "/url", // reference_id is yourself set when create order
74+
Value: replaceUrl,
75+
}
76+
ps = append(ps, item)
77+
ppRsp, err := client.UpdateWebhook(client.ctx, webhookId, ps)
78+
if err != nil {
79+
xlog.Error(err)
80+
return
81+
}
82+
if ppRsp.Code != Success {
83+
xlog.Debugf("ppRsp.Code: %+v", ppRsp.Code)
84+
xlog.Debugf("ppRsp.Error: %+v", ppRsp.Error)
85+
xlog.Debugf("ppRsp.ErrorResponse: %+v", ppRsp.ErrorResponse)
86+
return
87+
}
88+
xlog.Debugf("ppRsp.Response: %+v", ppRsp.Response)
89+
}
90+
91+
func TestClient_DeleteWebhook(t *testing.T) {
92+
ppRsp, err := client.DeleteWebhook(client.ctx, webhookId)
93+
if err != nil {
94+
xlog.Error(err)
95+
return
96+
}
97+
if ppRsp.Code != Success {
98+
xlog.Debugf("ppRsp.Code: %+v", ppRsp.Code)
99+
xlog.Debugf("ppRsp.Error: %+v", ppRsp.Error)
100+
xlog.Debugf("ppRsp.ErrorResponse: %+v", ppRsp.ErrorResponse)
101+
return
102+
}
103+
xlog.Debugf("ppRsp.Response: %+v", ppRsp.Response)
104+
}
105+
106+
func TestClient_VerifyWebhookSignature(t *testing.T) {
107+
bm := make(gopay.BodyMap)
108+
bm.Set("auth_algo", "SHA256withRSA").
109+
Set("cert_url", "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-ad47cb8d").
110+
Set("transmission_id", "b9d46480-2162-11ee-a2ae-61fbe51a886c").
111+
Set("transmission_sig", "NcbK6Mxok1iu12VU2bEgXUiFhifdX9eYlJJLtfc0etlVPgbigCZiQq3+Z8z7uNnCMh9S9rKjGr5eTscIHvUmB3jnPqUeLlGI3d670lXUkATH+p6Q/HI33ZidDAFTsgc3kZizqlONsPvmu5fdSA9UmKsaDmBEbACZXH/P4hTY4/pdAmk9OOPdySAhXj7gDwSz4ChMM0H+nSwXdyQC5IrjFQdoGABNoEPtRDUI7n0RCphu/kaZmQl7BtDXhoJAKYKmUS0pw4DhVW8hGoxBNrwizSW9eFE5tDhYO5WdGuWraGPKS5X/FD5JVfA2Kxj83rFvxHgyfKuYiMtnvevZVDp3Xg==").
112+
Set("transmission_time", "2023-07-13T09:50:40Z").
113+
Set("webhook_id", "3WA07241VT312694T").
114+
SetBodyMap("webhook_event", func(b gopay.BodyMap) {
115+
b.Set("event_version", "1.0").
116+
Set("resource_version", "2.0")
117+
})
118+
119+
xlog.Debug("bm:", bm.JsonBody())
120+
verifyRes, err := client.VerifyWebhookSignature(ctx, bm)
121+
if err != nil {
122+
xlog.Error(err)
123+
return
124+
}
125+
xlog.Debugf("verifyRes: %+v", verifyRes)
126+
}

0 commit comments

Comments
 (0)