2019-10-23 13:04:54 +01:00
|
|
|
// Copyright (C) 2019 Storj Labs, Inc.
|
|
|
|
// See LICENSE for copying information.
|
|
|
|
|
|
|
|
package paymentsconfig
|
|
|
|
|
2019-11-15 14:27:44 +00:00
|
|
|
import (
|
2022-12-01 07:40:52 +00:00
|
|
|
"fmt"
|
2023-01-11 20:17:54 +00:00
|
|
|
"strconv"
|
2022-12-01 07:40:52 +00:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/shopspring/decimal"
|
|
|
|
"github.com/spf13/pflag"
|
|
|
|
"github.com/zeebo/errs"
|
|
|
|
|
2023-01-11 20:17:54 +00:00
|
|
|
"storj.io/common/useragent"
|
2023-01-12 03:41:14 +00:00
|
|
|
"storj.io/storj/satellite/payments"
|
2022-06-17 00:29:31 +01:00
|
|
|
"storj.io/storj/satellite/payments/billing"
|
2022-04-28 03:54:56 +01:00
|
|
|
"storj.io/storj/satellite/payments/storjscan"
|
2019-11-15 14:27:44 +00:00
|
|
|
"storj.io/storj/satellite/payments/stripecoinpayments"
|
|
|
|
)
|
2019-10-23 13:04:54 +01:00
|
|
|
|
2022-12-01 07:40:52 +00:00
|
|
|
// Error is payments config err class.
|
|
|
|
var Error = errs.Class("payments config")
|
|
|
|
|
2019-10-23 13:04:54 +01:00
|
|
|
// Config defines global payments config.
|
|
|
|
type Config struct {
|
2023-03-03 18:10:01 +00:00
|
|
|
Provider string `help:"payments provider to use" default:""`
|
|
|
|
MockProvider stripecoinpayments.StripeClient `internal:"true"`
|
|
|
|
|
2022-06-17 00:29:31 +01:00
|
|
|
BillingConfig billing.Config
|
2020-04-08 11:19:06 +01:00
|
|
|
StripeCoinPayments stripecoinpayments.Config
|
2022-04-28 03:54:56 +01:00
|
|
|
Storjscan storjscan.Config
|
2022-12-01 07:40:52 +00:00
|
|
|
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"`
|
2023-01-11 20:17:54 +00:00
|
|
|
PackagePlans PackagePlans `help:"semicolon-separated partner package plans in the format partner:couponID,price. Price is in cents USD."`
|
2022-12-01 07:40:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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"`
|
|
|
|
}
|
|
|
|
|
2023-01-12 03:41:14 +00:00
|
|
|
// ToModel returns the payments.ProjectUsagePriceModel representation of the project usage price.
|
|
|
|
func (p ProjectUsagePrice) ToModel() (model payments.ProjectUsagePriceModel, err error) {
|
2022-12-01 07:40:52 +00:00
|
|
|
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
|
2023-01-12 03:41:14 +00:00
|
|
|
return payments.ProjectUsagePriceModel{
|
2022-12-01 07:40:52 +00:00
|
|
|
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
|
2021-03-30 00:37:46 +01:00
|
|
|
}
|
|
|
|
|
2023-01-12 03:41:14 +00:00
|
|
|
// SetMap sets the internal mapping between partners and project usage prices.
|
|
|
|
func (p *ProjectUsagePriceOverrides) SetMap(overrides map[string]ProjectUsagePrice) {
|
|
|
|
p.overrideMap = overrides
|
|
|
|
}
|
|
|
|
|
2022-12-01 07:40:52 +00:00
|
|
|
// ToModels returns the price overrides represented as a mapping between partners and project usage price models.
|
2023-01-12 03:41:14 +00:00
|
|
|
func (p ProjectUsagePriceOverrides) ToModels() (map[string]payments.ProjectUsagePriceModel, error) {
|
|
|
|
models := make(map[string]payments.ProjectUsagePriceModel)
|
2022-12-01 07:40:52 +00:00
|
|
|
for partner, prices := range p.overrideMap {
|
|
|
|
model, err := prices.ToModel()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
models[partner] = model
|
|
|
|
}
|
|
|
|
return models, nil
|
2021-04-15 18:53:09 +01:00
|
|
|
}
|
2023-01-11 20:17:54 +00:00
|
|
|
|
|
|
|
// PackagePlans contains one time prices for partners.
|
|
|
|
type PackagePlans struct {
|
2023-01-30 22:11:12 +00:00
|
|
|
Packages map[string]payments.PackagePlan
|
2023-01-11 20:17:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2023-01-30 22:11:12 +00:00
|
|
|
left := len(p.Packages)
|
|
|
|
for partner, pkg := range p.Packages {
|
2023-01-11 20:17:54 +00:00
|
|
|
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 {
|
2023-01-30 22:11:12 +00:00
|
|
|
packages := make(map[string]payments.PackagePlan)
|
2023-01-11 20:17:54 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-01-30 22:11:12 +00:00
|
|
|
packages[info[0]] = payments.PackagePlan{
|
2023-01-11 20:17:54 +00:00
|
|
|
CouponID: pkg[0],
|
|
|
|
Price: int64(cents),
|
|
|
|
}
|
|
|
|
}
|
2023-01-30 22:11:12 +00:00
|
|
|
p.Packages = packages
|
2023-01-11 20:17:54 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get a package plan by user agent.
|
2023-01-30 22:11:12 +00:00
|
|
|
func (p *PackagePlans) Get(userAgent []byte) (pkg payments.PackagePlan, err error) {
|
2023-01-11 20:17:54 +00:00
|
|
|
entries, err := useragent.ParseEntries(userAgent)
|
|
|
|
if err != nil {
|
2023-01-30 22:11:12 +00:00
|
|
|
return payments.PackagePlan{}, Error.Wrap(err)
|
2023-01-11 20:17:54 +00:00
|
|
|
}
|
|
|
|
for _, entry := range entries {
|
2023-01-30 22:11:12 +00:00
|
|
|
if pkg, ok := p.Packages[entry.Product]; ok {
|
2023-01-11 20:17:54 +00:00
|
|
|
return pkg, nil
|
|
|
|
}
|
|
|
|
}
|
2023-01-30 22:11:12 +00:00
|
|
|
return payments.PackagePlan{}, errs.New("no matching partner for (%s)", userAgent)
|
2023-01-11 20:17:54 +00:00
|
|
|
}
|