Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle radom webhook events #2640

Merged
merged 23 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5e03123
feat: add radom checkout session integration for new orders
clD11 Aug 1, 2024
98ca85c
fix: rectify skus model tests after rebase
clD11 Aug 14, 2024
dffd6ba
refactor: address comments
clD11 Aug 14, 2024
3b2da14
feat: add radom checkout session integration for new orders
clD11 Aug 1, 2024
8f1e1d1
feat: handle radom webhook events
clD11 Aug 13, 2024
53cbc9f
test: remove duplicate test and fix pointer after rebase
clD11 Aug 15, 2024
824781a
feat: set radom webhook verification key
clD11 Aug 16, 2024
51b0a2d
refactor: use pointers for radom events
clD11 Aug 16, 2024
8312223
refactor: fix import order skus controller
clD11 Aug 16, 2024
fcb8753
refactor: simplify radom event processing checks
clD11 Aug 19, 2024
57bb93b
style: fix parse apostrophe
clD11 Aug 19, 2024
3feb41d
refactor: use pointer for radom subscription response
clD11 Aug 19, 2024
712f22f
refactor: remove call to error when calling err on logging
clD11 Aug 22, 2024
385a2fa
refactor: use index directly on radom mdata
clD11 Aug 22, 2024
7e43af7
refactor: make radom action error naming less generic
clD11 Aug 22, 2024
274ff31
refactor: use pointer for service in test
clD11 Aug 22, 2024
3b3cc79
refactor: sort imports in radom.go
clD11 Aug 27, 2024
32bef2c
refactor: update radom empty payments error message
clD11 Aug 27, 2024
a24f04f
style: return from func call opposed to explicit return nil
clD11 Aug 27, 2024
baa461c
refactor: use string in radom get subscription
clD11 Sep 6, 2024
69c8166
refactor: add on conflict do nothing to order history insert statement
clD11 Sep 10, 2024
d736497
refactor: rename radom event to radom notification
clD11 Sep 10, 2024
539d0fc
test: remove duplicate test after rebase
clD11 Sep 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions services/skus/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (

"github.com/brave-intl/bat-go/services/skus/handler"
"github.com/brave-intl/bat-go/services/skus/model"
"github.com/brave-intl/bat-go/services/skus/radom"
)

