From 083b396c164eaeaac5c94fd767c792c6c2b2bf8d Mon Sep 17 00:00:00 2001 From: Yaroslav Vorobiov Date: Tue, 28 Jan 2020 18:36:54 -0500 Subject: [PATCH] satellite/payments: allow floating point numbers for pricing Change-Id: I78b60134cf043746efef5371b761939a10f75aaf --- go.mod | 1 + go.sum | 2 + satellite/api.go | 12 ++- .../consoleweb/consoleql/mutation_test.go | 5 +- .../consoleweb/consoleql/query_test.go | 5 +- satellite/core.go | 12 ++- satellite/payments/paymentsconfig/config.go | 6 +- .../payments/stripecoinpayments/accounts.go | 11 ++- .../payments/stripecoinpayments/service.go | 98 ++++++++++++++----- scripts/testdata/satellite-config.yaml.lock | 14 +-- 10 files changed, 112 insertions(+), 54 deletions(-) diff --git a/go.mod b/go.mod index 256bd4c68..4b7fbca2c 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index dad6e0886..a4971e73d 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/satellite/api.go b/satellite/api.go index 93332a607..1667cd5a1 100644 --- a/satellite/api.go +++ b/satellite/api.go @@ -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) diff --git a/satellite/console/consoleweb/consoleql/mutation_test.go b/satellite/console/consoleweb/consoleql/mutation_test.go index 22f0a7eb8..a7c410dcb 100644 --- a/satellite/console/consoleweb/consoleql/mutation_test.go +++ b/satellite/console/consoleweb/consoleql/mutation_test.go @@ -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() diff --git a/satellite/console/consoleweb/consoleql/query_test.go b/satellite/console/consoleweb/consoleql/query_test.go index 013090891..04cf5b922 100644 --- a/satellite/console/consoleweb/consoleql/query_test.go +++ b/satellite/console/consoleweb/consoleql/query_test.go @@ -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() diff --git a/satellite/core.go b/satellite/core.go index dfddc46b3..43fb05ecd 100644 --- a/satellite/core.go +++ b/satellite/core.go @@ -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() diff --git a/satellite/payments/paymentsconfig/config.go b/satellite/payments/paymentsconfig/config.go index 112b72b67..9ef5169f8 100644 --- a/satellite/payments/paymentsconfig/config.go +++ b/satellite/payments/paymentsconfig/config.go @@ -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"` } diff --git a/satellite/payments/stripecoinpayments/accounts.go b/satellite/payments/stripecoinpayments/accounts.go index a22e0c920..28023e50d 100644 --- a/satellite/payments/stripecoinpayments/accounts.go +++ b/satellite/payments/stripecoinpayments/accounts.go @@ -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(), }) } diff --git a/satellite/payments/stripecoinpayments/service.go b/satellite/payments/stripecoinpayments/service.go index 443d45a86..a0a83f02d 100644 --- a/satellite/payments/stripecoinpayments/service.go +++ b/satellite/payments/stripecoinpayments/service.go @@ -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)), + } +} diff --git a/scripts/testdata/satellite-config.yaml.lock b/scripts/testdata/satellite-config.yaml.lock index cb569e985..ba2388952 100644 --- a/scripts/testdata/satellite-config.yaml.lock +++ b/scripts/testdata/satellite-config.yaml.lock @@ -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