satellite/payments: allow floating point numbers for pricing

Change-Id: I78b60134cf043746efef5371b761939a10f75aaf
This commit is contained in:
Yaroslav Vorobiov 2020-01-28 18:36:54 -05:00
parent a0c9f7f3b0
commit 083b396c16
10 changed files with 112 additions and 54 deletions

1
go.mod
View File

@ -81,6 +81,7 @@ require (
github.com/pkg/profile v1.2.1 // indirect
github.com/prometheus/procfs v0.0.0-20190517135640-51af30a78b0e // indirect
github.com/rs/cors v1.5.0 // indirect
github.com/shopspring/decimal v0.0.0-20200105231215-408a2507e114
github.com/skyrings/skyring-common v0.0.0-20160929130248-d1c0bb1cbd5e
github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf // indirect
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect

2
go.sum
View File

@ -375,6 +375,8 @@ github.com/segmentio/go-prompt v1.2.1-0.20161017233205-f0d19b6901ad h1:EqOdoSJGI
github.com/segmentio/go-prompt v1.2.1-0.20161017233205-f0d19b6901ad/go.mod h1:B3ehdD1xPoWDKgrQgUaGk+m8H1xb1J5TyYDfKpKNeEE=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200105231215-408a2507e114 h1:Pm6R878vxWWWR+Sa3ppsLce/Zq+JNTs6aVvRu13jv9A=
github.com/shopspring/decimal v0.0.0-20200105231215-408a2507e114/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=

View File

@ -479,15 +479,19 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
default:
peer.Payments.Accounts = mockpayments.Accounts()
case "stripecoinpayments":
service := stripecoinpayments.NewService(
service, err := stripecoinpayments.NewService(
peer.Log.Named("payments.stripe:service"),
pc.StripeCoinPayments,
peer.DB.StripeCoinPayments(),
peer.DB.Console().Projects(),
peer.DB.ProjectAccounting(),
pc.PerObjectPrice,
pc.EgressPrice,
pc.TbhPrice)
pc.StorageTBPrice,
pc.EgressTBPrice,
pc.ObjectPrice)
if err != nil {
return nil, errs.Combine(err, peer.Close())
}
peer.Payments.Accounts = service.Accounts()
peer.Payments.Inspector = stripecoinpayments.NewEndpoint(service)

View File

@ -57,14 +57,15 @@ func TestGrapqhlMutation(t *testing.T) {
},
)
payments := stripecoinpayments.NewService(
payments, err := stripecoinpayments.NewService(
log.Named("payments"),
stripecoinpayments.Config{},
db.StripeCoinPayments(),
db.Console().Projects(),
db.ProjectAccounting(),
0, 0, 0,
"0", "0", "0",
)
require.NoError(t, err)
miniredis := redisserver.NewMini()
addr, cleanup, err := miniredis.Run()

View File

@ -42,14 +42,15 @@ func TestGraphqlQuery(t *testing.T) {
},
)
payments := stripecoinpayments.NewService(
payments, err := stripecoinpayments.NewService(
log.Named("payments"),
stripecoinpayments.Config{},
db.StripeCoinPayments(),
db.Console().Projects(),
db.ProjectAccounting(),
0, 0, 0,
"0", "0", "0",
)
require.NoError(t, err)
miniredis := redisserver.NewMini()
addr, cleanup, err := miniredis.Run()

View File

@ -372,15 +372,19 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB,
default:
peer.Payments.Accounts = mockpayments.Accounts()
case "stripecoinpayments":
service := stripecoinpayments.NewService(
service, err := stripecoinpayments.NewService(
peer.Log.Named("payments.stripe:service"),
pc.StripeCoinPayments,
peer.DB.StripeCoinPayments(),
peer.DB.Console().Projects(),
peer.DB.ProjectAccounting(),
pc.PerObjectPrice,
pc.EgressPrice,
pc.TbhPrice)
pc.StorageTBPrice,
pc.EgressTBPrice,
pc.ObjectPrice)
if err != nil {
return nil, errs.Combine(err, peer.Close())
}
peer.Payments.Accounts = service.Accounts()

View File

@ -11,7 +11,7 @@ import (
type Config struct {
Provider string `help:"payments provider to use" default:""`
StripeCoinPayments stripecoinpayments.Config
PerObjectPrice int64 `help:"price in cents user should pay for each object storing in network" devDefault:"0" releaseDefault:"0"`
EgressPrice int64 `help:"price in cents user should pay for each TB of egress" devDefault:"0" releaseDefault:"0"`
TbhPrice int64 `help:"price in cents user should pay for storing each TB per hour" devDefault:"0" releaseDefault:"0"`
StorageTBPrice string `help:"price user should pay for storing TB per month" default:"10"`
EgressTBPrice string `help:"price user should pay for each TB of egress" default:"45"`
ObjectPrice string `help:"price user should pay for each object stored in network per month" default:"0.0000022"`
}

View File

@ -127,12 +127,13 @@ func (accounts *accounts) ProjectCharges(ctx context.Context, userID uuid.UUID)
return charges, Error.Wrap(err)
}
projectPrice := accounts.service.calculateProjectUsagePrice(usage.Egress, usage.Storage, usage.ObjectCount)
charges = append(charges, payments.ProjectCharge{
ProjectID: project.ID,
// TODO: check precision
Egress: usage.Egress * accounts.service.EgressPrice / int64(memory.TB),
ObjectCount: int64(usage.ObjectCount * float64(accounts.service.PerObjectPrice)),
StorageGbHrs: int64(usage.Storage*float64(accounts.service.TBhPrice)) / int64(memory.TB),
ProjectID: project.ID,
Egress: projectPrice.Egress.IntPart(),
ObjectCount: projectPrice.Objects.IntPart(),
StorageGbHrs: projectPrice.Storage.IntPart(),
})
}