const (
Expand Down Expand Up @@ -1187,12 +1188,53 @@ func handleWebhookAppStoreH(w http.ResponseWriter, r *http.Request, svc *Service
}

// handleRadomWebhook handles Radom checkout session webhooks.
func handleRadomWebhook(_ *Service) handlers.AppHandler {
func handleRadomWebhook(s *Service) handlers.AppHandler {
return func(w http.ResponseWriter, r *http.Request) *handlers.AppError {
return handlers.RenderContent(context.Background(), struct{}{}, w, http.StatusNotImplemented)
return handleRadomWebhookH(w, r, s)
}
}

func handleRadomWebhookH(w http.ResponseWriter, r *http.Request, svc *Service) *handlers.AppError {
ctx := r.Context()

l := logging.Logger(ctx, "skus").With().Str("func", "handleRadomWebhookH").Logger()

if err := svc.radomAuth.Authenticate(ctx, r.Header.Get("radom-verification-key")); err != nil {
l.Err(err).Msg("invalid request")

return handlers.WrapError(err, "invalid request", http.StatusUnauthorized)
}

b, err := io.ReadAll(io.LimitReader(r.Body, reqBodyLimit10MB))
if err != nil {
l.Err(err).Msg("failed to read payload")

return handlers.RenderContent(ctx, struct{}{}, w, http.StatusOK)
}

ntf, err := radom.ParseNotification(b)
if err != nil {
l.Err(err).Msg("failed to parse radom event")

return handlers.WrapError(err, "failed to parse radom event", http.StatusBadRequest)
}

if err := svc.processRadomNotification(ctx, ntf); err != nil {
l.Err(err).Msg("failed to process radom notification")

return handlers.WrapError(model.ErrSomethingWentWrong, "something went wrong", http.StatusInternalServerError)
}

msg := "skipped radom notification"
if ntf.ShouldProcess() {
msg = "processed radom notification"
}

l.Info().Str("ntf_type", ntf.NtfType()).Str("ntf_effect", ntf.Effect()).Msg(msg)

return handlers.RenderContent(ctx, struct{}{}, w, http.StatusOK)
}

func handleStripeWebhook(svc *Service) handlers.AppHandler {
return func(w http.ResponseWriter, r *http.Request) *handlers.AppError {
ctx := r.Context()
Expand Down
73 changes: 73 additions & 0 deletions services/skus/controllers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1826,6 +1826,79 @@ func (suite *ControllersTestSuite) TestCreateOrder_RadomPayable() {
suite.Equal(sessID, actual)
}

func (suite *ControllersTestSuite) TestWebhook_Radom() {
ctx := context.Background()

oreq := &model.OrderNew{
Status: OrderStatusPending,
}

res, err := suite.service.orderRepo.Create(ctx, suite.service.Datastore.RawDB(), oreq)
suite.Require().NoError(err)

subID := uuid.NewV4()

suite.service.radomClient = &mockRadomClient{
fnGetSubscription: func(ctx context.Context, subID string) (*radom.SubscriptionResponse, error) {
return &radom.SubscriptionResponse{
ID: subID,
NextBillingDateAt: "2023-06-12T09:38:13.604410Z",
Payments: []radom.Payment{
{
Date: "2023-06-12T09:38:13.604410Z",
},
},
}, nil
},
}

suite.service.radomAuth = radom.NewMessageAuthenticator(radom.MessageAuthConfig{
Token: []byte("test-token"),
Enabled: true,
})

suite.service.payHistRepo = repository.NewOrderPayHistory()

event := &radom.Notification{
EventData: &radom.EventData{
New: &radom.NewSubscription{
SubscriptionID: subID,
},
},
RadomData: &radom.Data{
CheckoutSession: &radom.CheckoutSession{
Metadata: []radom.Metadata{
{
Key: "brave_order_id",
Value: res.ID.String(),
},
},
},
},
}

b, err := json.Marshal(event)
suite.Require().NoError(err)

req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(b))

req.Header.Add("radom-verification-key", "test-token")

rw := httptest.NewRecorder()

oh := handleRadomWebhook(suite.service)
svr := &http.Server{Addr: ":8080", Handler: oh}

svr.Handler.ServeHTTP(rw, req)

suite.Require().Equal(http.StatusOK, rw.Code)

order, err := suite.service.orderRepo.Get(ctx, suite.service.Datastore.RawDB(), res.ID)
suite.Require().NoError(err)

suite.Equal(model.OrderStatusPaid, order.Status)
}

// ReadSigningOrderRequestMessage reads messages from the unsigned order request topic
func (suite *ControllersTestSuite) ReadSigningOrderRequestMessage(ctx context.Context, topic string) SigningOrderRequest {
kafkaReader, err := kafkautils.NewKafkaReader(ctx, test.RandomString(), topic)
Expand Down
1 change: 1 addition & 0 deletions services/skus/model/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
must "github.com/stretchr/testify/require"

"github.com/brave-intl/bat-go/libs/datastore"

"github.com/brave-intl/bat-go/services/skus/model"
)

Expand Down
198 changes: 198 additions & 0 deletions services/skus/radom/notification.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package radom

import (
"context"
"crypto/subtle"
"encoding/json"

uuid "github.com/satori/go.uuid"
)

const (
ErrUnsupportedEvent = Error("radom: unsupported event")
ErrNoCheckoutSessionData = Error("radom: no checkout session data")
ErrBraveOrderIDNotFound = Error("radom: brave order id not found")
ErrNoRadomPaymentData = Error("radom: no radom payment data")

ErrDisabled = Error("radom: disabled")
ErrVerificationKeyEmpty = Error("radom: verification key is empty")
ErrVerificationKeyInvalid = Error("radom: verification key is invalid")
)

type Notification struct {
EventType string `json:"eventType"`
EventData *EventData `json:"eventData"`
RadomData *Data `json:"radomData"`
}

type EventData struct {
New *NewSubscription `json:"newSubscription"`
Payment *SubscriptionPayment `json:"subscriptionPayment"`
Cancelled *SubscriptionCancelled `json:"subscriptionCancelled"`
Expired *SubscriptionExpired `json:"subscriptionExpired"`
}

type NewSubscription struct {
SubscriptionID uuid.UUID `json:"subscriptionId"`
}

type SubscriptionPayment struct {
RadomData *Data `json:"radomData"`
}

type SubscriptionCancelled struct {
SubscriptionID uuid.UUID `json:"subscriptionId"`
}

type SubscriptionExpired struct {
SubscriptionID uuid.UUID `json:"subscriptionId"`
}

type Data struct {
CheckoutSession *CheckoutSession `json:"checkoutSession"`
Subscription *Subscription `json:"subscription"`
}

type CheckoutSession struct {
CheckoutSessionID string `json:"checkoutSessionId"`
Metadata []Metadata `json:"metadata"`
}

type Subscription struct {
SubscriptionID uuid.UUID `json:"subscriptionId"`
}

func (n *Notification) OrderID() (uuid.UUID, error) {
switch {
case n.EventData == nil || n.EventData.New == nil:
return uuid.Nil, ErrUnsupportedEvent

case n.RadomData == nil || n.RadomData.CheckoutSession == nil:
return uuid.Nil, ErrNoCheckoutSessionData

default:
mdata := n.RadomData.CheckoutSession.Metadata

for i := range mdata {
if mdata[i].Key == "brave_order_id" {
return uuid.FromString(mdata[i].Value)
}
}

return uuid.Nil, ErrBraveOrderIDNotFound
}
}

func (n *Notification) SubID() (uuid.UUID, error) {
switch {
case n.EventData == nil:
return uuid.Nil, ErrUnsupportedEvent

case n.EventData.New != nil:
return n.EventData.New.SubscriptionID, nil

case n.EventData.Payment != nil:
if n.EventData.Payment.RadomData == nil || n.EventData.Payment.RadomData.Subscription == nil {
return uuid.Nil, ErrNoRadomPaymentData
}

return n.EventData.Payment.RadomData.Subscription.SubscriptionID, nil

case n.EventData.Cancelled != nil:
return n.EventData.Cancelled.SubscriptionID, nil

case n.EventData.Expired != nil:
return n.EventData.Expired.SubscriptionID, nil

default:
return uuid.Nil, ErrUnsupportedEvent
}
}

func (n *Notification) IsNewSub() bool {
return n.EventData != nil && n.EventData.New != nil
}

func (n *Notification) ShouldRenew() bool {
return n.EventData != nil && n.EventData.Payment != nil
}

func (n *Notification) ShouldCancel() bool {
switch {
case n.EventData == nil:
return false

case n.EventData.Cancelled != nil:
return true

case n.EventData.Expired != nil:
return true

default:
return false
}
}

func (n *Notification) ShouldProcess() bool {
return n.IsNewSub() || n.ShouldRenew() || n.ShouldCancel()
}

func (n *Notification) Effect() string {
switch {
case n.IsNewSub():
return "new"

case n.ShouldRenew():
return "renew"

case n.ShouldCancel():
return "cancel"

default:
return "skip"
}
}

func (n *Notification) NtfType() string {
return n.EventType
}

func ParseNotification(b []byte) (*Notification, error) {
ntf := &Notification{}
if err := json.Unmarshal(b, ntf); err != nil {
return nil, err
}

return ntf, nil
}

type MessageAuthConfig struct {
Enabled bool
Token []byte
}

type MessageAuthenticator struct {
cfg MessageAuthConfig
}

func NewMessageAuthenticator(cfg MessageAuthConfig) *MessageAuthenticator {
return &MessageAuthenticator{
cfg: cfg,
}
}

func (r *MessageAuthenticator) Authenticate(_ context.Context, token string) error {
if !r.cfg.Enabled {
return ErrDisabled
}

if token == "" {
return ErrVerificationKeyEmpty
}

if subtle.ConstantTimeCompare(r.cfg.Token, []byte(token)) != 1 {
return ErrVerificationKeyInvalid
}

return nil
}
Loading
Loading