satellite/payments/stripecoinpayments: add config for price overrides

This change adds configuration flags for defining partner-specific
project usage price overrides.

Resolves https://github.com/storj/storj-private/issues/61

Change-Id: Ia535ac22576382211d045f9ff2c9b983a07e86f3
This commit is contained in:
Jeremy Wharton 2022-12-01 01:40:52 -06:00 committed by Storj Robot
parent 99f4b2a26e
commit ba7d2c2dbe
12 changed files with 363 additions and 109 deletions

View File

@ -53,6 +53,16 @@ func setupPayments(log *zap.Logger, db satellite.DB) (*stripecoinpayments.Servic
stripeClient = stripecoinpayments.NewStripeClient(log, pc.StripeCoinPayments) stripeClient = stripecoinpayments.NewStripeClient(log, pc.StripeCoinPayments)
} }
prices, err := pc.UsagePrice.ToModel()
if err != nil {
return nil, err
}
priceOverrides, err := pc.UsagePriceOverrides.ToModels()
if err != nil {
return nil, err
}
return stripecoinpayments.NewService( return stripecoinpayments.NewService(
log.Named("payments.stripe:service"), log.Named("payments.stripe:service"),
stripeClient, stripeClient,
@ -62,9 +72,8 @@ func setupPayments(log *zap.Logger, db satellite.DB) (*stripecoinpayments.Servic
db.Billing(), db.Billing(),
db.Console().Projects(), db.Console().Projects(),
db.ProjectAccounting(), db.ProjectAccounting(),
pc.StorageTBPrice, prices,
pc.EgressTBPrice, priceOverrides,
pc.SegmentPrice,
pc.BonusRate) pc.BonusRate)
} }

View File

