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"
2023-04-06 12:41:14 +01:00
"storj.io/storj/satellite/payments/stripe"
2019-11-15 14:27:44 +00:00
)
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-04-06 12:41:14 +01:00
Provider string ` help:"payments provider to use" default:"" `
MockProvider stripe . Client ` internal:"true" `
2023-03-03 18:10:01 +00:00
2023-06-09 17:42:19 +01:00
BillingConfig billing . Config
StripeCoinPayments stripe . 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" `
UsagePriceOverrides ProjectUsagePriceOverrides ` help:"semicolon-separated usage price overrides in the format partner:storage,egress,segment,egress_discount_ratio. The egress discount ratio is the ratio of free egress per unit-month of storage" `
PackagePlans PackagePlans ` help:"semicolon-separated partner package plans in the format partner:price,credit. Price and credit are 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 {
2023-04-03 14:24:34 +01:00
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" `
EgressDiscountRatio float64 ` internal:"true" `
2022-12-01 07:40:52 +00:00
}
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 ) ,
2023-04-03 14:24:34 +01:00
EgressDiscountRatio : p . EgressDiscountRatio ,
2022-12-01 07:40:52 +00:00
} , 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 {
2023-04-03 14:24:34 +01:00
egressDiscount := strconv . FormatFloat ( prices . EgressDiscountRatio , 'f' , - 1 , 64 )
s . WriteString ( fmt . Sprintf ( "%s:%s,%s,%s,%s" , partner , prices . StorageTB , prices . EgressTB , prices . Segment , egressDiscount ) )
2022-12-01 07:40:52 +00:00
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" )
}
2023-04-03 14:24:34 +01:00
valuesStr := info [ 1 ]
values := strings . Split ( valuesStr , "," )
if len ( values ) != 4 {
return Error . New ( "Invalid values (expected format storage,egress,segment,egress_discount_ratio, got %s)" , valuesStr )
2022-12-01 07:40:52 +00:00
}
2023-04-03 14:24:34 +01:00
for i := 0 ; i < 3 ; i ++ {
if _ , err := decimal . NewFromString ( values [ i ] ) ; err != nil {
return Error . New ( "Invalid price '%s' (%s)" , values [ i ] , err )
2022-12-01 07:40:52 +00:00
}
}
2023-04-03 14:24:34 +01:00
egressDiscount , err := strconv . ParseFloat ( values [ 3 ] , 64 )
if err != nil {
return Error . New ( "Invalid egress discount ratio '%s' (%s)" , values [ 3 ] , err )
}
2022-12-01 07:40:52 +00:00
overrideMap [ info [ 0 ] ] = ProjectUsagePrice {
2023-04-03 14:24:34 +01:00
StorageTB : values [ 0 ] ,
EgressTB : values [ 1 ] ,
Segment : values [ 2 ] ,
EgressDiscountRatio : egressDiscount ,
2022-12-01 07:40:52 +00:00
}
}
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-03-22 15:28:52 +00:00
s . WriteString ( fmt . Sprintf ( "%s:%d,%d" , partner , pkg . Price , pkg . Credit ) )
2023-01-11 20:17:54 +00:00
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 {
2023-03-22 15:28:52 +00:00
return Error . New ( "Invalid package plan (expected format partner:price,credit got %s)" , packagePlansStr )
2023-01-11 20:17:54 +00:00
}
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 ] == "" {
2023-03-22 15:28:52 +00:00
return Error . New ( "Invalid package (expected format price,credit got %s)" , packageStr )
2023-01-11 20:17:54 +00:00
}
if _ , err := decimal . NewFromString ( pkg [ 1 ] ) ; err != nil {
return Error . New ( "Invalid price (%s)" , err )
}
2023-03-22 15:28:52 +00:00
priceCents , err := strconv . Atoi ( pkg [ 0 ] )
if err != nil {
return Error . Wrap ( err )
}
creditCents , err := strconv . Atoi ( pkg [ 1 ] )
2023-01-11 20:17:54 +00:00
if err != nil {
return Error . Wrap ( err )
}
2023-01-30 22:11:12 +00:00
packages [ info [ 0 ] ] = payments . PackagePlan {
2023-03-22 15:28:52 +00:00
Price : int64 ( priceCents ) ,
Credit : int64 ( creditCents ) ,
2023-01-11 20:17:54 +00:00
}
}
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
}