View File

@ -10,6 +10,7 @@ import (
"sync"
"time"
"github.com/shopspring/decimal"
"github.com/skyrings/skyring-common/tools/uuid"
"github.com/stripe/stripe-go"
"github.com/stripe/stripe-go/client"
@ -17,7 +18,6 @@ import (
"go.uber.org/zap"
"gopkg.in/spacemonkeygo/monkit.v2"
"storj.io/common/memory"
"storj.io/storj/satellite/accounting"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/payments"
@ -55,9 +55,9 @@ type Service struct {
stripeClient *client.API
coinPayments *coinpayments.Client
PerObjectPrice int64
EgressPrice int64
TBhPrice int64
ByteHourCents decimal.Decimal
EgressByteCents decimal.Decimal
ObjectHourCents decimal.Decimal
mu sync.Mutex
rates coinpayments.CurrencyRateInfos
@ -65,7 +65,7 @@ type Service struct {
}
// NewService creates a Service instance.
func NewService(log *zap.Logger, config Config, db DB, projectsDB console.Projects, usageDB accounting.ProjectAccounting, perObjectPrice, egressPrice, tbhPrice int64) *Service {
func NewService(log *zap.Logger, config Config, db DB, projectsDB console.Projects, usageDB accounting.ProjectAccounting, storageTBPrice, egressTBPrice, objectPrice string) (*Service, error) {
backendConfig := &stripe.BackendConfig{
LeveledLogger: log.Sugar(),
}
@ -85,17 +85,44 @@ func NewService(log *zap.Logger, config Config, db DB, projectsDB console.Projec
},
)
return &Service{
log: log,
db: db,
projectsDB: projectsDB,
usageDB: usageDB,
stripeClient: stripeClient,
coinPayments: coinPaymentsClient,
TBhPrice: tbhPrice,
PerObjectPrice: perObjectPrice,
EgressPrice: egressPrice,
tbMonthDollars, err := decimal.NewFromString(storageTBPrice)
if err != nil {
return nil, err
}
egressByteDollars, err := decimal.NewFromString(egressTBPrice)
if err != nil {
return nil, err
}
objectMonthDollars, err := decimal.NewFromString(objectPrice)
if err != nil {
return nil, err
}
// change the precision from dollars to cents
tbMonthCents := tbMonthDollars.Shift(2)
egressByteCents := egressByteDollars.Shift(2)
objectHourCents := objectMonthDollars.Shift(2)
// get per hour prices from storage and objects
hoursPerMonth := decimal.New(30*24, 0)
tbHourCents := tbMonthCents.Div(hoursPerMonth)
objectHourCents = objectHourCents.Div(hoursPerMonth)
// convert tb to bytes for storage price
byteHourCents := tbHourCents.Div(decimal.New(1000000000000, 0))
return &Service{
log: log,
db: db,
projectsDB: projectsDB,
usageDB: usageDB,
stripeClient: stripeClient,
coinPayments: coinPaymentsClient,
ByteHourCents: byteHourCents,
EgressByteCents: egressByteCents,
ObjectHourCents: objectHourCents,
}, nil
}
// Accounts exposes all needed functionality to manage payment accounts.
@ -394,11 +421,7 @@ func (service *Service) createProjectRecords(ctx context.Context, projects []con
return err
}
egressPrice := usage.Egress * service.EgressPrice / int64(memory.TB)
objectCountPrice := int64(usage.ObjectCount * float64(service.PerObjectPrice))
storageGbHrsPrice := int64(usage.Storage*float64(service.TBhPrice)) / int64(memory.TB)
currentUsagePrice := egressPrice + objectCountPrice + storageGbHrsPrice
currentUsagePrice := service.calculateProjectUsagePrice(usage.Egress, usage.Storage, usage.ObjectCount).TotalInt64()
// TODO: only for 1 coupon per project
for _, coupon := range coupons {
@ -506,15 +529,10 @@ func (service *Service) createInvoiceItems(ctx context.Context, cusID, projName
return err
}
// TODO: reuse this code fragment.
egressPrice := record.Egress * service.EgressPrice / int64(memory.TB)
objectCountPrice := int64(record.Objects * float64(service.PerObjectPrice))
storageGbHrsPrice := int64(record.Storage*float64(service.TBhPrice)) / int64(memory.TB)
currentUsageAmount := egressPrice + objectCountPrice + storageGbHrsPrice
projectPrice := service.calculateProjectUsagePrice(record.Egress, record.Storage, record.Objects)
projectItem := &stripe.InvoiceItemParams{
Amount: stripe.Int64(currentUsageAmount),
Amount: stripe.Int64(projectPrice.TotalInt64()),
Currency: stripe.String(string(stripe.CurrencyUSD)),
Customer: stripe.String(cusID),
Description: stripe.String(fmt.Sprintf("project %s", projName)),
@ -706,3 +724,29 @@ func (service *Service) createInvoice(ctx context.Context, cusID string) (err er
return nil
}
// projectUsagePrice represents pricing for project usage.
type projectUsagePrice struct {
Storage decimal.Decimal
Egress decimal.Decimal
Objects decimal.Decimal
}
// Total returns project usage price total.
func (price projectUsagePrice) Total() decimal.Decimal {
return price.Storage.Add(price.Egress).Add(price.Objects)
}
// Total returns project usage price total.
func (price projectUsagePrice) TotalInt64() int64 {
return price.Storage.Add(price.Egress).Add(price.Objects).IntPart()
}
// calculateProjectUsagePrice calculate project usage price.
func (service *Service) calculateProjectUsagePrice(egress int64, storage, objects float64) projectUsagePrice {
return projectUsagePrice{
Storage: service.ByteHourCents.Mul(decimal.NewFromFloat(storage)),
Egress: service.EgressByteCents.Mul(decimal.New(egress, 0)),
Objects: service.ObjectHourCents.Mul(decimal.NewFromFloat(objects)),
}
}

View File

@ -367,15 +367,18 @@ identity.key-path: /root/.local/share/storj/identity/satellite/identity.key
# number of update requests to process per transaction
# overlay.update-stats-batch-size: 100
# price in cents user should pay for each TB of egress
# payments.egress-price: 0
# price user should pay for each TB of egress
# payments.egress-tb-price: "45"
# price in cents user should pay for each object storing in network
# payments.per-object-price: 0
# price user should pay for each object stored in network per month
# payments.object-price: "0.0000022"
# payments provider to use
# payments.provider: ""
# price user should pay for storing TB per month
# payments.storage-tb-price: "10"
# amount of time we wait before running next account balance update loop
# payments.stripe-coin-payments.account-balance-update-interval: 1h30m0s
@ -397,9 +400,6 @@ identity.key-path: /root/.local/share/storj/identity/satellite/identity.key
# amount of time we wait before running next transaction update loop
# payments.stripe-coin-payments.transaction-update-interval: 30m0s
# price in cents user should pay for storing each TB per hour
# payments.tbh-price: 0
# referrals.referral-manager-url: ""
# time limit for downloading pieces from a node for repair