@ -145,6 +145,16 @@ func NewAdmin(log *zap.Logger, full *identity.FullIdentity, db DB, metabaseDB *m
stripeClient = stripecoinpayments.NewStripeClient(log, pc.StripeCoinPayments) stripeClient = stripecoinpayments.NewStripeClient(log, pc.StripeCoinPayments)
} }
prices, err := pc.UsagePrice.ToModel()
if err != nil {
return nil, errs.Combine(err, peer.Close())
}
priceOverrides, err := pc.UsagePriceOverrides.ToModels()
if err != nil {
return nil, errs.Combine(err, peer.Close())
}
peer.Payments.Service, err = stripecoinpayments.NewService( peer.Payments.Service, err = stripecoinpayments.NewService(
peer.Log.Named("payments.stripe:service"), peer.Log.Named("payments.stripe:service"),
stripeClient, stripeClient,
@ -154,9 +164,8 @@ func NewAdmin(log *zap.Logger, full *identity.FullIdentity, db DB, metabaseDB *m
peer.DB.Billing(), peer.DB.Billing(),
peer.DB.Console().Projects(), peer.DB.Console().Projects(),
peer.DB.ProjectAccounting(), peer.DB.ProjectAccounting(),
pc.StorageTBPrice, prices,
pc.EgressTBPrice, priceOverrides,
pc.SegmentPrice,
pc.BonusRate) pc.BonusRate)
if err != nil { if err != nil {

View File

@ -48,7 +48,6 @@ import (
"storj.io/storj/satellite/orders" "storj.io/storj/satellite/orders"
"storj.io/storj/satellite/overlay" "storj.io/storj/satellite/overlay"
"storj.io/storj/satellite/payments" "storj.io/storj/satellite/payments"
"storj.io/storj/satellite/payments/paymentsconfig"
"storj.io/storj/satellite/payments/storjscan" "storj.io/storj/satellite/payments/storjscan"
"storj.io/storj/satellite/payments/stripecoinpayments" "storj.io/storj/satellite/payments/stripecoinpayments"
"storj.io/storj/satellite/reputation" "storj.io/storj/satellite/reputation"
@ -512,6 +511,16 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
stripeClient = stripecoinpayments.NewStripeClient(log, pc.StripeCoinPayments) stripeClient = stripecoinpayments.NewStripeClient(log, pc.StripeCoinPayments)
} }
prices, err := pc.UsagePrice.ToModel()
if err != nil {
return nil, errs.Combine(err, peer.Close())
}
priceOverrides, err := pc.UsagePriceOverrides.ToModels()
if err != nil {
return nil, errs.Combine(err, peer.Close())
}
peer.Payments.StripeService, err = stripecoinpayments.NewService( peer.Payments.StripeService, err = stripecoinpayments.NewService(
peer.Log.Named("payments.stripe:service"), peer.Log.Named("payments.stripe:service"),
stripeClient, stripeClient,
@ -521,9 +530,8 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
peer.DB.Billing(), peer.DB.Billing(),
peer.DB.Console().Projects(), peer.DB.Console().Projects(),
peer.DB.ProjectAccounting(), peer.DB.ProjectAccounting(),
pc.StorageTBPrice, prices,
pc.EgressTBPrice, priceOverrides,
pc.SegmentPrice,
pc.BonusRate) pc.BonusRate)
if err != nil { if err != nil {
@ -591,12 +599,6 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
return nil, errs.Combine(err, peer.Close()) return nil, errs.Combine(err, peer.Close())
} }
pricing := paymentsconfig.PricingValues{
StorageTBPrice: config.Payments.StorageTBPrice,
EgressTBPrice: config.Payments.EgressTBPrice,
SegmentPrice: config.Payments.SegmentPrice,
}
peer.Console.Endpoint = consoleweb.NewServer( peer.Console.Endpoint = consoleweb.NewServer(
peer.Log.Named("console:endpoint"), peer.Log.Named("console:endpoint"),
consoleConfig, consoleConfig,
@ -608,7 +610,7 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
peer.ABTesting.Service, peer.ABTesting.Service,
peer.Console.Listener, peer.Console.Listener,
config.Payments.StripeCoinPayments.StripePublicKey, config.Payments.StripeCoinPayments.StripePublicKey,
pricing, config.Payments.UsagePrice,
peer.URL(), peer.URL(),
) )

View File

@ -73,11 +73,19 @@ func TestGraphqlMutation(t *testing.T) {
// TODO maybe switch this test to testplanet to avoid defining config and Stripe service // TODO maybe switch this test to testplanet to avoid defining config and Stripe service
pc := paymentsconfig.Config{ pc := paymentsconfig.Config{
StorageTBPrice: "10", UsagePrice: paymentsconfig.ProjectUsagePrice{
EgressTBPrice: "45", StorageTB: "10",
SegmentPrice: "0.0000022", EgressTB: "45",
Segment: "0.0000022",
},
} }
prices, err := pc.UsagePrice.ToModel()
require.NoError(t, err)
priceOverrides, err := pc.UsagePriceOverrides.ToModels()
require.NoError(t, err)
paymentsService, err := stripecoinpayments.NewService( paymentsService, err := stripecoinpayments.NewService(
log.Named("payments.stripe:service"), log.Named("payments.stripe:service"),
stripecoinpayments.NewStripeMock( stripecoinpayments.NewStripeMock(
@ -91,9 +99,8 @@ func TestGraphqlMutation(t *testing.T) {
db.Billing(), db.Billing(),
db.Console().Projects(), db.Console().Projects(),
db.ProjectAccounting(), db.ProjectAccounting(),
pc.StorageTBPrice, prices,
pc.EgressTBPrice, priceOverrides,
pc.SegmentPrice,
pc.BonusRate) pc.BonusRate)
require.NoError(t, err) require.NoError(t, err)

View File

@ -57,11 +57,19 @@ func TestGraphqlQuery(t *testing.T) {
// TODO maybe switch this test to testplanet to avoid defining config and Stripe service // TODO maybe switch this test to testplanet to avoid defining config and Stripe service
pc := paymentsconfig.Config{ pc := paymentsconfig.Config{
StorageTBPrice: "10", UsagePrice: paymentsconfig.ProjectUsagePrice{
EgressTBPrice: "45", StorageTB: "10",
SegmentPrice: "0.0000022", EgressTB: "45",
Segment: "0.0000022",
},
} }
prices, err := pc.UsagePrice.ToModel()
require.NoError(t, err)
priceOverrides, err := pc.UsagePriceOverrides.ToModels()
require.NoError(t, err)
paymentsService, err := stripecoinpayments.NewService( paymentsService, err := stripecoinpayments.NewService(
log.Named("payments.stripe:service"), log.Named("payments.stripe:service"),
stripecoinpayments.NewStripeMock( stripecoinpayments.NewStripeMock(
@ -75,9 +83,8 @@ func TestGraphqlQuery(t *testing.T) {
db.Billing(), db.Billing(),
db.Console().Projects(), db.Console().Projects(),
db.ProjectAccounting(), db.ProjectAccounting(),
pc.StorageTBPrice, prices,
pc.EgressTBPrice, priceOverrides,
pc.SegmentPrice,
pc.BonusRate) pc.BonusRate)
require.NoError(t, err) require.NoError(t, err)

View File

@ -135,7 +135,7 @@ type Server struct {
stripePublicKey string stripePublicKey string
pricing paymentsconfig.PricingValues usagePrice paymentsconfig.ProjectUsagePrice
schema graphql.Schema schema graphql.Schema
@ -206,7 +206,7 @@ func (a *apiAuth) RemoveAuthCookie(w http.ResponseWriter) {
} }
// NewServer creates new instance of console server. // NewServer creates new instance of console server.
func NewServer(logger *zap.Logger, config Config, service *console.Service, oidcService *oidc.Service, mailService *mailservice.Service, partners *rewards.PartnersService, analytics *analytics.Service, abTesting *abtesting.Service, listener net.Listener, stripePublicKey string, pricing paymentsconfig.PricingValues, nodeURL storj.NodeURL) *Server { func NewServer(logger *zap.Logger, config Config, service *console.Service, oidcService *oidc.Service, mailService *mailservice.Service, partners *rewards.PartnersService, analytics *analytics.Service, abTesting *abtesting.Service, listener net.Listener, stripePublicKey string, usagePrice paymentsconfig.ProjectUsagePrice, nodeURL storj.NodeURL) *Server {
server := Server{ server := Server{
log: logger, log: logger,
config: config, config: config,
@ -220,7 +220,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, oidc
ipRateLimiter: web.NewIPRateLimiter(config.RateLimit, logger), ipRateLimiter: web.NewIPRateLimiter(config.RateLimit, logger),
userIDRateLimiter: NewUserIDRateLimiter(config.RateLimit, logger), userIDRateLimiter: NewUserIDRateLimiter(config.RateLimit, logger),
nodeURL: nodeURL, nodeURL: nodeURL,
pricing: pricing, usagePrice: usagePrice,
} }
logger.Debug("Starting Satellite UI.", zap.Stringer("Address", server.listener.Addr())) logger.Debug("Starting Satellite UI.", zap.Stringer("Address", server.listener.Addr()))
@ -500,9 +500,9 @@ func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) {
data.PathwayOverviewEnabled = server.config.PathwayOverviewEnabled data.PathwayOverviewEnabled = server.config.PathwayOverviewEnabled
data.DefaultPaidStorageLimit = server.config.UsageLimits.Storage.Paid data.DefaultPaidStorageLimit = server.config.UsageLimits.Storage.Paid
data.DefaultPaidBandwidthLimit = server.config.UsageLimits.Bandwidth.Paid data.DefaultPaidBandwidthLimit = server.config.UsageLimits.Bandwidth.Paid
data.StorageTBPrice = server.pricing.StorageTBPrice data.StorageTBPrice = server.usagePrice.StorageTB
data.EgressTBPrice = server.pricing.EgressTBPrice data.EgressTBPrice = server.usagePrice.EgressTB
data.SegmentPrice = server.pricing.SegmentPrice data.SegmentPrice = server.usagePrice.Segment
data.RegistrationRecaptchaEnabled = server.config.Captcha.Registration.Recaptcha.Enabled data.RegistrationRecaptchaEnabled = server.config.Captcha.Registration.Recaptcha.Enabled
data.RegistrationRecaptchaSiteKey = server.config.Captcha.Registration.Recaptcha.SiteKey data.RegistrationRecaptchaSiteKey = server.config.Captcha.Registration.Recaptcha.SiteKey
data.RegistrationHcaptchaEnabled = server.config.Captcha.Registration.Hcaptcha.Enabled data.RegistrationHcaptchaEnabled = server.config.Captcha.Registration.Hcaptcha.Enabled

View File

@ -563,6 +563,16 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB,
stripeClient = stripecoinpayments.NewStripeClient(log, pc.StripeCoinPayments) stripeClient = stripecoinpayments.NewStripeClient(log, pc.StripeCoinPayments)
} }
prices, err := pc.UsagePrice.ToModel()
if err != nil {
return nil, errs.Combine(err, peer.Close())
}
priceOverrides, err := pc.UsagePriceOverrides.ToModels()
if err != nil {
return nil, errs.Combine(err, peer.Close())
}
service, err := stripecoinpayments.NewService( service, err := stripecoinpayments.NewService(
peer.Log.Named("payments.stripe:service"), peer.Log.Named("payments.stripe:service"),
stripeClient, stripeClient,
@ -572,9 +582,8 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB,
peer.DB.Billing(), peer.DB.Billing(),
peer.DB.Console().Projects(), peer.DB.Console().Projects(),
peer.DB.ProjectAccounting(), peer.DB.ProjectAccounting(),
pc.StorageTBPrice, prices,
pc.EgressTBPrice, priceOverrides,
pc.SegmentPrice,
pc.BonusRate) pc.BonusRate)
if err != nil { if err != nil {
return nil, errs.Combine(err, peer.Close()) return nil, errs.Combine(err, peer.Close())

View File

@ -4,30 +4,143 @@
package paymentsconfig package paymentsconfig
import ( import (
"fmt"
"strings"
"github.com/shopspring/decimal"
"github.com/spf13/pflag"
"github.com/zeebo/errs"
"storj.io/storj/satellite/payments/billing" "storj.io/storj/satellite/payments/billing"
"storj.io/storj/satellite/payments/storjscan" "storj.io/storj/satellite/payments/storjscan"
"storj.io/storj/satellite/payments/stripecoinpayments" "storj.io/storj/satellite/payments/stripecoinpayments"
) )
// Error is payments config err class.
var Error = errs.Class("payments config")
// Config defines global payments config. // Config defines global payments config.
type Config struct { type Config struct {
Provider string `help:"payments provider to use" default:""` Provider string `help:"payments provider to use" default:""`
BillingConfig billing.Config BillingConfig billing.Config
StripeCoinPayments stripecoinpayments.Config StripeCoinPayments stripecoinpayments.Config
Storjscan storjscan.Config Storjscan storjscan.Config
StorageTBPrice string `help:"price user should pay for storing TB per month" default:"4" testDefault:"10"` UsagePrice ProjectUsagePrice
EgressTBPrice string `help:"price user should pay for each TB of egress" default:"7" testDefault:"45"` BonusRate int64 `help:"amount of percents that user will earn as bonus credits by depositing in STORJ tokens" default:"10"`
SegmentPrice string `help:"price user should pay for each segment stored in network per month" default:"0.0000088" testDefault:"0.0000022"` NodeEgressBandwidthPrice int64 `help:"price node receive for storing TB of egress in cents" default:"2000"`
BonusRate int64 `help:"amount of percents that user will earn as bonus credits by depositing in STORJ tokens" default:"10"` NodeRepairBandwidthPrice int64 `help:"price node receive for storing TB of repair in cents" default:"1000"`
NodeEgressBandwidthPrice int64 `help:"price node receive for storing TB of egress in cents" default:"2000"` NodeAuditBandwidthPrice int64 `help:"price node receive for storing TB of audit in cents" default:"1000"`
NodeRepairBandwidthPrice int64 `help:"price node receive for storing TB of repair in cents" default:"1000"` NodeDiskSpacePrice int64 `help:"price node receive for storing disk space in cents/TB" default:"150"`
NodeAuditBandwidthPrice int64 `help:"price node receive for storing TB of audit in cents" default:"1000"` UsagePriceOverrides ProjectUsagePriceOverrides `help:"semicolon-separated usage price overrides in the format partner:storage,egress,segment"`
NodeDiskSpacePrice int64 `help:"price node receive for storing disk space in cents/TB" default:"150"`
} }
// PricingValues holds pricing model for satellite. // ProjectUsagePrice holds the configuration for the satellite's project usage price model.
type PricingValues struct { type ProjectUsagePrice struct {
StorageTBPrice string StorageTB string `help:"price user should pay for storage per month in dollars/TB" default:"4" testDefault:"10"`
EgressTBPrice string EgressTB string `help:"price user should pay for egress in dollars/TB" default:"7" testDefault:"45"`
SegmentPrice string Segment string `help:"price user should pay for segments stored on network per month in dollars/segment" default:"0.0000088" testDefault:"0.0000022"`
}
// ToModel returns the stripecoinpayments.ProjectUsagePriceModel representation of the project usage price.
func (p ProjectUsagePrice) ToModel() (model stripecoinpayments.ProjectUsagePriceModel, err error) {
storageTBMonthDollars, err := decimal.NewFromString(p.StorageTB)
if err != nil {
return model, Error.Wrap(err)
}
egressTBDollars, err := decimal.NewFromString(p.EgressTB)
if err != nil {
return model, Error.Wrap(err)
}
segmentMonthDollars, err := decimal.NewFromString(p.Segment)
if err != nil {
return model, Error.Wrap(err)
}
// Shift is to change the precision from TB dollars to MB cents
return stripecoinpayments.ProjectUsagePriceModel{
StorageMBMonthCents: storageTBMonthDollars.Shift(-6).Shift(2),
EgressMBCents: egressTBDollars.Shift(-6).Shift(2),
SegmentMonthCents: segmentMonthDollars.Shift(2),
}, nil
}
// Ensure that ProjectUsagePriceOverrides implements pflag.Value.
var _ pflag.Value = (*ProjectUsagePriceOverrides)(nil)
// ProjectUsagePriceOverrides represents a mapping between partners and project usage price overrides.
type ProjectUsagePriceOverrides struct {
overrideMap map[string]ProjectUsagePrice
}
// Type returns the type of the pflag.Value.
func (ProjectUsagePriceOverrides) Type() string { return "paymentsconfig.ProjectUsagePriceOverrides" }
// String returns the string representation of the price overrides.
func (p *ProjectUsagePriceOverrides) String() string {
if p == nil {
return ""
}
var s strings.Builder
left := len(p.overrideMap)
for partner, prices := range p.overrideMap {
s.WriteString(fmt.Sprintf("%s:%s,%s,%s", partner, prices.StorageTB, prices.EgressTB, prices.Segment))
left--
if left > 0 {
s.WriteRune(';')
}
}
return s.String()
}
// Set sets the list of price overrides to the parsed string.
func (p *ProjectUsagePriceOverrides) Set(s string) error {
overrideMap := make(map[string]ProjectUsagePrice)
for _, overrideStr := range strings.Split(s, ";") {
if overrideStr == "" {
continue
}
info := strings.Split(overrideStr, ":")
if len(info) != 2 {
return Error.New("Invalid price override (expected format partner:storage,egress,segment, got %s)", overrideStr)
}
partner := strings.TrimSpace(info[0])
if len(partner) == 0 {
return Error.New("Price override partner must not be empty")
}
pricesStr := info[1]
prices := strings.Split(pricesStr, ",")
if len(prices) != 3 {
return Error.New("Invalid prices (expected format storage,egress,segment, got %s)", pricesStr)
}
for _, price := range prices {
if _, err := decimal.NewFromString(price); err != nil {
return Error.New("Invalid price (%s)", err)
}
}
overrideMap[info[0]] = ProjectUsagePrice{
StorageTB: prices[0],
EgressTB: prices[1],
Segment: prices[2],
}
}
p.overrideMap = overrideMap
return nil
}
// ToModels returns the price overrides represented as a mapping between partners and project usage price models.
func (p ProjectUsagePriceOverrides) ToModels() (map[string]stripecoinpayments.ProjectUsagePriceModel, error) {
models := make(map[string]stripecoinpayments.ProjectUsagePriceModel)
for partner, prices := range p.overrideMap {
model, err := prices.ToModel()
if err != nil {
return nil, err
}
models[partner] = model
}
return models, nil
} }

View File

@ -0,0 +1,101 @@
// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
package paymentsconfig_test
import (
"sort"
"strings"
"testing"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/require"
"storj.io/storj/satellite/payments/paymentsconfig"
"storj.io/storj/satellite/payments/stripecoinpayments"
)
func TestProjectUsagePriceOverrides(t *testing.T) {
type Prices map[string]stripecoinpayments.ProjectUsagePriceModel
cases := []struct {
testID string
configValue string
expectedModel Prices
}{
{
testID: "empty",
configValue: "",
expectedModel: Prices{},
}, {
testID: "missing prices",
configValue: "partner",
}, {
testID: "missing partner",
configValue: ":1,2,3",
}, {
testID: "too few prices",
configValue: "partner:1",
}, {
testID: "single price override",
configValue: "partner:1,2,3",
expectedModel: Prices{
// Shift is to change the precision from TB dollars to MB cents
"partner": stripecoinpayments.ProjectUsagePriceModel{
StorageMBMonthCents: decimal.NewFromInt(1).Shift(-4),
EgressMBCents: decimal.NewFromInt(2).Shift(-4),
SegmentMonthCents: decimal.NewFromInt(3).Shift(2),
},
},
}, {
testID: "too many prices",
configValue: "partner:1,2,3,4",
}, {
testID: "invalid decimal",
configValue: "partner:0.0.1,2,3",
}, {
testID: "multiple price overrides",
configValue: "partner1:1,2,3;partner2:4,5,6",
expectedModel: Prices{
"partner1": stripecoinpayments.ProjectUsagePriceModel{
StorageMBMonthCents: decimal.NewFromInt(1).Shift(-4),
EgressMBCents: decimal.NewFromInt(2).Shift(-4),
SegmentMonthCents: decimal.NewFromInt(3).Shift(2),
},
"partner2": stripecoinpayments.ProjectUsagePriceModel{
StorageMBMonthCents: decimal.NewFromInt(4).Shift(-4),
EgressMBCents: decimal.NewFromInt(5).Shift(-4),
SegmentMonthCents: decimal.NewFromInt(6).Shift(2),
},
},
},
}
for _, c := range cases {
c := c
t.Run(c.testID, func(t *testing.T) {
price := &paymentsconfig.ProjectUsagePriceOverrides{}
err := price.Set(c.configValue)
if c.expectedModel == nil {
require.Error(t, err)
return
}
require.NoError(t, err)
strParts := strings.Split(price.String(), ";")
sort.Strings(strParts)
require.Equal(t, c.configValue, strings.Join(strParts, ";"))
models, err := price.ToModels()
require.NoError(t, err)
require.Len(t, models, len(c.expectedModel))
for partner, price := range c.expectedModel {
model := models[partner]
require.Contains(t, models, partner)
require.Equal(t, price.StorageMBMonthCents, model.StorageMBMonthCents)
require.Equal(t, price.EgressMBCents, model.EgressMBCents)
require.Equal(t, price.SegmentMonthCents, model.SegmentMonthCents)
}
})
}
}

View File

@ -51,11 +51,19 @@ func TestSignupCouponCodes(t *testing.T) {
projectUsage := accounting.NewService(db.ProjectAccounting(), cache, projectLimitCache, *sat.API.Metainfo.Metabase, 5*time.Minute, -10*time.Second) projectUsage := accounting.NewService(db.ProjectAccounting(), cache, projectLimitCache, *sat.API.Metainfo.Metabase, 5*time.Minute, -10*time.Second)
pc := paymentsconfig.Config{ pc := paymentsconfig.Config{
StorageTBPrice: "10", UsagePrice: paymentsconfig.ProjectUsagePrice{
EgressTBPrice: "45", StorageTB: "10",
SegmentPrice: "0.0000022", EgressTB: "45",
Segment: "0.0000022",
},
} }
prices, err := pc.UsagePrice.ToModel()
require.NoError(t, err)
priceOverrides, err := pc.UsagePriceOverrides.ToModels()
require.NoError(t, err)
paymentsService, err := stripecoinpayments.NewService( paymentsService, err := stripecoinpayments.NewService(
log.Named("payments.stripe:service"), log.Named("payments.stripe:service"),
stripecoinpayments.NewStripeMock( stripecoinpayments.NewStripeMock(
@ -69,9 +77,8 @@ func TestSignupCouponCodes(t *testing.T) {
db.Billing(), db.Billing(),
db.Console().Projects(), db.Console().Projects(),
db.ProjectAccounting(), db.ProjectAccounting(),
pc.StorageTBPrice, prices,
pc.EgressTBPrice, priceOverrides,
pc.SegmentPrice,
pc.BonusRate) pc.BonusRate)
require.NoError(t, err) require.NoError(t, err)

View File

@ -63,9 +63,8 @@ type Service struct {
usageDB accounting.ProjectAccounting usageDB accounting.ProjectAccounting
stripeClient StripeClient stripeClient StripeClient
StorageMBMonthPriceCents decimal.Decimal usagePrices ProjectUsagePriceModel
EgressMBPriceCents decimal.Decimal usagePriceOverrides map[string]ProjectUsagePriceModel
SegmentMonthPriceCents decimal.Decimal
// BonusRate amount of percents // BonusRate amount of percents
BonusRate int64 BonusRate int64
// Coupon Values // Coupon Values
@ -78,42 +77,30 @@ type Service struct {
nowFn func() time.Time nowFn func() time.Time
} }
// ProjectUsagePriceModel represents price model for project usage.
type ProjectUsagePriceModel struct {
StorageMBMonthCents decimal.Decimal
EgressMBCents decimal.Decimal
SegmentMonthCents decimal.Decimal
}
// NewService creates a Service instance. // NewService creates a Service instance.
func NewService(log *zap.Logger, stripeClient StripeClient, config Config, db DB, walletsDB storjscan.WalletsDB, billingDB billing.TransactionsDB, projectsDB console.Projects, usageDB accounting.ProjectAccounting, storageTBPrice, egressTBPrice, segmentPrice string, bonusRate int64) (*Service, error) { func NewService(log *zap.Logger, stripeClient StripeClient, config Config, db DB, walletsDB storjscan.WalletsDB, billingDB billing.TransactionsDB, projectsDB console.Projects, usageDB accounting.ProjectAccounting, usagePrices ProjectUsagePriceModel, usagePriceOverrides map[string]ProjectUsagePriceModel, bonusRate int64) (*Service, error) {
storageTBMonthDollars, err := decimal.NewFromString(storageTBPrice)
if err != nil {
return nil, err
}
egressTBDollars, err := decimal.NewFromString(egressTBPrice)
if err != nil {
return nil, err
}
segmentMonthDollars, err := decimal.NewFromString(segmentPrice)
if err != nil {
return nil, err
}
// change the precision from TB dollars to MB cents
storageMBMonthPriceCents := storageTBMonthDollars.Shift(-6).Shift(2)
egressMBPriceCents := egressTBDollars.Shift(-6).Shift(2)
segmentMonthPriceCents := segmentMonthDollars.Shift(2)
return &Service{ return &Service{
log: log, log: log,
db: db, db: db,
walletsDB: walletsDB, walletsDB: walletsDB,
billingDB: billingDB, billingDB: billingDB,
projectsDB: projectsDB, projectsDB: projectsDB,
usageDB: usageDB, usageDB: usageDB,
stripeClient: stripeClient, stripeClient: stripeClient,
StorageMBMonthPriceCents: storageMBMonthPriceCents, usagePrices: usagePrices,
EgressMBPriceCents: egressMBPriceCents, usagePriceOverrides: usagePriceOverrides,
SegmentMonthPriceCents: segmentMonthPriceCents, BonusRate: bonusRate,
BonusRate: bonusRate, StripeFreeTierCouponID: config.StripeFreeTierCouponID,
StripeFreeTierCouponID: config.StripeFreeTierCouponID, AutoAdvance: config.AutoAdvance,
AutoAdvance: config.AutoAdvance, listingLimit: config.ListingLimit,
listingLimit: config.ListingLimit, nowFn: time.Now,
nowFn: time.Now,
}, nil }, nil
} }
@ -502,21 +489,21 @@ func (service *Service) InvoiceItemsFromProjectRecord(projName string, record Pr
projectItem := &stripe.InvoiceItemParams{} projectItem := &stripe.InvoiceItemParams{}
projectItem.Description = stripe.String(fmt.Sprintf("Project %s - Segment Storage (MB-Month)", projName)) projectItem.Description = stripe.String(fmt.Sprintf("Project %s - Segment Storage (MB-Month)", projName))
projectItem.Quantity = stripe.Int64(storageMBMonthDecimal(record.Storage).IntPart()) projectItem.Quantity = stripe.Int64(storageMBMonthDecimal(record.Storage).IntPart())
storagePrice, _ := service.StorageMBMonthPriceCents.Float64() storagePrice, _ := service.usagePrices.StorageMBMonthCents.Float64()
projectItem.UnitAmountDecimal = stripe.Float64(storagePrice) projectItem.UnitAmountDecimal = stripe.Float64(storagePrice)
result = append(result, projectItem) result = append(result, projectItem)
projectItem = &stripe.InvoiceItemParams{} projectItem = &stripe.InvoiceItemParams{}
projectItem.Description = stripe.String(fmt.Sprintf("Project %s - Egress Bandwidth (MB)", projName)) projectItem.Description = stripe.String(fmt.Sprintf("Project %s - Egress Bandwidth (MB)", projName))
projectItem.Quantity = stripe.Int64(egressMBDecimal(record.Egress).IntPart()) projectItem.Quantity = stripe.Int64(egressMBDecimal(record.Egress).IntPart())
egressPrice, _ := service.EgressMBPriceCents.Float64() egressPrice, _ := service.usagePrices.EgressMBCents.Float64()
projectItem.UnitAmountDecimal = stripe.Float64(egressPrice) projectItem.UnitAmountDecimal = stripe.Float64(egressPrice)
result = append(result, projectItem) result = append(result, projectItem)
projectItem = &stripe.InvoiceItemParams{} projectItem = &stripe.InvoiceItemParams{}
projectItem.Description = stripe.String(fmt.Sprintf("Project %s - Segment Fee (Segment-Month)", projName)) projectItem.Description = stripe.String(fmt.Sprintf("Project %s - Segment Fee (Segment-Month)", projName))
projectItem.Quantity = stripe.Int64(segmentMonthDecimal(record.Segments).IntPart()) projectItem.Quantity = stripe.Int64(segmentMonthDecimal(record.Segments).IntPart())
segmentPrice, _ := service.SegmentMonthPriceCents.Float64() segmentPrice, _ := service.usagePrices.SegmentMonthCents.Float64()
projectItem.UnitAmountDecimal = stripe.Float64(segmentPrice) projectItem.UnitAmountDecimal = stripe.Float64(segmentPrice)
result = append(result, projectItem) result = append(result, projectItem)
service.log.Info("invoice items", zap.Any("result", result)) service.log.Info("invoice items", zap.Any("result", result))
@ -771,9 +758,9 @@ func (price projectUsagePrice) TotalInt64() int64 {
// calculateProjectUsagePrice calculate project usage price. // calculateProjectUsagePrice calculate project usage price.
func (service *Service) calculateProjectUsagePrice(egress int64, storage, segments float64) projectUsagePrice { func (service *Service) calculateProjectUsagePrice(egress int64, storage, segments float64) projectUsagePrice {
return projectUsagePrice{ return projectUsagePrice{
Storage: service.StorageMBMonthPriceCents.Mul(storageMBMonthDecimal(storage)).Round(0), Storage: service.usagePrices.StorageMBMonthCents.Mul(storageMBMonthDecimal(storage)).Round(0),
Egress: service.EgressMBPriceCents.Mul(egressMBDecimal(egress)).Round(0), Egress: service.usagePrices.EgressMBCents.Mul(egressMBDecimal(egress)).Round(0),
Segments: service.SegmentMonthPriceCents.Mul(segmentMonthDecimal(segments)).Round(0), Segments: service.usagePrices.SegmentMonthCents.Mul(segmentMonthDecimal(segments)).Round(0),
} }
} }

View File

@ -790,9 +790,6 @@ identity.key-path: /root/.local/share/storj/identity/satellite/identity.key
# amount of percents that user will earn as bonus credits by depositing in STORJ tokens # amount of percents that user will earn as bonus credits by depositing in STORJ tokens
# payments.bonus-rate: 10 # payments.bonus-rate: 10
# price user should pay for each TB of egress
# payments.egress-tb-price: "7"
# price node receive for storing TB of audit in cents # price node receive for storing TB of audit in cents
# payments.node-audit-bandwidth-price: 1000 # payments.node-audit-bandwidth-price: 1000
@ -808,12 +805,6 @@ identity.key-path: /root/.local/share/storj/identity/satellite/identity.key
# payments provider to use # payments provider to use
# payments.provider: "" # payments.provider: ""
# price user should pay for each segment stored in network per month
# payments.segment-price: "0.0000088"
# price user should pay for storing TB per month
# payments.storage-tb-price: "4"
# basic auth identifier # basic auth identifier
# payments.storjscan.auth.identifier: "" # payments.storjscan.auth.identifier: ""
@ -844,6 +835,18 @@ identity.key-path: /root/.local/share/storj/identity/satellite/identity.key
# stripe API secret key # stripe API secret key
# payments.stripe-coin-payments.stripe-secret-key: "" # payments.stripe-coin-payments.stripe-secret-key: ""
# semicolon-separated usage price overrides in the format partner:storage,egress,segment
# payments.usage-price-overrides: ""
# price user should pay for egress in dollars/TB
# payments.usage-price.egress-tb: "7"
# price user should pay for segments stored on network per month in dollars/segment
# payments.usage-price.segment: "0.0000088"
# price user should pay for storage per month in dollars/TB
# payments.usage-price.storage-tb: "4"
# how often to remove unused project bandwidth rollups # how often to remove unused project bandwidth rollups
# project-bw-cleanup.interval: 168h0m0s # project-bw-cleanup.interval: 168h0m0s