From 5697524a4bc1cac13c6e9f62005e25e268369fc6 Mon Sep 17 00:00:00 2001 From: clD11 <23483715+clD11@users.noreply.github.com> Date: Thu, 1 Aug 2024 20:54:55 +0100 Subject: [PATCH] feat: add radom checkout session integration for new orders --- libs/clients/radom/instrumented.go | 50 ---- libs/clients/radom/mock.go | 20 -- libs/clients/radom/radom.go | 212 --------------- libs/context/keys.go | 11 - services/grant/cmd/grant.go | 7 - services/skus/controllers.go | 89 +------ services/skus/controllers_test.go | 71 ++++- services/skus/model/model.go | 140 +++++----- services/skus/model/model_pvt_test.go | 2 +- services/skus/model/model_test.go | 283 +++++++++----------- services/skus/radom/radom.go | 71 +++++ services/skus/service.go | 222 +++++++++++----- services/skus/service_nonint_test.go | 360 ++++++++++++++++++++++++++ services/skus/service_test.go | 8 +- 14 files changed, 852 insertions(+), 694 deletions(-) delete mode 100644 libs/clients/radom/instrumented.go delete mode 100644 libs/clients/radom/mock.go delete mode 100644 libs/clients/radom/radom.go create mode 100644 services/skus/radom/radom.go diff --git a/libs/clients/radom/instrumented.go b/libs/clients/radom/instrumented.go deleted file mode 100644 index ae1cd4c35..000000000 --- a/libs/clients/radom/instrumented.go +++ /dev/null @@ -1,50 +0,0 @@ -package radom - -import ( - "context" - "time" - - "github.com/prometheus/client_golang/prometheus" -) - -type InstrumentedClient struct { - name string - cl *Client - vec *prometheus.SummaryVec -} - -// newInstrumentedClient returns an instance of the Client decorated with prometheus summary metric. -// This function panics if it cannot register the metric. -func newInstrumentedClient(name string, cl *Client) *InstrumentedClient { - v := prometheus.NewSummaryVec(prometheus.SummaryOpts{ - Name: "radom_client_duration_seconds", - Help: "client runtime duration and result", - MaxAge: time.Minute, - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }, - []string{"instance_name", "method", "result"}, - ) - prometheus.MustRegister(v) - - result := &InstrumentedClient{ - name: name, - cl: cl, - vec: v, - } - - return result -} - -func (_d *InstrumentedClient) CreateCheckoutSession(ctx context.Context, cp1 *CheckoutSessionRequest) (cp2 *CheckoutSessionResponse, err error) { - _since := time.Now() - defer func() { - result := "ok" - if err != nil { - result = "error" - } - - _d.vec.WithLabelValues(_d.name, "CreateCheckoutSession", result).Observe(time.Since(_since).Seconds()) - }() - - return _d.cl.CreateCheckoutSession(ctx, cp1) -} diff --git a/libs/clients/radom/mock.go b/libs/clients/radom/mock.go deleted file mode 100644 index 3205ccb35..000000000 --- a/libs/clients/radom/mock.go +++ /dev/null @@ -1,20 +0,0 @@ -package radom - -import ( - "context" -) - -type MockClient struct { - FnCreateCheckoutSession func(ctx context.Context, req *CheckoutSessionRequest) (*CheckoutSessionResponse, error) -} - -func (c *MockClient) CreateCheckoutSession( - ctx context.Context, - req *CheckoutSessionRequest, -) (*CheckoutSessionResponse, error) { - if c.FnCreateCheckoutSession == nil { - return &CheckoutSessionResponse{}, nil - } - - return c.FnCreateCheckoutSession(ctx, req) -} diff --git a/libs/clients/radom/radom.go b/libs/clients/radom/radom.go deleted file mode 100644 index 0efe49545..000000000 --- a/libs/clients/radom/radom.go +++ /dev/null @@ -1,212 +0,0 @@ -package radom - -import ( - "context" - "crypto/subtle" - "errors" - "time" - - "github.com/shopspring/decimal" - - "github.com/brave-intl/bat-go/libs/clients" - appctx "github.com/brave-intl/bat-go/libs/context" -) - -var ( - ErrInvalidMetadataKey = errors.New("invalid metadata key") -) - -// CheckoutSessionRequest represents a request to create a checkout session. -type CheckoutSessionRequest struct { - SuccessURL string `json:"successUrl"` - CancelURL string `json:"cancelUrl"` - Currency string `json:"currency"` - ExpiresAt int64 `json:"expiresAt"` // in unix seconds - LineItems []LineItem `json:"lineItems"` - Metadata Metadata `json:"metadata"` - Customizations map[string]interface{} `json:"customizations"` - Total decimal.Decimal `json:"total"` - Gateway Gateway `json:"gateway"` -} - -// Gateway provides access to managed services configurations -type Gateway struct { - Managed Managed `json:"managed"` -} - -// Managed is the Radom managed services configuration -type Managed struct { - Methods []Method `json:"methods"` -} - -// Method is a Radom payment method type -type Method struct { - Network string `json:"network"` - Token string `json:"token"` -} - -// CheckoutSessionResponse represents the result of submission of a checkout session. -type CheckoutSessionResponse struct { - SessionID string `json:"checkoutSessionId"` - SessionURL string `json:"checkoutSessionUrl"` -} - -// LineItem is a line item for a checkout session request. -type LineItem struct { - ProductID string `json:"productId"` - ItemData map[string]interface{} `json:"itemData"` -} - -// Metadata represents metaadata in a checkout session request. -type Metadata []KeyValue - -// Get allows returns a value based on the key from the Radom metadata. -func (m Metadata) Get(key string) (string, error) { - for _, v := range m { - if subtle.ConstantTimeCompare([]byte(key), []byte(v.Key)) == 1 { - return v.Value, nil - } - } - - return "", ErrInvalidMetadataKey -} - -// KeyValue represents a key-value metadata pair. -type KeyValue struct { - Key string `json:"key"` - Value string `json:"value"` -} - -// AutomatedEVMSubscripton defines an automated subscription -type AutomatedEVMSubscription struct { - BuyerAddress string `json:"buyerAddress"` - SubscriptionContractAddress string `json:"subscriptionContractAddress"` -} - -// Subscription is a radom subscription -type Subscription struct { - AutomatedEVMSubscription AutomatedEVMSubscription `json:"automatedEVMSubscription"` -} - -// NewSubscriptionData provides details about the new subscription -type NewSubscriptionData struct { - SubscriptionID string `json:"subscriptionId"` - Subscription Subscription `json:"subscriptionType"` - Network string `json:"network"` - Token string `json:"token"` - Amount decimal.Decimal `json:"amount"` - Currency string `json:"currency"` - Period string `json:"period"` - PeriodCustomDuration string `json:"periodCustomDuration"` - CreatedAt *time.Time `json:"createdAt"` - Tags map[string]string `json:"tags"` -} - -// Data is radom specific data attached to webhook calls -type Data struct { - CheckoutSession CheckoutSession `json:"checkoutSession"` -} - -// CheckoutSession describes a radom checkout session -type CheckoutSession struct { - CheckoutSessionID string `json:"checkoutSessionId"` - Metadata Metadata `json:"metadata"` -} - -// ManagedRecurringPayment provides details about the recurring payment from webhook -type ManagedRecurringPayment struct { - PaymentMethod Method `json:"paymentMethod"` - Amount decimal.Decimal `json:"amount"` -} - -// EventData encapsulates the webhook event -type EventData struct { - ManagedRecurringPayment *ManagedRecurringPayment `json:"managedRecurringPayment"` - NewSubscription *NewSubscriptionData `json:"newSubscription"` -} - -// WebhookRequest represents a radom webhook submission -type WebhookRequest struct { - EventType string `json:"eventType"` - EventData EventData `json:"eventData"` - Data Data `json:"radomData"` -} - -// Client communicates with Radom. -type Client struct { - client *clients.SimpleHTTPClient - gwMethodsProd []Method - gwMethods []Method -} - -// New returns a ready to use Client. -func New(srvURL, secret, proxyAddr string) (*Client, error) { - return newClient(srvURL, secret, proxyAddr) -} - -func NewInstrumented(srvURL, secret, proxyAddr string) (*InstrumentedClient, error) { - cl, err := newClient(srvURL, secret, proxyAddr) - if err != nil { - return nil, err - } - - return newInstrumentedClient("radom_client", cl), nil -} - -func newClient(srvURL, secret, proxyAddr string) (*Client, error) { - client, err := clients.NewWithProxy("radom", srvURL, secret, proxyAddr) - if err != nil { - return nil, err - } - - result := &Client{ - client: client, - gwMethodsProd: []Method{ - { - Network: "Polygon", - Token: "0x3cef98bb43d732e2f285ee605a8158cde967d219", - }, - - { - Network: "Ethereum", - Token: "0x0d8775f648430679a709e98d2b0cb6250d2887ef", - }, - }, - gwMethods: []Method{ - { - Network: "SepoliaTestnet", - Token: "0x5D684d37922dAf7Aa2013E65A22880a11C475e25", - }, - { - Network: "PolygonTestnet", - Token: "0xd445cAAbb9eA6685D3A512439256866563a16E93", - }, - }, - } - - return result, nil -} - -// CreateCheckoutSession creates a Radom checkout session. -func (c *Client) CreateCheckoutSession( - ctx context.Context, - req *CheckoutSessionRequest, -) (*CheckoutSessionResponse, error) { - // Get the environment so we know what is acceptable chain/tokens. - methods := c.methodsForEnv(ctx) - - req.Gateway = Gateway{ - Managed: Managed{Methods: methods}, - } - - return nil, errors.New("not implemented") -} - -func (c *Client) methodsForEnv(ctx context.Context) []Method { - env, ok := ctx.Value(appctx.EnvironmentCTXKey).(string) - if !ok || env != "production" { - return c.gwMethods - } - - return c.gwMethodsProd -} diff --git a/libs/context/keys.go b/libs/context/keys.go index 2e45a1874..b9f61baa5 100644 --- a/libs/context/keys.go +++ b/libs/context/keys.go @@ -115,17 +115,6 @@ const ( // DisableSolanaLinkingCTXKey - this informs if solana linking is enabled DisableSolanaLinkingCTXKey CTXKey = "disable_solana_linking" - // RadomWebhookSecretCTXKey - the webhook secret key for radom integration - RadomWebhookSecretCTXKey CTXKey = "radom_webhook_secret" - // RadomEnabledCTXKey - this informs if radom is enabled - RadomEnabledCTXKey CTXKey = "radom_enabled" - // RadomSellerAddressCTXKey is the seller address on radom - RadomSellerAddressCTXKey CTXKey = "radom_seller_address" - // RadomServerCTXKey is the server address on radom - RadomServerCTXKey CTXKey = "radom_server" - // RadomSecretCTXKey is the server secret on radom - RadomSecretCTXKey CTXKey = "radom_secret" - // stripe related keys // StripeEnabledCTXKey - this informs if stripe is enabled diff --git a/services/grant/cmd/grant.go b/services/grant/cmd/grant.go index 759b56b48..317255732 100644 --- a/services/grant/cmd/grant.go +++ b/services/grant/cmd/grant.go @@ -630,13 +630,6 @@ func GrantServer( ctx = context.WithValue(ctx, appctx.StripeWebhookSecretCTXKey, viper.GetString("stripe-webhook-secret")) ctx = context.WithValue(ctx, appctx.StripeSecretCTXKey, viper.GetString("stripe-secret")) - // Variables for Radom. - ctx = context.WithValue(ctx, appctx.RadomEnabledCTXKey, viper.GetBool("radom-enabled")) - ctx = context.WithValue(ctx, appctx.RadomWebhookSecretCTXKey, viper.GetString("radom-webhook-secret")) - ctx = context.WithValue(ctx, appctx.RadomSecretCTXKey, viper.GetString("radom-secret")) - ctx = context.WithValue(ctx, appctx.RadomServerCTXKey, viper.GetString("radom-server")) - ctx = context.WithValue(ctx, appctx.RadomSellerAddressCTXKey, viper.GetString("radom-seller-address")) - // require country present from uphold txs ctx = context.WithValue(ctx, appctx.RequireUpholdCountryCTXKey, viper.GetBool("require-uphold-destination-country")) diff --git a/services/skus/controllers.go b/services/skus/controllers.go index 774f29a39..d04d1c5f2 100644 --- a/services/skus/controllers.go +++ b/services/skus/controllers.go @@ -2,7 +2,6 @@ package skus import ( "context" - "crypto/subtle" "encoding/base64" "encoding/json" "errors" @@ -19,7 +18,6 @@ import ( uuid "github.com/satori/go.uuid" "github.com/stripe/stripe-go/v72/webhook" - "github.com/brave-intl/bat-go/libs/clients/radom" appctx "github.com/brave-intl/bat-go/libs/context" "github.com/brave-intl/bat-go/libs/handlers" "github.com/brave-intl/bat-go/libs/inputs" @@ -27,7 +25,6 @@ import ( "github.com/brave-intl/bat-go/libs/middleware" "github.com/brave-intl/bat-go/libs/requestutils" "github.com/brave-intl/bat-go/libs/responses" - "github.com/brave-intl/bat-go/services/skus/handler" "github.com/brave-intl/bat-go/services/skus/model" ) @@ -999,7 +996,7 @@ func WebhookRouter(svc *Service) chi.Router { r := chi.NewRouter() r.Method(http.MethodPost, "/stripe", middleware.InstrumentHandler("HandleStripeWebhook", handleStripeWebhook(svc))) - r.Method(http.MethodPost, "/radom", middleware.InstrumentHandler("HandleRadomWebhook", HandleRadomWebhook(svc))) + r.Method(http.MethodPost, "/radom", middleware.InstrumentHandler("handleRadomWebhook", handleRadomWebhook(svc))) r.Method(http.MethodPost, "/android", middleware.InstrumentHandler("handleWebhookPlayStore", handleWebhookPlayStore(svc))) r.Method(http.MethodPost, "/ios", middleware.InstrumentHandler("handleWebhookAppStore", handleWebhookAppStore(svc))) @@ -1189,88 +1186,10 @@ func handleWebhookAppStoreH(w http.ResponseWriter, r *http.Request, svc *Service return handlers.RenderContent(ctx, struct{}{}, w, http.StatusOK) } -// HandleRadomWebhook handles Radom checkout session webhooks. -func HandleRadomWebhook(service *Service) handlers.AppHandler { +// handleRadomWebhook handles Radom checkout session webhooks. +func handleRadomWebhook(_ *Service) handlers.AppHandler { return func(w http.ResponseWriter, r *http.Request) *handlers.AppError { - ctx := r.Context() - - lg := logging.Logger(ctx, "payments").With().Str("func", "HandleRadomWebhook").Logger() - - // Get webhook secret. - endpointSecret, err := appctx.GetStringFromContext(ctx, appctx.RadomWebhookSecretCTXKey) - if err != nil { - lg.Error().Err(err).Msg("failed to get radom_webhook_secret from context") - return handlers.WrapError(err, "error getting radom_webhook_secret from context", http.StatusInternalServerError) - } - - // Check verification key. - if subtle.ConstantTimeCompare([]byte(r.Header.Get("radom-verification-key")), []byte(endpointSecret)) != 1 { - lg.Error().Err(err).Msg("invalid verification key from webhook") - return handlers.WrapError(err, "invalid verification key", http.StatusBadRequest) - } - - req := radom.WebhookRequest{} - if err := requestutils.ReadJSON(ctx, r.Body, &req); err != nil { - lg.Error().Err(err).Msg("failed to read request body") - return handlers.WrapError(err, "error reading request body", http.StatusServiceUnavailable) - } - - lg.Debug().Str("event_type", req.EventType).Str("data", fmt.Sprintf("%+v", req)).Msg("webhook event captured") - - // Handle only successful payment events. - if req.EventType != "managedRecurringPayment" && req.EventType != "newSubscription" { - return handlers.WrapError(err, "event type not implemented", http.StatusBadRequest) - } - - // Lookup the order, the checkout session was created with orderId in metadata. - rawOrderID, err := req.Data.CheckoutSession.Metadata.Get("braveOrderId") - if err != nil || rawOrderID == "" { - return handlers.WrapError(err, "brave metadata not found in webhook", http.StatusBadRequest) - } - - orderID, err := uuid.FromString(rawOrderID) - if err != nil { - return handlers.WrapError(err, "invalid braveOrderId in request", http.StatusBadRequest) - } - - // Set order id to paid, and update metadata values. - if err := service.Datastore.UpdateOrder(orderID, OrderStatusPaid); err != nil { - lg.Error().Err(err).Msg("failed to update order status") - return handlers.WrapError(err, "error updating order status", http.StatusInternalServerError) - } - - if err := service.Datastore.AppendOrderMetadata( - ctx, &orderID, "radomCheckoutSession", req.Data.CheckoutSession.CheckoutSessionID); err != nil { - lg.Error().Err(err).Msg("failed to update order metadata") - return handlers.WrapError(err, "error updating order metadata", http.StatusInternalServerError) - } - - if req.EventType == "newSubscription" { - - if err := service.Datastore.AppendOrderMetadata( - ctx, &orderID, "subscriptionId", req.EventData.NewSubscription.SubscriptionID); err != nil { - lg.Error().Err(err).Msg("failed to update order metadata") - return handlers.WrapError(err, "error updating order metadata", http.StatusInternalServerError) - } - - if err := service.Datastore.AppendOrderMetadata( - ctx, &orderID, "subscriptionContractAddress", - req.EventData.NewSubscription.Subscription.AutomatedEVMSubscription.SubscriptionContractAddress); err != nil { - - lg.Error().Err(err).Msg("failed to update order metadata") - return handlers.WrapError(err, "error updating order metadata", http.StatusInternalServerError) - } - - } - - // Set paymentProcessor to Radom. - if err := service.Datastore.AppendOrderMetadata(ctx, &orderID, "paymentProcessor", model.RadomPaymentMethod); err != nil { - lg.Error().Err(err).Msg("failed to update order to add the payment processor") - return handlers.WrapError(err, "failed to update order to add the payment processor", http.StatusInternalServerError) - } - - lg.Debug().Str("orderID", orderID.String()).Msg("order is now paid") - return handlers.RenderContent(ctx, "payment successful", w, http.StatusOK) + return handlers.RenderContent(context.Background(), struct{}{}, w, http.StatusNotImplemented) } } diff --git a/services/skus/controllers_test.go b/services/skus/controllers_test.go index d8efc75d2..55fa56d4a 100644 --- a/services/skus/controllers_test.go +++ b/services/skus/controllers_test.go @@ -19,6 +19,7 @@ import ( "time" "github.com/asaskevich/govalidator" + "github.com/brave-intl/bat-go/services/skus/radom" "github.com/go-chi/chi" "github.com/go-chi/cors" "github.com/golang/mock/gomock" @@ -275,8 +276,8 @@ func (suite *ControllersTestSuite) AfterTest(sn, tn string) { suite.mockCtrl.Finish() } -func (s *ControllersTestSuite) TearDownSuite(sn, tn string) { - skustest.CleanDB(s.T(), s.storage.RawDB()) +func (suite *ControllersTestSuite) TearDownSuite(sn, tn string) { + skustest.CleanDB(suite.T(), suite.storage.RawDB()) } func (suite *ControllersTestSuite) setupCreateOrder(skuToken string, token macaroon.Token, quantity int) (Order, *Issuer) { @@ -1759,6 +1760,72 @@ func (suite *ControllersTestSuite) TestCreateOrderCreds_SingleUse_ExistingOrderC suite.Assert().Contains(appError.Error(), ErrCredsAlreadyExist.Error()) } +func (suite *ControllersTestSuite) TestCreateOrder_RadomPayable() { + suite.service.issuerRepo = &repository.MockIssuer{} + + sessID := uuid.NewV4().String() + + suite.service.radomClient = &mockRadomClient{ + fnCreateCheckoutSession: func(ctx context.Context, creq radom.CheckoutSessionRequest) (radom.CheckoutSessionResponse, error) { + return radom.CheckoutSessionResponse{ + SessionID: sessID, + }, nil + }} + + oreq := model.CreateOrderRequestNew{ + Email: "example@example.com", + Currency: "USD", + RadomMetadata: &model.OrderRadomMetadata{ + SuccessURI: "https://example-success.com", + CancelURI: "https://example-cancel.com", + }, + PaymentMethods: []string{model.RadomPaymentMethod}, + Items: []model.OrderItemRequestNew{ + { + Quantity: 1, + SKU: "sku", + Location: "https://example.com", + Description: "description", + CredentialType: timeLimitedV2, + CredentialValidDuration: "P1M", + CredentialValidDurationEach: ptrTo("P1M"), + Price: decimal.NewFromInt(1), + RadomMetadata: &model.ItemRadomMetadata{ + ProductID: "product_1", + }, + }, + }, + } + + b, err := json.Marshal(oreq) + suite.Require().NoError(err) + + req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(b)) + + rw := httptest.NewRecorder() + + oh := handlers.AppHandler(handler.NewOrder(suite.service).CreateNew) + svr := &http.Server{Addr: ":8080", Handler: oh} + + svr.Handler.ServeHTTP(rw, req) + + suite.Require().Equal(http.StatusCreated, rw.Code) + + var resp model.Order + { + err := json.Unmarshal(rw.Body.Bytes(), &resp) + suite.Require().NoError(err) + } + + order, err := suite.service.orderRepo.Get(context.Background(), suite.service.Datastore.RawDB(), resp.ID) + suite.Require().NoError(err) + + actual, ok := order.Metadata["radomCheckoutSessionId"].(string) + suite.Require().True(ok) + + suite.Equal(sessID, actual) +} + // 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) diff --git a/services/skus/model/model.go b/services/skus/model/model.go index 4a1ce1100..683b56c67 100644 --- a/services/skus/model/model.go +++ b/services/skus/model/model.go @@ -2,7 +2,6 @@ package model import ( - "context" "database/sql" "fmt" "net/url" @@ -17,7 +16,6 @@ import ( "github.com/stripe/stripe-go/v72/checkout/session" "github.com/stripe/stripe-go/v72/customer" - "github.com/brave-intl/bat-go/libs/clients/radom" "github.com/brave-intl/bat-go/libs/datastore" ) @@ -31,9 +29,6 @@ const ( ErrNoRowsChangedOrderPayHistory Error = "model: no rows changed in order_payment_history" ErrExpiredStripeCheckoutSessionIDNotFound Error = "model: expired stripeCheckoutSessionId not found" ErrInvalidOrderNoItems Error = "model: invalid order: no items" - ErrInvalidOrderNoSuccessURL Error = "model: invalid order: no success url" - ErrInvalidOrderNoCancelURL Error = "model: invalid order: no cancel url" - ErrInvalidOrderNoProductID Error = "model: invalid order: no product id" ErrNoStripeCheckoutSessID Error = "model: order: no stripe checkout session id" ErrInvalidOrderMetadataType Error = "model: order: invalid metadata type" @@ -98,10 +93,6 @@ func (v Vendor) String() string { return string(v) } -type radomClient interface { - CreateCheckoutSession(ctx context.Context, req *radom.CheckoutSessionRequest) (*radom.CheckoutSessionResponse, error) -} - // Order represents an individual order. type Order struct { ID uuid.UUID `json:"id" db:"id"` @@ -203,76 +194,6 @@ func CreateStripeCheckoutSession( return CreateCheckoutSessionResponse{SessionID: session.ID}, nil } -// CreateRadomCheckoutSession creates a Radom checkout session for o. -func (o *Order) CreateRadomCheckoutSession( - ctx context.Context, - client radomClient, - sellerAddr string, -) (CreateCheckoutSessionResponse, error) { - return o.CreateRadomCheckoutSessionWithTime(ctx, client, sellerAddr, time.Now().Add(24*time.Hour)) -} - -// CreateRadomCheckoutSessionWithTime creates a Radom checkout session for o. -// -// TODO: This must be refactored before it's usable. Issues with the current implementation: -// - it assumes one item per order; -// - most of the logic does not belong in here; -// - metadata information must be passed explisictly instead of being parsed (it's known prior to this place); -// And more. -func (o *Order) CreateRadomCheckoutSessionWithTime( - ctx context.Context, - client radomClient, - sellerAddr string, - expiresAt time.Time, -) (CreateCheckoutSessionResponse, error) { - if len(o.Items) < 1 { - return EmptyCreateCheckoutSessionResponse(), ErrInvalidOrderNoItems - } - - successURI, ok := o.Items[0].Metadata["radom_success_uri"].(string) - if !ok { - return EmptyCreateCheckoutSessionResponse(), ErrInvalidOrderNoSuccessURL - } - - cancelURI, ok := o.Items[0].Metadata["radom_cancel_uri"].(string) - if !ok { - return EmptyCreateCheckoutSessionResponse(), ErrInvalidOrderNoCancelURL - } - - productID, ok := o.Items[0].Metadata["radom_product_id"].(string) - if !ok { - return EmptyCreateCheckoutSessionResponse(), ErrInvalidOrderNoProductID - } - - resp, err := client.CreateCheckoutSession(ctx, &radom.CheckoutSessionRequest{ - SuccessURL: successURI, - CancelURL: cancelURI, - // Gateway will be set by the client. - Metadata: radom.Metadata([]radom.KeyValue{ - { - Key: "braveOrderId", - Value: o.ID.String(), - }, - }), - LineItems: []radom.LineItem{ - { - ProductID: productID, - }, - }, - ExpiresAt: expiresAt.Unix(), - Customizations: map[string]interface{}{ - "leftPanelColor": "linear-gradient(125deg, rgba(0,0,128,1) 0%, RGBA(196,22,196,1) 100%)", - "primaryButtonColor": "#000000", - "slantedEdge": true, - }, - }) - if err != nil { - return EmptyCreateCheckoutSessionResponse(), fmt.Errorf("failed to get checkout session response: %w", err) - } - - return CreateCheckoutSessionResponse{SessionID: resp.SessionID}, nil -} - // IsPaid returns true if the order is paid. func (o *Order) IsPaid() bool { switch o.Status { @@ -543,6 +464,7 @@ type CreateOrderRequestNew struct { Email string `json:"email" validate:"required,email"` Currency string `json:"currency" validate:"required,iso4217"` StripeMetadata *OrderStripeMetadata `json:"stripe_metadata"` + RadomMetadata *OrderRadomMetadata `json:"radom_metadata"` PaymentMethods []string `json:"payment_methods"` Items []OrderItemRequestNew `json:"items" validate:"required,gt=0,dive"` } @@ -561,6 +483,7 @@ type OrderItemRequestNew struct { CredentialValidDurationEach *string `json:"each_credential_valid_duration"` IssuanceInterval *string `json:"issuance_interval"` StripeMetadata *ItemStripeMetadata `json:"stripe_metadata"` + RadomMetadata *ItemRadomMetadata `json:"radom_metadata"` } func (r *OrderItemRequestNew) TokenBufferOrDefault() int { @@ -587,6 +510,22 @@ func (r *OrderItemRequestNew) TokenOverlapOrDefault() int { return r.IssuerTokenOverlap } +func (r *OrderItemRequestNew) Metadata() map[string]interface{} { + if r == nil { + return nil + } + + if r.StripeMetadata != nil { + return r.StripeMetadata.Metadata() + } + + if r.RadomMetadata != nil { + return r.RadomMetadata.Metadata() + } + + return nil +} + // OrderStripeMetadata holds data relevant to the order in Stripe. type OrderStripeMetadata struct { SuccessURI string `json:"success_uri" validate:"http_url"` @@ -635,6 +574,49 @@ func (m *ItemStripeMetadata) Metadata() map[string]interface{} { return result } +// OrderRadomMetadata holds data relevant to the order in Radom. +type OrderRadomMetadata struct { + SuccessURI string `json:"success_uri" validate:"http_url"` + CancelURI string `json:"cancel_uri" validate:"http_url"` +} + +func (m *OrderRadomMetadata) SuccessURL(oid string) (string, error) { + if m == nil { + return "", nil + } + + return addURLParam(m.SuccessURI, "order_id", oid) +} + +func (m *OrderRadomMetadata) CancelURL(oid string) (string, error) { + if m == nil { + return "", nil + } + + return addURLParam(m.CancelURI, "order_id", oid) +} + +// ItemRadomMetadata holds data about the product in Radom. +type ItemRadomMetadata struct { + ProductID string `json:"product_id"` +} + +// Metadata returns the contents of m as a map for datastore.Metadata. +// +// It can be called when m is nil. +func (m *ItemRadomMetadata) Metadata() map[string]interface{} { + if m == nil { + return nil + } + + result := make(map[string]interface{}) + if m.ProductID != "" { + result["radom_product_id"] = m.ProductID + } + + return result +} + // EnsureEqualPaymentMethods checks if the methods list equals the incoming list. // // This operation may change both slices due to sorting. diff --git a/services/skus/model/model_pvt_test.go b/services/skus/model/model_pvt_test.go index 84bb72e23..9de70bc54 100644 --- a/services/skus/model/model_pvt_test.go +++ b/services/skus/model/model_pvt_test.go @@ -26,7 +26,7 @@ func TestAddURLParam(t *testing.T) { exp tcExpected } - // Don't test for invalid inputs due to url.Parse's tolerance. + // Don't test for invalid inputs due to url.Parses tolerance. tests := []testCase{ { name: "empty", diff --git a/services/skus/model/model_test.go b/services/skus/model/model_test.go index 94320f73b..f569a7a13 100644 --- a/services/skus/model/model_test.go +++ b/services/skus/model/model_test.go @@ -1,12 +1,9 @@ package model_test import ( - "context" "encoding/json" "errors" - "net" "testing" - "time" "github.com/lib/pq" uuid "github.com/satori/go.uuid" @@ -14,9 +11,7 @@ import ( should "github.com/stretchr/testify/assert" must "github.com/stretchr/testify/require" - "github.com/brave-intl/bat-go/libs/clients/radom" "github.com/brave-intl/bat-go/libs/datastore" - "github.com/brave-intl/bat-go/services/skus/model" ) @@ -139,164 +134,6 @@ func TestEnsureEqualPaymentMethods(t *testing.T) { } } -func TestOrder_CreateRadomCheckoutSessionWithTime(t *testing.T) { - type tcGiven struct { - order *model.Order - client *radom.MockClient - saddr string - expiresAt time.Time - } - type tcExpected struct { - val model.CreateCheckoutSessionResponse - err error - } - type testCase struct { - name string - given tcGiven - exp tcExpected - } - tests := []testCase{ - { - name: "no_items", - given: tcGiven{ - order: &model.Order{}, - client: &radom.MockClient{}, - }, - exp: tcExpected{ - err: model.ErrInvalidOrderNoItems, - }, - }, - - { - name: "no_radom_success_uri", - given: tcGiven{ - order: &model.Order{ - Items: []model.OrderItem{{}}, - }, - client: &radom.MockClient{}, - }, - exp: tcExpected{ - err: model.ErrInvalidOrderNoSuccessURL, - }, - }, - - { - name: "no_radom_cancel_uri", - given: tcGiven{ - order: &model.Order{ - Items: []model.OrderItem{ - { - Metadata: datastore.Metadata{ - "radom_success_uri": "something", - }, - }, - }, - }, - client: &radom.MockClient{}, - }, - exp: tcExpected{ - err: model.ErrInvalidOrderNoCancelURL, - }, - }, - - { - name: "no_radom_product_id", - given: tcGiven{ - order: &model.Order{ - Items: []model.OrderItem{ - { - Metadata: datastore.Metadata{ - "radom_success_uri": "something_success", - "radom_cancel_uri": "something_cancel", - }, - }, - }, - }, - client: &radom.MockClient{}, - }, - exp: tcExpected{ - err: model.ErrInvalidOrderNoProductID, - }, - }, - - { - name: "client_error", - given: tcGiven{ - order: &model.Order{ - Items: []model.OrderItem{ - { - Metadata: datastore.Metadata{ - "radom_success_uri": "something_success", - "radom_cancel_uri": "something_cancel", - "radom_product_id": "something_id", - }, - }, - }, - }, - client: &radom.MockClient{ - FnCreateCheckoutSession: func(ctx context.Context, req *radom.CheckoutSessionRequest) (*radom.CheckoutSessionResponse, error) { - return nil, net.ErrClosed - }, - }, - }, - exp: tcExpected{ - err: net.ErrClosed, - }, - }, - - { - name: "client_success", - given: tcGiven{ - order: &model.Order{ - Items: []model.OrderItem{ - { - Metadata: datastore.Metadata{ - "radom_success_uri": "something_success", - "radom_cancel_uri": "something_cancel", - "radom_product_id": "something_id", - }, - }, - }, - }, - client: &radom.MockClient{ - FnCreateCheckoutSession: func(ctx context.Context, req *radom.CheckoutSessionRequest) (*radom.CheckoutSessionResponse, error) { - result := &radom.CheckoutSessionResponse{ - SessionID: "session_id", - SessionURL: "session_url", - } - - return result, nil - }, - }, - }, - exp: tcExpected{ - val: model.CreateCheckoutSessionResponse{ - SessionID: "session_id", - }, - }, - }, - } - - for i := range tests { - tc := tests[i] - t.Run(tc.name, func(t *testing.T) { - ctx := context.TODO() - act, err := tc.given.order.CreateRadomCheckoutSessionWithTime( - ctx, - tc.given.client, - tc.given.saddr, - tc.given.expiresAt, - ) - must.Equal(t, true, errors.Is(err, tc.exp.err)) - - if tc.exp.err != nil { - return - } - should.Equal(t, tc.exp.val, act) - }) - } -} - func TestOrderItemRequestNew_Unmarshal(t *testing.T) { type testCase struct { name string @@ -520,6 +357,61 @@ func TestOrderStripeMetadata(t *testing.T) { } } +func TestOrderRadomMetadata(t *testing.T) { + type tcGiven struct { + data *model.OrderRadomMetadata + oid string + } + + type tcExpected struct { + surl string + curl string + } + + type testCase struct { + name string + given tcGiven + exp tcExpected + } + + tests := []testCase{ + { + name: "empty", + }, + + { + name: "add_id", + given: tcGiven{ + data: &model.OrderRadomMetadata{ + SuccessURI: "https://example.com/success", + CancelURI: "https://example.com/cancel", + }, + oid: "some_order_id", + }, + exp: tcExpected{ + surl: "https://example.com/success?order_id=some_order_id", + curl: "https://example.com/cancel?order_id=some_order_id", + }, + }, + } + + for i := range tests { + tc := tests[i] + + t.Run(tc.name, func(t *testing.T) { + act1, err := tc.given.data.SuccessURL(tc.given.oid) + must.Equal(t, nil, err) + + should.Equal(t, tc.exp.surl, act1) + + act2, err := tc.given.data.CancelURL(tc.given.oid) + must.Equal(t, nil, err) + + should.Equal(t, tc.exp.curl, act2) + }) + } +} + func TestOrderItemList_TotalCost(t *testing.T) { type testCase struct { name string @@ -1143,6 +1035,71 @@ func TestOrder_ShouldSetTrialDays(t *testing.T) { } } +func TestOrderItemRequestNew_Metadata(t *testing.T) { + type tcGiven struct { + oreq model.OrderItemRequestNew + } + + type tcExpected struct { + metadata map[string]interface{} + } + + type testCase struct { + name string + given tcGiven + exp tcExpected + } + + tests := []testCase{ + { + name: "nil", + }, + + { + name: "stripe_metadata", + given: tcGiven{ + oreq: model.OrderItemRequestNew{ + StripeMetadata: &model.ItemStripeMetadata{ + ProductID: "product_1", + ItemID: "item_1", + }, + }, + }, + exp: tcExpected{ + metadata: map[string]interface{}{ + "stripe_product_id": "product_1", + "stripe_item_id": "item_1", + }, + }, + }, + + { + name: "radom_metadata", + given: tcGiven{ + oreq: model.OrderItemRequestNew{ + RadomMetadata: &model.ItemRadomMetadata{ + ProductID: "product_1", + }, + }, + }, + exp: tcExpected{ + metadata: map[string]interface{}{ + "radom_product_id": "product_1", + }, + }, + }, + } + + for i := range tests { + tc := tests[i] + + t.Run(tc.name, func(t *testing.T) { + actual := tc.given.oreq.Metadata() + should.Equal(t, tc.exp.metadata, actual) + }) + } +} + func mustDecimalFromString(v string) decimal.Decimal { result, err := decimal.NewFromString(v) if err != nil { diff --git a/services/skus/radom/radom.go b/services/skus/radom/radom.go new file mode 100644 index 000000000..970dbf974 --- /dev/null +++ b/services/skus/radom/radom.go @@ -0,0 +1,71 @@ +package radom + +import ( + "context" + "net/http" + + "github.com/brave-intl/bat-go/libs/clients" +) + +type Client struct { + client *clients.SimpleHTTPClient +} + +func New(srvURL, authToken, proxyAddr string) (*Client, error) { + cl, err := clients.NewWithProxy("radom", srvURL, authToken, proxyAddr) + if err != nil { + return nil, err + } + + return &Client{client: cl}, nil +} + +type Gateway struct { + Managed Managed `json:"managed"` +} + +type Managed struct { + Methods []Method `json:"methods"` +} + +type Method struct { + Network string `json:"network"` + Token string `json:"token"` +} + +type LineItem struct { + ProductID string `json:"productId"` +} + +type Metadata struct { + Key string `json:"key"` + Value string `json:"value"` +} + +type CheckoutSessionRequest struct { + LineItems []LineItem `json:"lineItems"` + Gateway Gateway `json:"gateway"` + SuccessURL string `json:"successUrl"` + CancelURL string `json:"cancelUrl"` + Metadata []Metadata `json:"metadata"` + ExpiresAt int64 `json:"expiresAt"` // in unix seconds +} + +type CheckoutSessionResponse struct { + SessionID string `json:"checkoutSessionId"` + SessionURL string `json:"checkoutSessionUrl"` +} + +func (c *Client) CreateCheckoutSession(ctx context.Context, creq CheckoutSessionRequest) (CheckoutSessionResponse, error) { + req, err := c.client.NewRequest(ctx, http.MethodPost, "/checkout_session", creq, nil) + if err != nil { + return CheckoutSessionResponse{}, err + } + + var resp CheckoutSessionResponse + if _, err := c.client.Do(ctx, req, &resp); err != nil { + return CheckoutSessionResponse{}, err + } + + return resp, nil +} diff --git a/services/skus/service.go b/services/skus/service.go index af5207b76..a34ea952e 100644 --- a/services/skus/service.go +++ b/services/skus/service.go @@ -32,7 +32,6 @@ import ( "github.com/brave-intl/bat-go/libs/backoff" "github.com/brave-intl/bat-go/libs/clients/cbr" "github.com/brave-intl/bat-go/libs/clients/gemini" - "github.com/brave-intl/bat-go/libs/clients/radom" appctx "github.com/brave-intl/bat-go/libs/context" "github.com/brave-intl/bat-go/libs/cryptography" "github.com/brave-intl/bat-go/libs/datastore" @@ -45,6 +44,7 @@ import ( walletutils "github.com/brave-intl/bat-go/libs/wallet" "github.com/brave-intl/bat-go/libs/wallet/provider" "github.com/brave-intl/bat-go/libs/wallet/provider/uphold" + "github.com/brave-intl/bat-go/services/skus/radom" "github.com/brave-intl/bat-go/services/wallet" "github.com/brave-intl/bat-go/services/skus/model" @@ -85,7 +85,6 @@ const ( errSetRetryAfter = model.Error("set retry-after") errClosingResource = model.Error("error closing resource") - errInvalidRadomURL = model.Error("service: invalid radom url") errGeminiClientNotConfigured = model.Error("service: gemini client not configured") errLegacyOutboxNotFound = model.Error("error no order credentials have been submitted for signing") errWrongOrderIDForRequestID = model.Error("signed request order id does not belong to request id") @@ -120,7 +119,10 @@ type gpsMessageAuthenticator interface { authenticate(ctx context.Context, token string) error } -// Service contains datastore +type radomClient interface { + CreateCheckoutSession(ctx context.Context, creq radom.CheckoutSessionRequest) (radom.CheckoutSessionResponse, error) +} + type Service struct { orderRepo orderStoreSvc orderItemRepo orderItemStore @@ -131,20 +133,21 @@ type Service struct { // TODO: Eventually remove it. Datastore Datastore - wallet *wallet.Service - cbClient cbr.Client - geminiClient gemini.Client - geminiConf *gemini.Conf - scClient *client.API - codecs map[string]*goavro.Codec - kafkaWriter *kafka.Writer - kafkaDialer *kafka.Dialer - jobs []srv.Job - pauseVoteUntil time.Time - pauseVoteUntilMu sync.RWMutex - retry backoff.RetryFunc - radomClient *radom.InstrumentedClient - radomSellerAddress string + wallet *wallet.Service + cbClient cbr.Client + geminiClient gemini.Client + geminiConf *gemini.Conf + scClient *client.API + codecs map[string]*goavro.Codec + kafkaWriter *kafka.Writer + kafkaDialer *kafka.Dialer + jobs []srv.Job + pauseVoteUntil time.Time + pauseVoteUntilMu sync.RWMutex + retry backoff.RetryFunc + + radomClient radomClient + radomGateway radom.Gateway vendorReceiptValid vendorReceiptValidator gpsAuth gpsMessageAuthenticator @@ -206,45 +209,53 @@ func InitService( payHistRepo orderPayHistoryStore, tlv2repo tlv2Store, ) (*Service, error) { - sublogger := logging.Logger(ctx, "payments").With().Str("func", "InitService").Logger() + lg := logging.Logger(ctx, "payments").With().Str("func", "InitService").Logger() // setup stripe if exists in context and enabled scClient := &client.API{} if enabled, ok := ctx.Value(appctx.StripeEnabledCTXKey).(bool); ok && enabled { - sublogger.Debug().Msg("stripe enabled") + lg.Debug().Msg("stripe enabled") var err error stripe.Key, err = appctx.GetStringFromContext(ctx, appctx.StripeSecretCTXKey) if err != nil { - sublogger.Panic().Err(err).Msg("failed to get Stripe secret from context, and Stripe enabled") + lg.Panic().Err(err).Msg("failed to get Stripe secret from context, and Stripe enabled") } // initialize stripe client scClient.Init(stripe.Key, nil) } - var ( - radomSellerAddress string - radomClient *radom.InstrumentedClient - ) + env, err := appctx.GetStringFromContext(ctx, appctx.EnvironmentCTXKey) + if err != nil { + return nil, err + } - // setup radom if exists in context and enabled - if enabled, ok := ctx.Value(appctx.RadomEnabledCTXKey).(bool); ok && enabled { - sublogger.Debug().Msg("radom enabled") - var err error - radomSellerAddress, err = appctx.GetStringFromContext(ctx, appctx.RadomSellerAddressCTXKey) - if err != nil { - sublogger.Error().Err(err).Msg("failed to get Stripe secret from context, and Stripe enabled") - return nil, err - } + var radomCl *radom.Client + var radomGateway radom.Gateway + if enabled, _ := strconv.ParseBool(os.Getenv("RADOM_ENABLED")); enabled { srvURL := os.Getenv("RADOM_SERVER") if srvURL == "" { - return nil, errInvalidRadomURL + return nil, model.Error("skus: invalid radom url") + } + + authToken := os.Getenv("RADOM_SECRET") + if authToken == "" { + return nil, model.Error("skus: radom secret not found") } - rdSecret := os.Getenv("RADOM_SECRET") proxyAddr := os.Getenv("HTTP_PROXY") + if proxyAddr == "" { + return nil, model.Error("skus: radom http proxy value not found") + } + + var err error - radomClient, err = radom.NewInstrumented(srvURL, rdSecret, proxyAddr) + radomCl, err = radom.New(srvURL, authToken, proxyAddr) + if err != nil { + return nil, err + } + + radomGateway, err = newRadomGateway(env) if err != nil { return nil, err } @@ -293,11 +304,6 @@ func InitService( return nil, err } - env, err := appctx.GetStringFromContext(ctx, appctx.EnvironmentCTXKey) - if err != nil { - return nil, err - } - idv, err := idtoken.NewValidator(ctx, option.WithTelemetryDisabled()) if err != nil { return nil, err @@ -305,22 +311,22 @@ func InitService( disabled, _ := strconv.ParseBool(os.Getenv("GCP_PUSH_NOTIFICATION")) if disabled { - sublogger.Warn().Msg("gcp push notification is disabled") + lg.Warn().Msg("gcp push notification is disabled") } aud := os.Getenv("GCP_PUSH_SUBSCRIPTION_AUDIENCE") if aud == "" { - sublogger.Warn().Msg("gcp push subscription audience is empty") + lg.Warn().Msg("gcp push subscription audience is empty") } iss := os.Getenv("GCP_CERT_ISSUER") if iss == "" { - sublogger.Warn().Msg("gcp cert issuer is empty") + lg.Warn().Msg("gcp cert issuer is empty") } sa := os.Getenv("GCP_PUSH_SUBSCRIPTION_SERVICE_ACCOUNT") if sa == "" { - sublogger.Warn().Msg("gcp push subscription service account is empty") + lg.Warn().Msg("gcp push subscription service account is empty") } gpsCfg := gpsValidatorConfig{ @@ -339,15 +345,16 @@ func InitService( Datastore: datastore, - wallet: walletService, - geminiClient: geminiClient, - geminiConf: geminiConf, - cbClient: cbClient, - scClient: scClient, - pauseVoteUntilMu: sync.RWMutex{}, - retry: backoff.Retry, - radomClient: radomClient, - radomSellerAddress: radomSellerAddress, + wallet: walletService, + geminiClient: geminiClient, + geminiConf: geminiConf, + cbClient: cbClient, + scClient: scClient, + pauseVoteUntilMu: sync.RWMutex{}, + retry: backoff.Retry, + + radomClient: radomCl, + radomGateway: radomGateway, vendorReceiptValid: rcptValidator, gpsAuth: newGPSNtfAuthenticator(gpsCfg, idv), @@ -357,7 +364,6 @@ func InitService( newItemReqSet: newOrderItemReqNewMobileSet(env), } - // setup runnable jobs service.jobs = []srv.Job{ { Func: service.RunNextVoteDrainJob, @@ -1955,7 +1961,7 @@ func (s *Service) createOrderIssuers(ctx context.Context, dbi sqlx.QueryerContex return numIntervals, nil } -func (s *Service) createStripeSessID(ctx context.Context, req *model.CreateOrderRequestNew, order *model.Order) (string, error) { +func (s *Service) createStripeSessID(_ context.Context, req *model.CreateOrderRequestNew, order *model.Order) (string, error) { oid := order.ID.String() surl, err := req.StripeMetadata.SuccessURL(oid) @@ -1976,14 +1982,72 @@ func (s *Service) createStripeSessID(ctx context.Context, req *model.CreateOrder return sess.SessionID, nil } -// TODO: Refactor the Radom-related logic. func (s *Service) createRadomSessID(ctx context.Context, req *model.CreateOrderRequestNew, order *model.Order) (string, error) { - sess, err := order.CreateRadomCheckoutSession(ctx, s.radomClient, s.radomSellerAddress) + oid := order.ID.String() + + surl, err := req.RadomMetadata.SuccessURL(oid) if err != nil { return "", err } - return sess.SessionID, nil + curl, err := req.RadomMetadata.CancelURL(oid) + if err != nil { + return "", err + } + + items, err := orderItemsToLineItems(order.Items) + if err != nil { + return "", err + } + + reqx := radom.CheckoutSessionRequest{ + LineItems: items, + Gateway: s.radomGateway, + SuccessURL: surl, + CancelURL: curl, + Metadata: []radom.Metadata{ + { + Key: "brave_order_id", + Value: oid, + }, + }, + ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), + } + + resp, err := s.radomClient.CreateCheckoutSession(ctx, reqx) + if err != nil { + return "", err + } + + return resp.SessionID, nil +} + +const ( + errRadomProductIDNotFound = model.Error("product id not found in metadata") + errRadomInvalidType = model.Error("invalid type for product id") +) + +func orderItemsToLineItems(orderItems []model.OrderItem) ([]radom.LineItem, error) { + lineItems := make([]radom.LineItem, 0, len(orderItems)) + for i := range orderItems { + m, ok := orderItems[i].Metadata["radom_product_id"] + if !ok { + return nil, errRadomProductIDNotFound + } + + pid, ok := m.(string) + if !ok { + return nil, errRadomInvalidType + } + + li := radom.LineItem{ + ProductID: pid, + } + + lineItems = append(lineItems, li) + } + + return lineItems, nil } func (s *Service) redeemBlindedCred(ctx context.Context, w http.ResponseWriter, kind string, cred *cbr.CredentialRedemption) *handlers.AppError { @@ -2410,7 +2474,7 @@ func createOrderItem(req *model.OrderItemRequestNew) (*model.OrderItem, error) { }, }, Quantity: req.Quantity, - Metadata: req.StripeMetadata.Metadata(), + Metadata: req.Metadata(), Subtotal: req.Price.Mul(decimal.NewFromInt(int64(req.Quantity))), IssuerConfig: &model.IssuerConfig{ Buffer: req.TokenBufferOrDefault(), @@ -2488,3 +2552,41 @@ func shouldUpdateOrderStripeSubID(ord *model.Order, subID string) bool { return false } + +func newRadomGateway(env string) (radom.Gateway, error) { + switch env { + case "development", "staging": + return radom.Gateway{ + Managed: radom.Managed{ + Methods: []radom.Method{ + { + Network: "SepoliaTestnet", + Token: "0x5D684d37922dAf7Aa2013E65A22880a11C475e25", + }, + { + Network: "PolygonTestnet", + Token: "0xd445cAAbb9eA6685D3A512439256866563a16E93", + }, + }, + }, + }, nil + case "production": + return radom.Gateway{ + Managed: radom.Managed{ + Methods: []radom.Method{ + { + Network: "Polygon", + Token: "0x3cef98bb43d732e2f285ee605a8158cde967d219", + }, + + { + Network: "Ethereum", + Token: "0x0d8775f648430679a709e98d2b0cb6250d2887ef", + }, + }, + }, + }, nil + default: + return radom.Gateway{}, model.Error("skus: unknown environment") + } +} diff --git a/services/skus/service_nonint_test.go b/services/skus/service_nonint_test.go index 9c43f5c2a..11daee509 100644 --- a/services/skus/service_nonint_test.go +++ b/services/skus/service_nonint_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/awa/go-iap/appstore" + "github.com/brave-intl/bat-go/services/skus/radom" "github.com/jmoiron/sqlx" "github.com/lib/pq" uuid "github.com/satori/go.uuid" @@ -2695,6 +2696,365 @@ func TestShouldUpdateOrderStripeSubID(t *testing.T) { } } +func TestService_createRadomSessID(t *testing.T) { + type tcExpected struct { + sessionID string + mustErr must.ErrorAssertionFunc + } + + type tcGiven struct { + req *model.CreateOrderRequestNew + order *model.Order + radCl radomClient + } + + type testCase struct { + name string + given tcGiven + exp tcExpected + } + + tests := []testCase{ + { + name: "err_success_url", + given: tcGiven{ + order: &model.Order{}, + req: &model.CreateOrderRequestNew{ + RadomMetadata: &model.OrderRadomMetadata{ + SuccessURI: "https://invalid%.com", + }, + }, + }, + exp: tcExpected{ + mustErr: func(t must.TestingT, err error, i ...interface{}) { + must.NotNil(t, err) + }, + }, + }, + + { + name: "err_cancel_url", + given: tcGiven{ + order: &model.Order{}, + req: &model.CreateOrderRequestNew{ + RadomMetadata: &model.OrderRadomMetadata{ + SuccessURI: "https://example.com", + CancelURI: "https://invalid%.com", + }, + }, + }, + exp: tcExpected{ + mustErr: func(t must.TestingT, err error, i ...interface{}) { + must.NotNil(t, err) + }, + }, + }, + + { + name: "err_order_items_to_line_items", + given: tcGiven{ + order: &model.Order{ + Items: []OrderItem{{}}, + }, + req: &model.CreateOrderRequestNew{ + RadomMetadata: &model.OrderRadomMetadata{ + SuccessURI: "https://example.com", + CancelURI: "https://example.com", + }, + }, + }, + exp: tcExpected{ + mustErr: func(t must.TestingT, err error, i ...interface{}) { + must.ErrorIs(t, err, errRadomProductIDNotFound) + }, + }, + }, + + { + name: "err_create_checkout_session", + given: tcGiven{ + order: &model.Order{ + Items: []OrderItem{ + { + Metadata: datastore.Metadata{ + "radom_product_id": "product_1", + }, + }, + }, + }, + req: &model.CreateOrderRequestNew{ + RadomMetadata: &model.OrderRadomMetadata{ + SuccessURI: "https://example.com", + CancelURI: "https://example.com", + }, + }, + radCl: &mockRadomClient{ + fnCreateCheckoutSession: func(ctx context.Context, creq radom.CheckoutSessionRequest) (radom.CheckoutSessionResponse, error) { + return radom.CheckoutSessionResponse{}, model.Error("some error") + }, + }, + }, + exp: tcExpected{ + mustErr: func(t must.TestingT, err error, i ...interface{}) { + must.ErrorIs(t, err, model.Error("some error")) + }, + }, + }, + + { + name: "success", + given: tcGiven{ + order: &model.Order{ + Items: []OrderItem{ + { + Metadata: datastore.Metadata{ + "radom_product_id": "product_1", + }, + }, + }, + }, + req: &model.CreateOrderRequestNew{ + RadomMetadata: &model.OrderRadomMetadata{ + SuccessURI: "https://example.com", + CancelURI: "https://example.com", + }, + }, + radCl: &mockRadomClient{ + fnCreateCheckoutSession: func(ctx context.Context, creq radom.CheckoutSessionRequest) (radom.CheckoutSessionResponse, error) { + return radom.CheckoutSessionResponse{SessionID: "session_id"}, nil + }, + }, + }, + exp: tcExpected{ + sessionID: "session_id", + mustErr: func(t must.TestingT, err error, i ...interface{}) { + must.Nil(t, err) + }, + }, + }, + } + + for i := range tests { + tc := tests[i] + + t.Run(tc.name, func(t *testing.T) { + s := Service{ + radomClient: tc.given.radCl, + } + ctx := context.Background() + + actual, err := s.createRadomSessID(ctx, tc.given.req, tc.given.order) + tc.exp.mustErr(t, err) + + should.Equal(t, tc.exp.sessionID, actual) + }) + } +} + +func Test_orderItemsToLineItems(t *testing.T) { + type tcGiven struct { + orderItems []model.OrderItem + } + + type tcExpected struct { + lineItems []radom.LineItem + err error + } + + type testCase struct { + name string + given tcGiven + exp tcExpected + } + + tests := []testCase{ + { + name: "product_id_not_found", + given: tcGiven{ + orderItems: []OrderItem{{}}, + }, + exp: tcExpected{ + err: errRadomProductIDNotFound, + }, + }, + + { + name: "product_id_invalid_type", + given: tcGiven{ + orderItems: []OrderItem{{ + Metadata: map[string]interface{}{ + "radom_product_id": 12345, + }, + }}, + }, + exp: tcExpected{ + err: errRadomInvalidType, + }, + }, + + { + name: "success", + given: tcGiven{ + orderItems: []OrderItem{ + { + Metadata: map[string]interface{}{ + "radom_product_id": "product_1", + }, + }, + { + Metadata: map[string]interface{}{ + "radom_product_id": "product_2", + }, + }, + }, + }, + exp: tcExpected{ + lineItems: []radom.LineItem{ + { + ProductID: "product_1", + }, + { + ProductID: "product_2", + }, + }, + }, + }, + } + + for i := range tests { + tc := tests[i] + + t.Run(tc.name, func(t *testing.T) { + actual, err := orderItemsToLineItems(tc.given.orderItems) + must.Equal(t, tc.exp.err, err) + + should.Equal(t, tc.exp.lineItems, actual) + }) + } +} + +func Test_newRadomGateway(t *testing.T) { + type tcGiven struct { + env string + } + + type tcExpected struct { + gateway radom.Gateway + err error + } + + type testCase struct { + name string + given tcGiven + exp tcExpected + } + + tests := []testCase{ + { + name: "development", + given: tcGiven{ + "development", + }, + exp: tcExpected{ + gateway: radom.Gateway{ + Managed: radom.Managed{ + Methods: []radom.Method{ + { + Network: "SepoliaTestnet", + Token: "0x5D684d37922dAf7Aa2013E65A22880a11C475e25", + }, + { + Network: "PolygonTestnet", + Token: "0xd445cAAbb9eA6685D3A512439256866563a16E93", + }, + }, + }, + }, + }, + }, + + { + name: "staging", + given: tcGiven{ + "staging", + }, + exp: tcExpected{ + gateway: radom.Gateway{ + Managed: radom.Managed{ + Methods: []radom.Method{ + { + Network: "SepoliaTestnet", + Token: "0x5D684d37922dAf7Aa2013E65A22880a11C475e25", + }, + { + Network: "PolygonTestnet", + Token: "0xd445cAAbb9eA6685D3A512439256866563a16E93", + }, + }, + }, + }, + }, + }, + + { + name: "production", + given: tcGiven{ + "production", + }, + exp: tcExpected{ + gateway: radom.Gateway{ + Managed: radom.Managed{ + Methods: []radom.Method{ + { + Network: "Polygon", + Token: "0x3cef98bb43d732e2f285ee605a8158cde967d219", + }, + + { + Network: "Ethereum", + Token: "0x0d8775f648430679a709e98d2b0cb6250d2887ef", + }, + }, + }, + }, + }, + }, + + { + name: "unknown", + given: tcGiven{ + "random_env", + }, + exp: tcExpected{ + err: model.Error("skus: unknown environment"), + }, + }, + } + + for i := range tests { + tc := tests[i] + + t.Run(tc.name, func(t *testing.T) { + actual, err := newRadomGateway(tc.given.env) + must.Equal(t, tc.exp.err, err) + + should.Equal(t, tc.exp.gateway, actual) + }) + } +} + +type mockRadomClient struct { + fnCreateCheckoutSession func(ctx context.Context, creq radom.CheckoutSessionRequest) (radom.CheckoutSessionResponse, error) +} + +func (m *mockRadomClient) CreateCheckoutSession(ctx context.Context, creq radom.CheckoutSessionRequest) (radom.CheckoutSessionResponse, error) { + if m.fnCreateCheckoutSession == nil { + return radom.CheckoutSessionResponse{}, nil + } + + return m.fnCreateCheckoutSession(ctx, creq) +} + type mockPaidOrderCreator struct { fnCreateOrderPremium func(ctx context.Context, req *model.CreateOrderRequestNew, ordNew *model.OrderNew, items []model.OrderItem) (*model.Order, error) fnRenewOrderWithExpPaidTime func(ctx context.Context, id uuid.UUID, expt, paidt time.Time) error diff --git a/services/skus/service_test.go b/services/skus/service_test.go index 683203ba0..2502e875e 100644 --- a/services/skus/service_test.go +++ b/services/skus/service_test.go @@ -8,14 +8,14 @@ import ( "testing" "time" + "github.com/brave-intl/bat-go/libs/datastore" "github.com/brave-intl/bat-go/libs/ptr" + timeutils "github.com/brave-intl/bat-go/libs/time" + "github.com/brave-intl/bat-go/services/skus/model" + "github.com/shopspring/decimal" should "github.com/stretchr/testify/assert" must "github.com/stretchr/testify/require" - - "github.com/brave-intl/bat-go/libs/datastore" - timeutils "github.com/brave-intl/bat-go/libs/time" - "github.com/brave-intl/bat-go/services/skus/model" ) func TestCredChunkFn(t *testing.T) {