storj/satellite/payments/paymentsconfig/config.go
Egon Elbre 63fa386b0a satellite/payments/stripecoinpayments: avoid mock cross-talk in tests
The tests were using global variables for keeping the mock state, which
was indexed by the satellite ID. However, the satellite ID-s are
deterministic and it's possible for two tests end up using the same
mocks.

Instead make the mock creation not depend on the satellite ID and
instead require it being configured via paymentsconfig.

This fixes TestAutoFreezeChore failure.

Change-Id: I531d3550a934fbb36cff2973be96fd43b7edc44a
2023-03-03 20:20:48 +02:00

239 lines
7.8 KiB
Go

// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package paymentsconfig
import (
"fmt"
"strconv"
"strings"
"github.com/shopspring/decimal"
"github.com/spf13/pflag"
"github.com/zeebo/errs"
"storj.io/common/useragent"
"storj.io/storj/satellite/payments"
"storj.io/storj/satellite/payments/billing"
"storj.io/storj/satellite/payments/storjscan"
"storj.io/storj/satellite/payments/stripecoinpayments"
)
// Error is payments config err class.
var Error = errs.Class("payments config")
// Config defines global payments config.
type Config struct {
Provider string `help:"payments provider to use" default:""`
MockProvider stripecoinpayments.StripeClient `internal:"true"`
BillingConfig billing.Config
StripeCoinPayments stripecoinpayments.Config
Storjscan storjscan.Config
UsagePrice ProjectUsagePrice
BonusRate int64 `help:"amount of percents that user will earn as bonus credits by depositing in STORJ tokens" default:"10"`
NodeEgressBandwidthPrice int64 `help:"price node receive for storing TB of egress in cents" default:"2000"`
NodeRepairBandwidthPrice int64 `help:"price node receive for storing TB of repair in cents" default:"1000"`
NodeAuditBandwidthPrice int64 `help:"price node receive for storing TB of audit in cents" default:"1000"`
NodeDiskSpacePrice int64 `help:"price node receive for storing disk space in cents/TB" default:"150"`
UsagePriceOverrides ProjectUsagePriceOverrides `help:"semicolon-separated usage price overrides in the format partner:storage,egress,segment"`
PackagePlans PackagePlans `help:"semicolon-separated partner package plans in the format partner:couponID,price. Price is in cents USD."`
}
// ProjectUsagePrice holds the configuration for the satellite's project usage price model.
type ProjectUsagePrice struct {
StorageTB string `help:"price user should pay for storage per month in dollars/TB" default:"4" testDefault:"10"`
EgressTB string `help:"price user should pay for egress in dollars/TB" default:"7" testDefault:"45"`
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 payments.ProjectUsagePriceModel representation of the project usage price.
func (p ProjectUsagePrice) ToModel() (model payments.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 payments.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
}
// SetMap sets the internal mapping between partners and project usage prices.
func (p *ProjectUsagePriceOverrides) SetMap(overrides map[string]ProjectUsagePrice) {
p.overrideMap = overrides
}
// ToModels returns the price overrides represented as a mapping between partners and project usage price models.
func (p ProjectUsagePriceOverrides) ToModels() (map[string]payments.ProjectUsagePriceModel, error) {
models := make(map[string]payments.ProjectUsagePriceModel)
for partner, prices := range p.overrideMap {
model, err := prices.ToModel()
if err != nil {
return nil, err
}
models[partner] = model
}
return models, nil
}
// PackagePlans contains one time prices for partners.
type PackagePlans struct {
Packages map[string]payments.PackagePlan
}
// Type returns the type of the pflag.Value.
func (PackagePlans) Type() string { return "paymentsconfig.PackagePlans" }
// String returns the string representation of the package plans.
func (p *PackagePlans) String() string {
if p == nil {
return ""
}
var s strings.Builder
left := len(p.Packages)
for partner, pkg := range p.Packages {
s.WriteString(fmt.Sprintf("%s:%s,%d", partner, pkg.CouponID, pkg.Price))
left--
if left > 0 {
s.WriteRune(';')
}
}
return s.String()
}
// Set sets the list of pricing plans to the parsed string.
func (p *PackagePlans) Set(s string) error {
packages := make(map[string]payments.PackagePlan)
for _, packagePlansStr := range strings.Split(s, ";") {
if packagePlansStr == "" {
continue
}
info := strings.Split(packagePlansStr, ":")
if len(info) != 2 {
return Error.New("Invalid package plan (expected format partner:couponID,price got %s)", packagePlansStr)
}
partner := strings.TrimSpace(info[0])
if len(partner) == 0 {
return Error.New("Package plan partner must not be empty")
}
packageStr := info[1]
pkg := strings.Split(packageStr, ",")
if len(pkg) != 2 || pkg[0] == "" {
return Error.New("Invalid package (expected format couponID,price got %s)", packageStr)
}
if _, err := decimal.NewFromString(pkg[1]); err != nil {
return Error.New("Invalid price (%s)", err)
}
cents, err := strconv.Atoi(pkg[1])
if err != nil {
return Error.Wrap(err)
}
packages[info[0]] = payments.PackagePlan{
CouponID: pkg[0],
Price: int64(cents),
}
}
p.Packages = packages
return nil
}
// Get a package plan by user agent.
func (p *PackagePlans) Get(userAgent []byte) (pkg payments.PackagePlan, err error) {
entries, err := useragent.ParseEntries(userAgent)
if err != nil {
return payments.PackagePlan{}, Error.Wrap(err)
}
for _, entry := range entries {
if pkg, ok := p.Packages[entry.Product]; ok {
return pkg, nil
}
}
return payments.PackagePlan{}, errs.New("no matching partner for (%s)", userAgent)
}