diff --git a/paypal/constant.go b/paypal/constant.go index 11c7a1e5..d22eb111 100644 --- a/paypal/constant.go +++ b/paypal/constant.go @@ -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 ) diff --git a/paypal/model.go b/paypal/model.go index e9177a20..62911501 100644 --- a/paypal/model.go +++ b/paypal/model.go @@ -1,5 +1,7 @@ package paypal +import "encoding/json" + type AccessToken struct { Scope string `json:"scope"` AccessToken string `json:"access_token"` @@ -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"` +} diff --git a/paypal/webhook.go b/paypal/webhook.go new file mode 100644 index 00000000..9f9835d1 --- /dev/null +++ b/paypal/webhook.go @@ -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 +} diff --git a/paypal/webhook_test.go b/paypal/webhook_test.go new file mode 100644 index 00000000..97f44c66 --- /dev/null +++ b/paypal/webhook_test.go @@ -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) +}