satellite/console/consoleweb: create purchase-package endpoint
Add new purchase-package endpoint to Server. The endpoint can be enabled or disabled by a new config, --console.pricing-packages-enabled. The purchase-package endpoint applies a coupon and adds and charges a credit card if user's useragent is a partner with a configured package plan. github issue: https://github.com/storj/storj-private/issues/125 Change-Id: I0d6ccccd6874ddba360c45f338fd1c44f95e135a
This commit is contained in:
parent
529e3674e4
commit
8842985571
@ -633,6 +633,7 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
|
||||
peer.Console.Listener,
|
||||
config.Payments.StripeCoinPayments.StripePublicKey,
|
||||
peer.URL(),
|
||||
config.Payments.PackagePlans,
|
||||
)
|
||||
|
||||
peer.Servers.Add(lifecycle.Item{
|
||||
|
@ -6,6 +6,7 @@ package consoleapi
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@ -20,6 +21,7 @@ import (
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/payments"
|
||||
"storj.io/storj/satellite/payments/billing"
|
||||
"storj.io/storj/satellite/payments/paymentsconfig"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -33,14 +35,16 @@ type Payments struct {
|
||||
log *zap.Logger
|
||||
service *console.Service
|
||||
accountFreezeService *console.AccountFreezeService
|
||||
packagePlans paymentsconfig.PackagePlans
|
||||
}
|
||||
|
||||
// NewPayments is a constructor for api payments controller.
|
||||
func NewPayments(log *zap.Logger, service *console.Service, accountFreezeService *console.AccountFreezeService) *Payments {
|
||||
func NewPayments(log *zap.Logger, service *console.Service, accountFreezeService *console.AccountFreezeService, packagePlans paymentsconfig.PackagePlans) *Payments {
|
||||
return &Payments{
|
||||
log: log,
|
||||
service: service,
|
||||
accountFreezeService: accountFreezeService,
|
||||
packagePlans: packagePlans,
|
||||
}
|
||||
}
|
||||
|
||||
@ -463,6 +467,56 @@ func (p *Payments) GetProjectUsagePriceModel(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
}
|
||||
|
||||
// PurchasePackage purchases one of the configured paymentsconfig.PackagePlans.
|
||||
func (p *Payments) PurchasePackage(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
p.serveJSONError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
token := string(bodyBytes)
|
||||
|
||||
u, err := console.GetUser(ctx)
|
||||
if err != nil {
|
||||
p.serveJSONError(w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
pkg, err := p.packagePlans.Get(u.UserAgent)
|
||||
if err != nil {
|
||||
p.serveJSONError(w, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = p.service.Payments().ApplyCoupon(ctx, pkg.CouponID)
|
||||
if err != nil {
|
||||
p.serveJSONError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
card, err := p.service.Payments().AddCreditCard(ctx, token)
|
||||
if err != nil {
|
||||
switch {
|
||||
case console.ErrUnauthorized.Has(err):
|
||||
p.serveJSONError(w, http.StatusUnauthorized, err)
|
||||
default:
|
||||
p.serveJSONError(w, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err = p.service.Payments().Purchase(ctx, pkg.Price, fmt.Sprintf("%s package plan", string(u.UserAgent)), card.ID)
|
||||
if err != nil {
|
||||
p.serveJSONError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// serveJSONError writes JSON error to response output stream.
|
||||
func (p *Payments) serveJSONError(w http.ResponseWriter, status int, err error) {
|
||||
web.ServeJSONError(p.log, w, status, err)
|
||||
|
110
satellite/console/consoleweb/consoleapi/payments_test.go
Normal file
110
satellite/console/consoleweb/consoleapi/payments_test.go
Normal file
@ -0,0 +1,110 @@
|
||||
// Copyright (C) 2023 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package consoleapi_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/testcontext"
|
||||
"storj.io/storj/private/testplanet"
|
||||
"storj.io/storj/satellite"
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/payments"
|
||||
"storj.io/storj/satellite/payments/stripecoinpayments"
|
||||
)
|
||||
|
||||
func Test_PurchasePackage(t *testing.T) {
|
||||
partner := "partner1"
|
||||
partner2 := "partner2"
|
||||
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
|
||||
Reconfigure: testplanet.Reconfigure{
|
||||
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
||||
config.Console.OpenRegistrationEnabled = true
|
||||
config.Console.RateLimit.Burst = 10
|
||||
config.Payments.StripeCoinPayments.StripeFreeTierCouponID = stripecoinpayments.MockCouponID1
|
||||
config.Payments.PackagePlans.Packages = map[string]payments.PackagePlan{
|
||||
partner: {CouponID: stripecoinpayments.MockCouponID2, Price: 1000},
|
||||
partner2: {CouponID: "invalidCouponID", Price: 1000},
|
||||
}
|
||||
},
|
||||
},
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
sat := planet.Satellites[0]
|
||||
validCardToken := "testValidCardToken"
|
||||
|
||||
tests := []struct {
|
||||
name, cardToken, partner string
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
// "unknownPartner" does not exist in paymentsconfig.PackagePlans
|
||||
"No matching package plan for partner", validCardToken, "unknownPartner",
|
||||
http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
// partner2's coupon ID configured above in Reconfigure does not exist in underlying
|
||||
// stipe mock coupons list.
|
||||
"Coupon doesn't exist", validCardToken, partner2,
|
||||
http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
"Add credit card fails", stripecoinpayments.TestPaymentMethodsNewFailure, partner,
|
||||
http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
"Purchase fails", stripecoinpayments.MockInvoicesPayFailure, partner,
|
||||
http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
"Success", validCardToken, partner,
|
||||
http.StatusOK,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
user, err := sat.AddUser(ctx, console.CreateUser{
|
||||
FullName: "test_name",
|
||||
ShortName: "",
|
||||
Email: fmt.Sprintf("test%d@storj.test", i),
|
||||
UserAgent: []byte(tt.partner),
|
||||
}, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
userCtx, err := sat.UserContext(ctx, user.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
tokenInfo, err := sat.API.Console.Service.Token(ctx, console.AuthUser{Email: user.Email, Password: user.FullName})
|
||||
require.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequestWithContext(userCtx, "POST", "http://"+planet.Satellites[0].API.Console.Listener.Addr().String()+"/api/v0/payments/purchase-package", strings.NewReader(tt.cardToken))
|
||||
require.NoError(t, err)
|
||||
|
||||
expire := time.Now().AddDate(0, 0, 1)
|
||||
cookie := http.Cookie{
|
||||
Name: "_tokenKey",
|
||||
Path: "/",
|
||||
Value: tokenInfo.Token.String(),
|
||||
Expires: expire,
|
||||
}
|
||||
|
||||
req.AddCookie(&cookie)
|
||||
|
||||
client := http.Client{}
|
||||
result, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expectedStatus, result.StatusCode)
|
||||
require.NoError(t, result.Body.Close())
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
@ -42,6 +42,7 @@ import (
|
||||
"storj.io/storj/satellite/console/consoleweb/consolewebauth"
|
||||
"storj.io/storj/satellite/mailservice"
|
||||
"storj.io/storj/satellite/oidc"
|
||||
"storj.io/storj/satellite/payments/paymentsconfig"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -98,6 +99,7 @@ type Config struct {
|
||||
OptionalSignupSuccessURL string `help:"optional url to external registration success page" default:""`
|
||||
HomepageURL string `help:"url link to storj.io homepage" default:"https://www.storj.io"`
|
||||
NativeTokenPaymentsEnabled bool `help:"indicates if storj native token payments system is enabled" default:"false"`
|
||||
PricingPackagesEnabled bool `help:"whether to allow purchasing pricing packages" default:"false" devDefault:"true"`
|
||||
|
||||
OauthCodeExpiry time.Duration `help:"how long oauth authorization codes are issued for" default:"10m"`
|
||||
OauthAccessTokenExpiry time.Duration `help:"how long oauth access tokens are issued for" default:"24h"`
|
||||
@ -132,6 +134,8 @@ type Server struct {
|
||||
|
||||
stripePublicKey string
|
||||
|
||||
packagePlans paymentsconfig.PackagePlans
|
||||
|
||||
schema graphql.Schema
|
||||
|
||||
templatesCache *templates
|
||||
@ -201,7 +205,7 @@ func (a *apiAuth) RemoveAuthCookie(w http.ResponseWriter) {
|
||||
}
|
||||
|
||||
// NewServer creates new instance of console server.
|
||||
func NewServer(logger *zap.Logger, config Config, service *console.Service, oidcService *oidc.Service, mailService *mailservice.Service, analytics *analytics.Service, abTesting *abtesting.Service, accountFreezeService *console.AccountFreezeService, listener net.Listener, stripePublicKey string, nodeURL storj.NodeURL) *Server {
|
||||
func NewServer(logger *zap.Logger, config Config, service *console.Service, oidcService *oidc.Service, mailService *mailservice.Service, analytics *analytics.Service, abTesting *abtesting.Service, accountFreezeService *console.AccountFreezeService, listener net.Listener, stripePublicKey string, nodeURL storj.NodeURL, packagePlans paymentsconfig.PackagePlans) *Server {
|
||||
server := Server{
|
||||
log: logger,
|
||||
config: config,
|
||||
@ -214,6 +218,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, oidc
|
||||
ipRateLimiter: web.NewIPRateLimiter(config.RateLimit, logger),
|
||||
userIDRateLimiter: NewUserIDRateLimiter(config.RateLimit, logger),
|
||||
nodeURL: nodeURL,
|
||||
packagePlans: packagePlans,
|
||||
}
|
||||
|
||||
logger.Debug("Starting Satellite UI.", zap.Stringer("Address", server.listener.Addr()))
|
||||
@ -299,7 +304,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, oidc
|
||||
abRouter.Handle("/hit/{action}", server.withAuth(http.HandlerFunc(abController.SendHit))).Methods(http.MethodPost)
|
||||
}
|
||||
|
||||
paymentController := consoleapi.NewPayments(logger, service, accountFreezeService)
|
||||
paymentController := consoleapi.NewPayments(logger, service, accountFreezeService, packagePlans)
|
||||
paymentsRouter := router.PathPrefix("/api/v0/payments").Subrouter()
|
||||
paymentsRouter.Use(server.withAuth)
|
||||
paymentsRouter.Handle("/cards", server.userIDRateLimiter.Limit(http.HandlerFunc(paymentController.AddCreditCard))).Methods(http.MethodPost)
|
||||
@ -316,6 +321,9 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, oidc
|
||||
paymentsRouter.Handle("/coupon/apply", server.userIDRateLimiter.Limit(http.HandlerFunc(paymentController.ApplyCouponCode))).Methods(http.MethodPatch)
|
||||
paymentsRouter.HandleFunc("/coupon", paymentController.GetCoupon).Methods(http.MethodGet)
|
||||
paymentsRouter.HandleFunc("/pricing", paymentController.GetProjectUsagePriceModel).Methods(http.MethodGet)
|
||||
if config.PricingPackagesEnabled {
|
||||
paymentsRouter.HandleFunc("/purchase-package", paymentController.PurchasePackage).Methods(http.MethodPost)
|
||||
}
|
||||
|
||||
bucketsController := consoleapi.NewBuckets(logger, service)
|
||||
bucketsRouter := router.PathPrefix("/api/v0/buckets").Subrouter()
|
||||
|
@ -3089,7 +3089,7 @@ func (payment Payments) WalletPayments(ctx context.Context) (_ WalletPayments, e
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Purchase makes a purchase of `price` amount with payment method with id of `paymentMethodID`.
|
||||
// Purchase makes a purchase of `price` amount with description of `desc` and payment method with id of `paymentMethodID`.
|
||||
func (payment Payments) Purchase(ctx context.Context, price int64, desc string, paymentMethodID string) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
|
@ -418,6 +418,8 @@ func (m *mockPaymentMethods) New(params *stripe.PaymentMethodParams) (*stripe.Pa
|
||||
return nil, &stripe.Error{}
|
||||
case TestPaymentMethodsAttachFailure:
|
||||
id = TestPaymentMethodsAttachFailure
|
||||
case MockInvoicesPayFailure:
|
||||
id = MockInvoicesPayFailure
|
||||
}
|
||||
}
|
||||
|
||||
|
3
scripts/testdata/satellite-config.yaml.lock
vendored
3
scripts/testdata/satellite-config.yaml.lock
vendored
@ -280,6 +280,9 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
|
||||
# indicates if the overview onboarding step should render with pathways
|
||||
# console.pathway-overview-enabled: true
|
||||
|
||||
# whether to allow purchasing pricing packages
|
||||
# console.pricing-packages-enabled: false
|
||||
|
||||
# url link to project limit increase request page
|
||||
# console.project-limits-increase-request-url: https://supportdcs.storj.io/hc/en-us/requests/new?ticket_form_id=360000683212
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user