satellite/payments: Implement coupon codes
Full path: satellite/{payments,console},web/satellite * Adds the ability to apply coupon codes from the billing page in the satellite UI. * Flag for coupon code UI is split into two flags - one for the billing page and one for the signup page. This commit implements the first, but not the second. * Update the Stripe dependency to v72, which is necessary to use Stripe's promo code functionality. Change-Id: I19d9815c48205932bef68d87d5cb0b000498fa70
This commit is contained in:
parent
dae6ed7d03
commit
149f6f2626
2
go.mod
2
go.mod
@ -37,7 +37,7 @@ require (
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.7.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/stripe/stripe-go v70.15.0+incompatible
|
||||
github.com/stripe/stripe-go/v72 v72.51.0
|
||||
github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3
|
||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
|
||||
github.com/zeebo/assert v1.3.0
|
||||
|
5
go.sum
5
go.sum
@ -507,8 +507,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stripe/stripe-go v70.15.0+incompatible h1:hNML7M1zx8RgtepEMlxyu/FpVPrP7KZm1gPFQquJQvM=
|
||||
github.com/stripe/stripe-go v70.15.0+incompatible/go.mod h1:A1dQZmO/QypXmsL0T8axYZkSN/uA/T/A64pfKdBAMiY=
|
||||
github.com/stripe/stripe-go/v72 v72.51.0 h1:scXELorHW1SnAfARThO1QayscOsfEIoIAUy0yxoTqxY=
|
||||
github.com/stripe/stripe-go/v72 v72.51.0/go.mod h1:QwqJQtduHubZht9mek5sds9CtQcKFdsykV9ZepRWwo0=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
@ -660,6 +660,7 @@ golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
|
@ -5,6 +5,7 @@ package consoleapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@ -308,6 +309,27 @@ func (p *Payments) TokenDeposit(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyCouponCode applies a coupon code to the user's account.
|
||||
func (p *Payments) ApplyCouponCode(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
// limit the size of the body to prevent excessive memory usage
|
||||
bodyBytes, err := ioutil.ReadAll(io.LimitReader(r.Body, 1*1024*1024))
|
||||
if err != nil {
|
||||
p.serveJSONError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
couponCode := string(bodyBytes)
|
||||
|
||||
err = p.service.Payments().ApplyCouponCode(ctx, couponCode)
|
||||
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) {
|
||||
serveJSONError(p.log, w, status, err)
|
||||
|
@ -84,7 +84,8 @@ type Config struct {
|
||||
BetaSatelliteFeedbackURL string `help:"url link for for beta satellite feedback" default:""`
|
||||
BetaSatelliteSupportURL string `help:"url link for for beta satellite support" default:""`
|
||||
DocumentationURL string `help:"url link to documentation" default:"https://docs.storj.io/"`
|
||||
CouponCodeUIEnabled bool `help:"indicates if user is allowed to add coupon codes to account" default:"false"`
|
||||
CouponCodeBillingUIEnabled bool `help:"indicates if user is allowed to add coupon codes to account from billing" default:"false"`
|
||||
CouponCodeSignupUIEnabled bool `help:"indicates if user is allowed to add coupon codes to account from signup" default:"false"`
|
||||
FileBrowserFlowDisabled bool `help:"indicates if file browser flow is disabled" default:"false"`
|
||||
CSPEnabled bool `help:"indicates if Content Security Policy is enabled" devDefault:"false" releaseDefault:"true"`
|
||||
LinksharingURL string `help:"url link for linksharing requests" default:"https://link.us1.storjshare.io"`
|
||||
@ -245,6 +246,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, mail
|
||||
paymentsRouter.HandleFunc("/account", paymentController.SetupAccount).Methods(http.MethodPost)
|
||||
paymentsRouter.HandleFunc("/billing-history", paymentController.BillingHistory).Methods(http.MethodGet)
|
||||
paymentsRouter.HandleFunc("/tokens/deposit", paymentController.TokenDeposit).Methods(http.MethodPost)
|
||||
paymentsRouter.HandleFunc("/couponcodes/apply", paymentController.ApplyCouponCode).Methods(http.MethodPatch)
|
||||
|
||||
bucketsController := consoleapi.NewBuckets(logger, service)
|
||||
bucketsRouter := router.PathPrefix("/api/v0/buckets").Subrouter()
|
||||
@ -356,7 +358,8 @@ func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) {
|
||||
BetaSatelliteFeedbackURL string
|
||||
BetaSatelliteSupportURL string
|
||||
DocumentationURL string
|
||||
CouponCodeUIEnabled bool
|
||||
CouponCodeBillingUIEnabled bool
|
||||
CouponCodeSignupUIEnabled bool
|
||||
FileBrowserFlowDisabled bool
|
||||
LinksharingURL string
|
||||
PathwayOverviewEnabled bool
|
||||
@ -381,7 +384,8 @@ func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data.BetaSatelliteFeedbackURL = server.config.BetaSatelliteFeedbackURL
|
||||
data.BetaSatelliteSupportURL = server.config.BetaSatelliteSupportURL
|
||||
data.DocumentationURL = server.config.DocumentationURL
|
||||
data.CouponCodeUIEnabled = server.config.CouponCodeUIEnabled
|
||||
data.CouponCodeBillingUIEnabled = server.config.CouponCodeBillingUIEnabled
|
||||
data.CouponCodeSignupUIEnabled = server.config.CouponCodeSignupUIEnabled
|
||||
data.FileBrowserFlowDisabled = server.config.FileBrowserFlowDisabled
|
||||
data.LinksharingURL = server.config.LinksharingURL
|
||||
data.PathwayOverviewEnabled = server.config.PathwayOverviewEnabled
|
||||
|
@ -15,7 +15,7 @@ import (
|
||||
|
||||
"github.com/spacemonkeygo/monkit/v3"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stripe/stripe-go"
|
||||
"github.com/stripe/stripe-go/v72"
|
||||
"github.com/zeebo/errs"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@ -529,6 +529,17 @@ func (paymentService PaymentsService) checkProjectInvoicingStatus(ctx context.Co
|
||||
return paymentService.service.accounts.CheckProjectInvoicingStatus(ctx, projectID)
|
||||
}
|
||||
|
||||
// ApplyCouponCode applies a coupon code to a Stripe customer.
|
||||
func (paymentService PaymentsService) ApplyCouponCode(ctx context.Context, couponCode string) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "apply coupon code")
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
return paymentService.service.accounts.Coupons().ApplyCouponCode(ctx, auth.User.ID, couponCode)
|
||||
}
|
||||
|
||||
// AddPromotionalCoupon creates new coupon for specified user.
|
||||
func (paymentService PaymentsService) AddPromotionalCoupon(ctx context.Context, userID uuid.UUID) (err error) {
|
||||
defer mon.Task()(&ctx, userID)(&err)
|
||||
|
@ -33,6 +33,9 @@ type Coupons interface {
|
||||
// a project, payment method and do not have a promotional coupon yet.
|
||||
// And updates project limits to selected size.
|
||||
PopulatePromotionalCoupons(ctx context.Context, duration *int, amount int64, projectLimit memory.Size) error
|
||||
|
||||
// ApplyCouponCode attempts to apply a coupon code to the user.
|
||||
ApplyCouponCode(ctx context.Context, userID uuid.UUID, couponCode string) error
|
||||
}
|
||||
|
||||
// Coupon is an entity that adds some funds to Accounts balance for some fixed period.
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/stripe/stripe-go"
|
||||
"github.com/stripe/stripe-go/v72"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/storj/satellite/payments"
|
||||
|
@ -4,13 +4,14 @@
|
||||
package stripecoinpayments
|
||||
|
||||
import (
|
||||
"github.com/stripe/stripe-go"
|
||||
"github.com/stripe/stripe-go/charge"
|
||||
"github.com/stripe/stripe-go/client"
|
||||
"github.com/stripe/stripe-go/customerbalancetransaction"
|
||||
"github.com/stripe/stripe-go/invoice"
|
||||
"github.com/stripe/stripe-go/invoiceitem"
|
||||
"github.com/stripe/stripe-go/paymentmethod"
|
||||
"github.com/stripe/stripe-go/v72"
|
||||
"github.com/stripe/stripe-go/v72/charge"
|
||||
"github.com/stripe/stripe-go/v72/client"
|
||||
"github.com/stripe/stripe-go/v72/customerbalancetransaction"
|
||||
"github.com/stripe/stripe-go/v72/invoice"
|
||||
"github.com/stripe/stripe-go/v72/invoiceitem"
|
||||
"github.com/stripe/stripe-go/v72/paymentmethod"
|
||||
"github.com/stripe/stripe-go/v72/promotioncode"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@ -22,6 +23,7 @@ type StripeClient interface {
|
||||
InvoiceItems() StripeInvoiceItems
|
||||
CustomerBalanceTransactions() StripeCustomerBalanceTransactions
|
||||
Charges() StripeCharges
|
||||
PromoCodes() StripePromoCodes
|
||||
}
|
||||
|
||||
// StripeCustomers Stripe Customers interface.
|
||||
@ -57,6 +59,11 @@ type StripeCharges interface {
|
||||
List(listParams *stripe.ChargeListParams) *charge.Iter
|
||||
}
|
||||
|
||||
// StripePromoCodes is the Stripe PromoCodes interface.
|
||||
type StripePromoCodes interface {
|
||||
List(params *stripe.PromotionCodeListParams) *promotioncode.Iter
|
||||
}
|
||||
|
||||
// StripeCustomerBalanceTransactions Stripe CustomerBalanceTransactions interface.
|
||||
type StripeCustomerBalanceTransactions interface {
|
||||
New(params *stripe.CustomerBalanceTransactionParams) (*stripe.CustomerBalanceTransaction, error)
|
||||
@ -91,6 +98,10 @@ func (s *stripeClient) Charges() StripeCharges {
|
||||
return s.client.Charges
|
||||
}
|
||||
|
||||
func (s *stripeClient) PromoCodes() StripePromoCodes {
|
||||
return s.client.PromotionCodes
|
||||
}
|
||||
|
||||
// NewStripeClient creates Stripe client from configuration.
|
||||
func NewStripeClient(log *zap.Logger, config Config) StripeClient {
|
||||
backendConfig := &stripe.BackendConfig{
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/stripe/stripe-go"
|
||||
"github.com/stripe/stripe-go/v72"
|
||||
|
||||
"storj.io/common/memory"
|
||||
"storj.io/common/uuid"
|
||||
@ -210,3 +210,32 @@ func (coupons *coupons) AddPromotionalCoupon(ctx context.Context, userID uuid.UU
|
||||
|
||||
return Error.Wrap(coupons.service.db.Coupons().PopulatePromotionalCoupons(ctx, []uuid.UUID{userID}, couponDuration, coupons.service.CouponValue, coupons.service.CouponProjectLimit))
|
||||
}
|
||||
|
||||
// ApplyCouponCode attempts to apply a coupon code to the user via Stripe.
|
||||
func (coupons *coupons) ApplyCouponCode(ctx context.Context, userID uuid.UUID, couponCode string) (err error) {
|
||||
defer mon.Task()(&ctx, userID, couponCode)(&err)
|
||||
|
||||
promoCodeIter := coupons.service.stripeClient.PromoCodes().List(&stripe.PromotionCodeListParams{
|
||||
Code: stripe.String(couponCode),
|
||||
})
|
||||
if !promoCodeIter.Next() {
|
||||
return Error.New("Invalid coupon code")
|
||||
}
|
||||
promoCode := promoCodeIter.PromotionCode()
|
||||
|
||||
customerID, err := coupons.service.db.Customers().GetCustomerID(ctx, userID)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
params := &stripe.CustomerParams{
|
||||
PromotionCode: stripe.String(promoCode.ID),
|
||||
}
|
||||
|
||||
_, err = coupons.service.stripeClient.Customers().Update(customerID, params)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ package stripecoinpayments
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stripe/stripe-go"
|
||||
"github.com/stripe/stripe-go/v72"
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/stripe/stripe-go"
|
||||
"github.com/stripe/stripe-go/v72"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/storj/satellite/payments"
|
||||
|
@ -15,7 +15,7 @@ import (
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/spacemonkeygo/monkit/v3"
|
||||
"github.com/stripe/stripe-go"
|
||||
"github.com/stripe/stripe-go/v72"
|
||||
"github.com/zeebo/errs"
|
||||
"go.uber.org/zap"
|
||||
|
||||
|
@ -10,13 +10,14 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stripe/stripe-go"
|
||||
"github.com/stripe/stripe-go/charge"
|
||||
"github.com/stripe/stripe-go/customerbalancetransaction"
|
||||
"github.com/stripe/stripe-go/form"
|
||||
"github.com/stripe/stripe-go/invoice"
|
||||
"github.com/stripe/stripe-go/invoiceitem"
|
||||
"github.com/stripe/stripe-go/paymentmethod"
|
||||
"github.com/stripe/stripe-go/v72"
|
||||
"github.com/stripe/stripe-go/v72/charge"
|
||||
"github.com/stripe/stripe-go/v72/customerbalancetransaction"
|
||||
"github.com/stripe/stripe-go/v72/form"
|
||||
"github.com/stripe/stripe-go/v72/invoice"
|
||||
"github.com/stripe/stripe-go/v72/invoiceitem"
|
||||
"github.com/stripe/stripe-go/v72/paymentmethod"
|
||||
"github.com/stripe/stripe-go/v72/promotioncode"
|
||||
|
||||
"storj.io/common/storj"
|
||||
"storj.io/common/testrand"
|
||||
@ -48,6 +49,7 @@ type mockStripeState struct {
|
||||
invoiceItems *mockInvoiceItems
|
||||
customerBalanceTransactions *mockCustomerBalanceTransactions
|
||||
charges *mockCharges
|
||||
promoCodes *mockPromoCodes
|
||||
}
|
||||
|
||||
type mockStripeClient struct {
|
||||
@ -56,6 +58,11 @@ type mockStripeClient struct {
|
||||
*mockStripeState
|
||||
}
|
||||
|
||||
// mockEmptyQuery is a query with no results.
|
||||
var mockEmptyQuery = stripe.Query(func(*stripe.Params, *form.Values) ([]interface{}, stripe.ListContainer, error) {
|
||||
return nil, newListContainer(&stripe.ListMeta{}), nil
|
||||
})
|
||||
|
||||
// NewStripeMock creates new Stripe client mock.
|
||||
//
|
||||
// A new mock is returned for each unique id. If this method is called multiple
|
||||
@ -78,6 +85,7 @@ func NewStripeMock(id storj.NodeID, customersDB CustomersDB, usersDB console.Use
|
||||
invoiceItems: &mockInvoiceItems{},
|
||||
customerBalanceTransactions: newMockCustomerBalanceTransactions(),
|
||||
charges: &mockCharges{},
|
||||
promoCodes: &mockPromoCodes{},
|
||||
}
|
||||
mocks.m[id] = state
|
||||
}
|
||||
@ -120,6 +128,10 @@ func (m *mockStripeClient) Charges() StripeCharges {
|
||||
return m.charges
|
||||
}
|
||||
|
||||
func (m *mockStripeClient) PromoCodes() StripePromoCodes {
|
||||
return m.promoCodes
|
||||
}
|
||||
|
||||
type mockCustomers struct {
|
||||
customersDB CustomersDB
|
||||
usersDB console.Users
|
||||
@ -261,12 +273,27 @@ func newMockPaymentMethods() *mockPaymentMethods {
|
||||
}
|
||||
}
|
||||
|
||||
// listContainer implements Stripe's ListContainer interface.
|
||||
type listContainer struct {
|
||||
listMeta *stripe.ListMeta
|
||||
}
|
||||
|
||||
func newListContainer(meta *stripe.ListMeta) *listContainer {
|
||||
return &listContainer{listMeta: meta}
|
||||
}
|
||||
|
||||
func (c *listContainer) GetListMeta() *stripe.ListMeta {
|
||||
return c.listMeta
|
||||
}
|
||||
|
||||
func (m *mockPaymentMethods) List(listParams *stripe.PaymentMethodListParams) *paymentmethod.Iter {
|
||||
listMeta := stripe.ListMeta{
|
||||
listMeta := &stripe.ListMeta{
|
||||
HasMore: false,
|
||||
TotalCount: uint32(len(m.attached)),
|
||||
}
|
||||
return &paymentmethod.Iter{Iter: stripe.GetIter(nil, func(*stripe.Params, *form.Values) ([]interface{}, stripe.ListMeta, error) {
|
||||
lc := newListContainer(listMeta)
|
||||
|
||||
query := stripe.Query(func(*stripe.Params, *form.Values) ([]interface{}, stripe.ListContainer, error) {
|
||||
mocks.Lock()
|
||||
defer mocks.Unlock()
|
||||
|
||||
@ -280,8 +307,9 @@ func (m *mockPaymentMethods) List(listParams *stripe.PaymentMethodListParams) *p
|
||||
ret[i] = v
|
||||
}
|
||||
|
||||
return ret, listMeta, nil
|
||||
})}
|
||||
return ret, lc, nil
|
||||
})
|
||||
return &paymentmethod.Iter{Iter: stripe.GetIter(nil, query)}
|
||||
}
|
||||
|
||||
func (m *mockPaymentMethods) New(params *stripe.PaymentMethodParams) (*stripe.PaymentMethod, error) {
|
||||
@ -355,7 +383,7 @@ func (m *mockInvoices) New(params *stripe.InvoiceParams) (*stripe.Invoice, error
|
||||
}
|
||||
|
||||
func (m *mockInvoices) List(listParams *stripe.InvoiceListParams) *invoice.Iter {
|
||||
return &invoice.Iter{Iter: &stripe.Iter{}}
|
||||
return &invoice.Iter{Iter: stripe.GetIter(listParams, mockEmptyQuery)}
|
||||
}
|
||||
|
||||
func (m *mockInvoices) FinalizeInvoice(id string, params *stripe.InvoiceFinalizeParams) (*stripe.Invoice, error) {
|
||||
@ -370,7 +398,7 @@ func (m *mockInvoiceItems) New(params *stripe.InvoiceItemParams) (*stripe.Invoic
|
||||
}
|
||||
|
||||
func (m *mockInvoiceItems) List(listParams *stripe.InvoiceItemListParams) *invoiceitem.Iter {
|
||||
return &invoiceitem.Iter{Iter: &stripe.Iter{}}
|
||||
return &invoiceitem.Iter{Iter: stripe.GetIter(listParams, mockEmptyQuery)}
|
||||
}
|
||||
|
||||
type mockCustomerBalanceTransactions struct {
|
||||
@ -404,7 +432,7 @@ func (m *mockCustomerBalanceTransactions) List(listParams *stripe.CustomerBalanc
|
||||
mocks.Lock()
|
||||
defer mocks.Unlock()
|
||||
|
||||
return &customerbalancetransaction.Iter{Iter: stripe.GetIter(listParams, func(p *stripe.Params, b *form.Values) ([]interface{}, stripe.ListMeta, error) {
|
||||
query := stripe.Query(func(p *stripe.Params, b *form.Values) ([]interface{}, stripe.ListContainer, error) {
|
||||
txs := m.transactions[*listParams.Customer]
|
||||
ret := make([]interface{}, len(txs))
|
||||
|
||||
@ -412,17 +440,49 @@ func (m *mockCustomerBalanceTransactions) List(listParams *stripe.CustomerBalanc
|
||||
ret[i] = v
|
||||
}
|
||||
|
||||
listMeta := stripe.ListMeta{
|
||||
listMeta := &stripe.ListMeta{
|
||||
TotalCount: uint32(len(txs)),
|
||||
}
|
||||
|
||||
return ret, listMeta, nil
|
||||
})}
|
||||
lc := newListContainer(listMeta)
|
||||
|
||||
return ret, lc, nil
|
||||
})
|
||||
|
||||
return &customerbalancetransaction.Iter{Iter: stripe.GetIter(listParams, query)}
|
||||
}
|
||||
|
||||
type mockCharges struct {
|
||||
}
|
||||
|
||||
func (m *mockCharges) List(listParams *stripe.ChargeListParams) *charge.Iter {
|
||||
return &charge.Iter{Iter: &stripe.Iter{}}
|
||||
return &charge.Iter{Iter: stripe.GetIter(listParams, mockEmptyQuery)}
|
||||
}
|
||||
|
||||
type mockPromoCodes struct {
|
||||
promoCodes map[string][]*stripe.PromotionCode
|
||||
}
|
||||
|
||||
func (m *mockPromoCodes) List(params *stripe.PromotionCodeListParams) *promotioncode.Iter {
|
||||
mocks.Lock()
|
||||
defer mocks.Unlock()
|
||||
|
||||
query := stripe.Query(func(p *stripe.Params, b *form.Values) ([]interface{}, stripe.ListContainer, error) {
|
||||
promoCodes := m.promoCodes[*params.Code]
|
||||
ret := make([]interface{}, len(promoCodes))
|
||||
|
||||
for i, v := range promoCodes {
|
||||
ret[i] = v
|
||||
}
|
||||
|
||||
listMeta := &stripe.ListMeta{
|
||||
TotalCount: uint32(len(promoCodes)),
|
||||
}
|
||||
|
||||
lc := newListContainer(listMeta)
|
||||
|
||||
return ret, lc, nil
|
||||
})
|
||||
|
||||
return &promotioncode.Iter{Iter: stripe.GetIter(params, query)}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/stripe/stripe-go"
|
||||
"github.com/stripe/stripe-go/v72"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stripe/stripe-go"
|
||||
"github.com/stripe/stripe-go/v72"
|
||||
|
||||
"storj.io/common/memory"
|
||||
"storj.io/common/testcontext"
|
||||
|
@ -13,7 +13,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stripe/stripe-go"
|
||||
"github.com/stripe/stripe-go/v72"
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
"storj.io/common/errs2"
|
||||
|
7
scripts/testdata/satellite-config.yaml.lock
vendored
7
scripts/testdata/satellite-config.yaml.lock
vendored
@ -88,8 +88,11 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
|
||||
# url link to contacts page
|
||||
# console.contact-info-url: https://forum.storj.io
|
||||
|
||||
# indicates if user is allowed to add coupon codes to account
|
||||
# console.coupon-code-ui-enabled: false
|
||||
# indicates if user is allowed to add coupon codes to account from billing
|
||||
# console.coupon-code-billing-ui-enabled: false
|
||||
|
||||
# indicates if user is allowed to add coupon codes to account from signup
|
||||
# console.coupon-code-signup-ui-enabled: false
|
||||
|
||||
# indicates if Content Security Policy is enabled
|
||||
# console.csp-enabled: true
|
||||
|
@ -16,7 +16,8 @@
|
||||
<meta name="beta-satellite-feedback-url" content="{{ .BetaSatelliteFeedbackURL }}">
|
||||
<meta name="beta-satellite-support-url" content="{{ .BetaSatelliteSupportURL }}">
|
||||
<meta name="documentation-url" content="{{ .DocumentationURL }}">
|
||||
<meta name="coupon-code-ui-enabled" content="{{ .CouponCodeUIEnabled }}">
|
||||
<meta name="coupon-code-billing-ui-enabled" content="{{ .CouponCodeBillingUIEnabled }}">
|
||||
<meta name="coupon-code-signup-ui-enabled" content="{{ .CouponCodeSignupUIEnabled }}">
|
||||
<meta name="file-browser-flow-disabled" content="{{ .FileBrowserFlowDisabled }}">
|
||||
<meta name="linksharing-url" content="{{ .LinksharingURL }}">
|
||||
<meta name="storage-tb-price" content="{{ .StorageTBPrice }}">
|
||||
|
@ -32,7 +32,8 @@ export default class App extends Vue {
|
||||
const satelliteName = MetaUtils.getMetaContent('satellite-name');
|
||||
const partneredSatellitesJson = JSON.parse(MetaUtils.getMetaContent('partnered-satellites'));
|
||||
const isBetaSatellite = MetaUtils.getMetaContent('is-beta-satellite');
|
||||
const couponCodeUIEnabled = MetaUtils.getMetaContent('coupon-code-ui-enabled');
|
||||
const couponCodeBillingUIEnabled = MetaUtils.getMetaContent('coupon-code-billing-ui-enabled');
|
||||
const couponCodeSignupUIEnabled = MetaUtils.getMetaContent('coupon-code-signup-ui-enabled');
|
||||
|
||||
if (satelliteName) {
|
||||
this.$store.dispatch(APP_STATE_ACTIONS.SET_SATELLITE_NAME, satelliteName);
|
||||
@ -55,8 +56,11 @@ export default class App extends Vue {
|
||||
this.$store.dispatch(APP_STATE_ACTIONS.SET_SATELLITE_STATUS, isBetaSatellite === 'true');
|
||||
}
|
||||
|
||||
if (couponCodeUIEnabled) {
|
||||
this.$store.dispatch(APP_STATE_ACTIONS.SET_COUPON_CODE_UI_STATUS, couponCodeUIEnabled === 'true');
|
||||
if (couponCodeBillingUIEnabled) {
|
||||
this.$store.dispatch(APP_STATE_ACTIONS.SET_COUPON_CODE_BILLING_UI_STATUS, couponCodeBillingUIEnabled === 'true');
|
||||
}
|
||||
if (couponCodeSignupUIEnabled) {
|
||||
this.$store.dispatch(APP_STATE_ACTIONS.SET_COUPON_CODE_SIGNUP_UI_STATUS, couponCodeSignupUIEnabled === 'true');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -253,4 +253,24 @@ export class PaymentsHttpApi implements PaymentsApi {
|
||||
|
||||
return new TokenDeposit(result.amount, result.address, result.link);
|
||||
}
|
||||
|
||||
/**
|
||||
* applyCouponCode applies a coupon code.
|
||||
*
|
||||
* @param couponCode
|
||||
* @throws Error
|
||||
*/
|
||||
public async applyCouponCode(couponCode: string): Promise<void> {
|
||||
const path = `${this.ROOT_PATH}/couponcodes/apply`;
|
||||
const response = await this.client.patch(path, couponCode);
|
||||
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
throw new ErrorUnauthorized();
|
||||
}
|
||||
throw new Error(`Can not apply coupon code "${couponCode}"`);
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
width="174px"
|
||||
:on-press="onCreateClick"
|
||||
label="Add Coupon Code"
|
||||
v-if="couponCodeUIEnabled"
|
||||
v-if="couponCodeBillingUIEnabled"
|
||||
/>
|
||||
<div class="credit-history__container">
|
||||
<div class="credit-history__content">
|
||||
@ -93,10 +93,10 @@ export default class CreditsHistory extends Vue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if coupon code ui is enabled
|
||||
* Indicates if coupon code ui is enabled on the billing page.
|
||||
*/
|
||||
public get couponCodeUIEnabled(): boolean {
|
||||
return this.$store.state.appStateModule.couponCodeUIEnabled;
|
||||
public get couponCodeBillingUIEnabled(): boolean {
|
||||
return this.$store.state.appStateModule.couponCodeBillingUIEnabled;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -22,19 +22,11 @@
|
||||
class="add-coupon__check"
|
||||
v-if="isCodeValid"
|
||||
/>
|
||||
<VButton
|
||||
label="Validate"
|
||||
class="add-coupon__claim-button"
|
||||
width="120px"
|
||||
height="32px"
|
||||
v-if="!isCodeValid"
|
||||
:on-press="onValidationCheckClick"
|
||||
/>
|
||||
</div>
|
||||
<ValidationMessage
|
||||
class="add-coupon__valid-message"
|
||||
successMessage="Get 50GB free and start using Storj DCS today."
|
||||
errorMessage="Invalid code. Please Try again"
|
||||
successMessage="Successfully applied coupon code."
|
||||
:errorMessage="errorMessage"
|
||||
:isValid="isCodeValid"
|
||||
:showMessage="showValidationMessage"
|
||||
/>
|
||||
@ -44,6 +36,7 @@
|
||||
width="85%"
|
||||
height="44px"
|
||||
v-if="!isSignupView"
|
||||
:on-press="onApplyClick"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@ -59,6 +52,11 @@ import CloseIcon from '@/../static/images/common/closeCross.svg';
|
||||
import CheckIcon from '@/../static/images/common/validCheck.svg';
|
||||
|
||||
import { RouteConfig } from '@/router';
|
||||
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
|
||||
|
||||
const {
|
||||
APPLY_COUPON_CODE,
|
||||
} = PAYMENTS_ACTIONS;
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@ -70,11 +68,12 @@ import { RouteConfig } from '@/router';
|
||||
},
|
||||
})
|
||||
export default class AddCouponCodeInput extends Vue {
|
||||
@Prop({default: false})
|
||||
private showValidationMessage = false;
|
||||
private errorMessage = '';
|
||||
private isCodeValid = false;
|
||||
|
||||
@Prop({default: false})
|
||||
protected readonly isCodeValid: boolean;
|
||||
@Prop({default: false})
|
||||
protected readonly showValidationMessage: boolean;
|
||||
private couponCode = '';
|
||||
|
||||
/**
|
||||
* Signup view requires some unque styling and element text.
|
||||
@ -91,13 +90,27 @@ export default class AddCouponCodeInput extends Vue {
|
||||
return this.isSignupView ? 'Add Coupon' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if coupon code is valid
|
||||
*/
|
||||
public onValidationCheckClick(): boolean {
|
||||
return true;
|
||||
public setCouponCode(value: string): void {
|
||||
this.couponCode = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if coupon code is valid
|
||||
*/
|
||||
public async onApplyClick() {
|
||||
try {
|
||||
await this.$store.dispatch(APPLY_COUPON_CODE, this.couponCode);
|
||||
} catch (error) {
|
||||
|
||||
this.errorMessage = error.message;
|
||||
this.isCodeValid = false;
|
||||
this.showValidationMessage = true;
|
||||
|
||||
return;
|
||||
}
|
||||
this.isCodeValid = true;
|
||||
this.showValidationMessage = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -118,15 +131,6 @@ export default class AddCouponCodeInput extends Vue {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__claim-button {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 8px;
|
||||
font-size: 14px;
|
||||
padding: 2px 0;
|
||||
z-index: 23;
|
||||
}
|
||||
|
||||
&__valid-message {
|
||||
position: relative;
|
||||
top: 15px;
|
||||
@ -138,10 +142,10 @@ export default class AddCouponCodeInput extends Vue {
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
bottom: 40px;
|
||||
background: #93a1af;
|
||||
background: #2683ff;
|
||||
|
||||
&:hover {
|
||||
background: darken(#93a1af, 10%);
|
||||
background: #0059d0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,9 +40,8 @@ export default class ValidationMessage extends Vue {
|
||||
&__wrapper {
|
||||
box-sizing: border-box;
|
||||
border-radius: 6px;
|
||||
height: 52px;
|
||||
width: 100%;
|
||||
padding-left: 25px;
|
||||
padding: 12px 25px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
|
@ -32,7 +32,8 @@ export const appStateModule = {
|
||||
satelliteName: '',
|
||||
partneredSatellites: new Array<PartneredSatellite>(),
|
||||
isBetaSatellite: false,
|
||||
couponCodeUIEnabled: false,
|
||||
couponCodeBillingUIEnabled: false,
|
||||
couponCodeSignupUIEnabled: false,
|
||||
},
|
||||
mutations: {
|
||||
// Mutation changing add projectMembers members popup visibility
|
||||
@ -126,8 +127,11 @@ export const appStateModule = {
|
||||
[APP_STATE_MUTATIONS.SET_SATELLITE_STATUS](state: any, isBetaSatellite: boolean): void {
|
||||
state.isBetaSatellite = isBetaSatellite;
|
||||
},
|
||||
[APP_STATE_MUTATIONS.SET_COUPON_CODE_UI_STATUS](state: any, couponCodeUIEnabled: boolean): void {
|
||||
state.couponCodeUIEnabled = couponCodeUIEnabled;
|
||||
[APP_STATE_MUTATIONS.SET_COUPON_CODE_BILLING_UI_STATUS](state: any, couponCodeBillingUIEnabled: boolean): void {
|
||||
state.couponCodeBillingUIEnabled = couponCodeBillingUIEnabled;
|
||||
},
|
||||
[APP_STATE_MUTATIONS.SET_COUPON_CODE_SIGNUP_UI_STATUS](state: any, couponCodeSignupUIEnabled: boolean): void {
|
||||
state.couponCodeSignupUIEnabled = couponCodeSignupUIEnabled;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
@ -261,8 +265,11 @@ export const appStateModule = {
|
||||
[APP_STATE_ACTIONS.SET_SATELLITE_STATUS]: function ({commit}: any, isBetaSatellite: boolean): void {
|
||||
commit(APP_STATE_MUTATIONS.SET_SATELLITE_STATUS, isBetaSatellite);
|
||||
},
|
||||
[APP_STATE_ACTIONS.SET_COUPON_CODE_UI_STATUS]: function ({commit}: any, couponCodeUIEnabled: boolean): void {
|
||||
commit(APP_STATE_MUTATIONS.SET_COUPON_CODE_UI_STATUS, couponCodeUIEnabled);
|
||||
[APP_STATE_ACTIONS.SET_COUPON_CODE_BILLING_UI_STATUS]: function ({commit}: any, couponCodeBillingUIEnabled: boolean): void {
|
||||
commit(APP_STATE_MUTATIONS.SET_COUPON_CODE_BILLING_UI_STATUS, couponCodeBillingUIEnabled);
|
||||
},
|
||||
[APP_STATE_ACTIONS.SET_COUPON_CODE_SIGNUP_UI_STATUS]: function ({commit}: any, couponCodeSignupUIEnabled: boolean): void {
|
||||
commit(APP_STATE_MUTATIONS.SET_COUPON_CODE_SIGNUP_UI_STATUS, couponCodeSignupUIEnabled);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -45,6 +45,7 @@ export const PAYMENTS_ACTIONS = {
|
||||
GET_PROJECT_USAGE_AND_CHARGES: 'getProjectUsageAndCharges',
|
||||
GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP: 'getProjectUsageAndChargesCurrentRollup',
|
||||
GET_PROJECT_USAGE_AND_CHARGES_PREVIOUS_ROLLUP: 'getProjectUsageAndChargesPreviousRollup',
|
||||
APPLY_COUPON_CODE: 'applyCouponCode',
|
||||
};
|
||||
|
||||
const {
|
||||
@ -75,6 +76,7 @@ const {
|
||||
MAKE_TOKEN_DEPOSIT,
|
||||
GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP,
|
||||
GET_PROJECT_USAGE_AND_CHARGES_PREVIOUS_ROLLUP,
|
||||
APPLY_COUPON_CODE,
|
||||
} = PAYMENTS_ACTIONS;
|
||||
|
||||
export class PaymentsState {
|
||||
@ -252,6 +254,9 @@ export function makePaymentsModule(api: PaymentsApi): StoreModule<PaymentsState>
|
||||
commit(SET_PROJECT_USAGE_AND_CHARGES, usageAndCharges);
|
||||
commit(SET_PRICE_SUMMARY, usageAndCharges);
|
||||
},
|
||||
[APPLY_COUPON_CODE]: async function({commit}: any, code: string): Promise<void> {
|
||||
await api.applyCouponCode(code);
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
canUserCreateFirstProject: (state: PaymentsState): boolean => {
|
||||
|
@ -34,5 +34,6 @@ export const APP_STATE_MUTATIONS = {
|
||||
SET_SATELLITE_NAME: 'SET_SATELLITE_NAME',
|
||||
SET_PARTNERED_SATELLITES: 'SET_PARTNERED_SATELLITES',
|
||||
SET_SATELLITE_STATUS: 'SET_SATELLITE_STATUS',
|
||||
SET_COUPON_CODE_UI_STATUS: 'SET_COUPON_CODE_UI_STATUS',
|
||||
SET_COUPON_CODE_BILLING_UI_STATUS: 'SET_COUPON_CODE_BILLING_UI_STATUS',
|
||||
SET_COUPON_CODE_SIGNUP_UI_STATUS: 'SET_COUPON_CODE_SIGNUP_UI_STATUS',
|
||||
};
|
||||
|
@ -69,6 +69,14 @@ export interface PaymentsApi {
|
||||
* @throws Error
|
||||
*/
|
||||
makeTokenDeposit(amount: number): Promise<TokenDeposit>;
|
||||
|
||||
/**
|
||||
* applyCouponCode applies a coupon code.
|
||||
*
|
||||
* @param couponCode
|
||||
* @throws Error
|
||||
*/
|
||||
applyCouponCode(couponCode: string): Promise<void>;
|
||||
}
|
||||
|
||||
export class AccountBalance {
|
||||
|
@ -28,7 +28,8 @@ export const APP_STATE_ACTIONS = {
|
||||
SET_SATELLITE_NAME: 'SET_SATELLITE_NAME',
|
||||
SET_PARTNERED_SATELLITES: 'SET_PARTNERED_SATELLITES',
|
||||
SET_SATELLITE_STATUS: 'SET_SATELLITE_STATUS',
|
||||
SET_COUPON_CODE_UI_STATUS: 'SET_COUPON_CODE_UI_STATUS',
|
||||
SET_COUPON_CODE_BILLING_UI_STATUS: 'SET_COUPON_CODE_BILLING_UI_STATUS',
|
||||
SET_COUPON_CODE_SIGNUP_UI_STATUS: 'SET_COUPON_CODE_SIGNUP_UI_STATUS',
|
||||
};
|
||||
|
||||
export const NOTIFICATION_ACTIONS = {
|
||||
|
@ -151,7 +151,7 @@
|
||||
is-password="true"
|
||||
/>
|
||||
</div>
|
||||
<AddCouponCodeInput v-if="couponCodeUIEnabled" />
|
||||
<AddCouponCodeInput v-if="couponCodeSignupUIEnabled" />
|
||||
<div v-if="isBetaSatellite" class="register-area__input-area__container__warning">
|
||||
<div class="register-area__input-area__container__warning__header">
|
||||
<label class="container">
|
||||
@ -463,8 +463,8 @@ export default class RegisterArea extends Vue {
|
||||
/**
|
||||
* Indicates if coupon code ui is enabled
|
||||
*/
|
||||
public get couponCodeUIEnabled(): boolean {
|
||||
return this.$store.state.appStateModule.couponCodeUIEnabled;
|
||||
public get couponCodeSignupUIEnabled(): boolean {
|
||||
return this.$store.state.appStateModule.couponCodeSigunpUIEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -50,7 +50,7 @@ export class PaymentsMock implements PaymentsApi {
|
||||
return Promise.resolve(new TokenDeposit(amount, 'testAddress', 'testLink'));
|
||||
}
|
||||
|
||||
getPaywallStatus(userId: string): Promise<boolean> {
|
||||
applyCouponCode(code: string): Promise<void> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user