satellite/payments/stripecoinpayments: implement invoice price override
This change allows for overriding project usage prices for a specific partner so that users who sign up with that partner do not need their invoices to be manually adjusted. Relates to storj/storj-private#90 Change-Id: Ia54a9cc7c2f8064922bbb15861f974e5dea82d5a
This commit is contained in:
parent
5644fb1a7e
commit
5d656e66bf
@ -71,6 +71,7 @@ func setupPayments(log *zap.Logger, db satellite.DB) (*stripecoinpayments.Servic
|
|||||||
db.Wallets(),
|
db.Wallets(),
|
||||||
db.Billing(),
|
db.Billing(),
|
||||||
db.Console().Projects(),
|
db.Console().Projects(),
|
||||||
|
db.Console().Users(),
|
||||||
db.ProjectAccounting(),
|
db.ProjectAccounting(),
|
||||||
prices,
|
prices,
|
||||||
priceOverrides,
|
priceOverrides,
|
||||||
|
@ -168,6 +168,7 @@ func NewAdmin(log *zap.Logger, full *identity.FullIdentity, db DB, metabaseDB *m
|
|||||||
peer.DB.Wallets(),
|
peer.DB.Wallets(),
|
||||||
peer.DB.Billing(),
|
peer.DB.Billing(),
|
||||||
peer.DB.Console().Projects(),
|
peer.DB.Console().Projects(),
|
||||||
|
peer.DB.Console().Users(),
|
||||||
peer.DB.ProjectAccounting(),
|
peer.DB.ProjectAccounting(),
|
||||||
prices,
|
prices,
|
||||||
priceOverrides,
|
priceOverrides,
|
||||||
|
@ -561,6 +561,7 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
|
|||||||
peer.DB.Wallets(),
|
peer.DB.Wallets(),
|
||||||
peer.DB.Billing(),
|
peer.DB.Billing(),
|
||||||
peer.DB.Console().Projects(),
|
peer.DB.Console().Projects(),
|
||||||
|
peer.DB.Console().Users(),
|
||||||
peer.DB.ProjectAccounting(),
|
peer.DB.ProjectAccounting(),
|
||||||
prices,
|
prices,
|
||||||
priceOverrides,
|
priceOverrides,
|
||||||
|
@ -98,6 +98,7 @@ func TestGraphqlMutation(t *testing.T) {
|
|||||||
db.Wallets(),
|
db.Wallets(),
|
||||||
db.Billing(),
|
db.Billing(),
|
||||||
db.Console().Projects(),
|
db.Console().Projects(),
|
||||||
|
db.Console().Users(),
|
||||||
db.ProjectAccounting(),
|
db.ProjectAccounting(),
|
||||||
prices,
|
prices,
|
||||||
priceOverrides,
|
priceOverrides,
|
||||||
|
@ -82,6 +82,7 @@ func TestGraphqlQuery(t *testing.T) {
|
|||||||
db.Wallets(),
|
db.Wallets(),
|
||||||
db.Billing(),
|
db.Billing(),
|
||||||
db.Console().Projects(),
|
db.Console().Projects(),
|
||||||
|
db.Console().Users(),
|
||||||
db.ProjectAccounting(),
|
db.ProjectAccounting(),
|
||||||
prices,
|
prices,
|
||||||
priceOverrides,
|
priceOverrides,
|
||||||
|
@ -537,6 +537,7 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB,
|
|||||||
peer.DB.Wallets(),
|
peer.DB.Wallets(),
|
||||||
peer.DB.Billing(),
|
peer.DB.Billing(),
|
||||||
peer.DB.Console().Projects(),
|
peer.DB.Console().Projects(),
|
||||||
|
peer.DB.Console().Users(),
|
||||||
peer.DB.ProjectAccounting(),
|
peer.DB.ProjectAccounting(),
|
||||||
prices,
|
prices,
|
||||||
priceOverrides,
|
priceOverrides,
|
||||||
|
@ -29,6 +29,11 @@ type Accounts interface {
|
|||||||
// ProjectCharges returns how much money current user will be charged for each project.
|
// ProjectCharges returns how much money current user will be charged for each project.
|
||||||
ProjectCharges(ctx context.Context, userID uuid.UUID, since, before time.Time) ([]ProjectCharge, error)
|
ProjectCharges(ctx context.Context, userID uuid.UUID, since, before time.Time) ([]ProjectCharge, error)
|
||||||
|
|
||||||
|
// GetProjectUsagePriceModel returns the project usage price model for a user agent.
|
||||||
|
// If the user agent is malformed or does not contain a valid partner ID, the default
|
||||||
|
// price model is returned.
|
||||||
|
GetProjectUsagePriceModel(userAgent []byte) ProjectUsagePriceModel
|
||||||
|
|
||||||
// CheckProjectInvoicingStatus returns error if for the given project there are outstanding project records and/or usage
|
// CheckProjectInvoicingStatus returns error if for the given project there are outstanding project records and/or usage
|
||||||
// which have not been applied/invoiced yet (meaning sent over to stripe).
|
// which have not been applied/invoiced yet (meaning sent over to stripe).
|
||||||
CheckProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) error
|
CheckProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) error
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"github.com/zeebo/errs"
|
"github.com/zeebo/errs"
|
||||||
|
|
||||||
|
"storj.io/storj/satellite/payments"
|
||||||
"storj.io/storj/satellite/payments/billing"
|
"storj.io/storj/satellite/payments/billing"
|
||||||
"storj.io/storj/satellite/payments/storjscan"
|
"storj.io/storj/satellite/payments/storjscan"
|
||||||
"storj.io/storj/satellite/payments/stripecoinpayments"
|
"storj.io/storj/satellite/payments/stripecoinpayments"
|
||||||
@ -41,8 +42,8 @@ type ProjectUsagePrice struct {
|
|||||||
Segment string `help:"price user should pay for segments stored on network per month in dollars/segment" default:"0.0000088" testDefault:"0.0000022"`
|
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 stripecoinpayments.ProjectUsagePriceModel representation of the project usage price.
|
// ToModel returns the payments.ProjectUsagePriceModel representation of the project usage price.
|
||||||
func (p ProjectUsagePrice) ToModel() (model stripecoinpayments.ProjectUsagePriceModel, err error) {
|
func (p ProjectUsagePrice) ToModel() (model payments.ProjectUsagePriceModel, err error) {
|
||||||
storageTBMonthDollars, err := decimal.NewFromString(p.StorageTB)
|
storageTBMonthDollars, err := decimal.NewFromString(p.StorageTB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model, Error.Wrap(err)
|
return model, Error.Wrap(err)
|
||||||
@ -57,7 +58,7 @@ func (p ProjectUsagePrice) ToModel() (model stripecoinpayments.ProjectUsagePrice
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Shift is to change the precision from TB dollars to MB cents
|
// Shift is to change the precision from TB dollars to MB cents
|
||||||
return stripecoinpayments.ProjectUsagePriceModel{
|
return payments.ProjectUsagePriceModel{
|
||||||
StorageMBMonthCents: storageTBMonthDollars.Shift(-6).Shift(2),
|
StorageMBMonthCents: storageTBMonthDollars.Shift(-6).Shift(2),
|
||||||
EgressMBCents: egressTBDollars.Shift(-6).Shift(2),
|
EgressMBCents: egressTBDollars.Shift(-6).Shift(2),
|
||||||
SegmentMonthCents: segmentMonthDollars.Shift(2),
|
SegmentMonthCents: segmentMonthDollars.Shift(2),
|
||||||
@ -132,9 +133,14 @@ func (p *ProjectUsagePriceOverrides) Set(s string) error {
|
|||||||
return nil
|
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.
|
// ToModels returns the price overrides represented as a mapping between partners and project usage price models.
|
||||||
func (p ProjectUsagePriceOverrides) ToModels() (map[string]stripecoinpayments.ProjectUsagePriceModel, error) {
|
func (p ProjectUsagePriceOverrides) ToModels() (map[string]payments.ProjectUsagePriceModel, error) {
|
||||||
models := make(map[string]stripecoinpayments.ProjectUsagePriceModel)
|
models := make(map[string]payments.ProjectUsagePriceModel)
|
||||||
for partner, prices := range p.overrideMap {
|
for partner, prices := range p.overrideMap {
|
||||||
model, err := prices.ToModel()
|
model, err := prices.ToModel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -11,12 +11,12 @@ import (
|
|||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"storj.io/storj/satellite/payments"
|
||||||
"storj.io/storj/satellite/payments/paymentsconfig"
|
"storj.io/storj/satellite/payments/paymentsconfig"
|
||||||
"storj.io/storj/satellite/payments/stripecoinpayments"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProjectUsagePriceOverrides(t *testing.T) {
|
func TestProjectUsagePriceOverrides(t *testing.T) {
|
||||||
type Prices map[string]stripecoinpayments.ProjectUsagePriceModel
|
type Prices map[string]payments.ProjectUsagePriceModel
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
testID string
|
testID string
|
||||||
@ -41,7 +41,7 @@ func TestProjectUsagePriceOverrides(t *testing.T) {
|
|||||||
configValue: "partner:1,2,3",
|
configValue: "partner:1,2,3",
|
||||||
expectedModel: Prices{
|
expectedModel: Prices{
|
||||||
// Shift is to change the precision from TB dollars to MB cents
|
// Shift is to change the precision from TB dollars to MB cents
|
||||||
"partner": stripecoinpayments.ProjectUsagePriceModel{
|
"partner": payments.ProjectUsagePriceModel{
|
||||||
StorageMBMonthCents: decimal.NewFromInt(1).Shift(-4),
|
StorageMBMonthCents: decimal.NewFromInt(1).Shift(-4),
|
||||||
EgressMBCents: decimal.NewFromInt(2).Shift(-4),
|
EgressMBCents: decimal.NewFromInt(2).Shift(-4),
|
||||||
SegmentMonthCents: decimal.NewFromInt(3).Shift(2),
|
SegmentMonthCents: decimal.NewFromInt(3).Shift(2),
|
||||||
@ -57,12 +57,12 @@ func TestProjectUsagePriceOverrides(t *testing.T) {
|
|||||||
testID: "multiple price overrides",
|
testID: "multiple price overrides",
|
||||||
configValue: "partner1:1,2,3;partner2:4,5,6",
|
configValue: "partner1:1,2,3;partner2:4,5,6",
|
||||||
expectedModel: Prices{
|
expectedModel: Prices{
|
||||||
"partner1": stripecoinpayments.ProjectUsagePriceModel{
|
"partner1": payments.ProjectUsagePriceModel{
|
||||||
StorageMBMonthCents: decimal.NewFromInt(1).Shift(-4),
|
StorageMBMonthCents: decimal.NewFromInt(1).Shift(-4),
|
||||||
EgressMBCents: decimal.NewFromInt(2).Shift(-4),
|
EgressMBCents: decimal.NewFromInt(2).Shift(-4),
|
||||||
SegmentMonthCents: decimal.NewFromInt(3).Shift(2),
|
SegmentMonthCents: decimal.NewFromInt(3).Shift(2),
|
||||||
},
|
},
|
||||||
"partner2": stripecoinpayments.ProjectUsagePriceModel{
|
"partner2": payments.ProjectUsagePriceModel{
|
||||||
StorageMBMonthCents: decimal.NewFromInt(4).Shift(-4),
|
StorageMBMonthCents: decimal.NewFromInt(4).Shift(-4),
|
||||||
EgressMBCents: decimal.NewFromInt(5).Shift(-4),
|
EgressMBCents: decimal.NewFromInt(5).Shift(-4),
|
||||||
SegmentMonthCents: decimal.NewFromInt(6).Shift(2),
|
SegmentMonthCents: decimal.NewFromInt(6).Shift(2),
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
package payments
|
package payments
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
|
||||||
"storj.io/common/uuid"
|
"storj.io/common/uuid"
|
||||||
"storj.io/storj/satellite/accounting"
|
"storj.io/storj/satellite/accounting"
|
||||||
)
|
)
|
||||||
@ -20,3 +22,10 @@ type ProjectCharge struct {
|
|||||||
// SegmentCount shows how many cents we should pay for objects count.
|
// SegmentCount shows how many cents we should pay for objects count.
|
||||||
SegmentCount int64 `json:"segmentPrice"`
|
SegmentCount int64 `json:"segmentPrice"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProjectUsagePriceModel represents price model for project usage.
|
||||||
|
type ProjectUsagePriceModel struct {
|
||||||
|
StorageMBMonthCents decimal.Decimal `json:"storageMBMonthCents"`
|
||||||
|
EgressMBCents decimal.Decimal `json:"egressMBCents"`
|
||||||
|
SegmentMonthCents decimal.Decimal `json:"segmentMonthCents"`
|
||||||
|
}
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/stripe/stripe-go/v72"
|
"github.com/stripe/stripe-go/v72"
|
||||||
"github.com/zeebo/errs"
|
"github.com/zeebo/errs"
|
||||||
|
|
||||||
|
"storj.io/common/useragent"
|
||||||
"storj.io/common/uuid"
|
"storj.io/common/uuid"
|
||||||
"storj.io/storj/satellite/payments"
|
"storj.io/storj/satellite/payments"
|
||||||
)
|
)
|
||||||
@ -119,13 +120,19 @@ func (accounts *accounts) ProjectCharges(ctx context.Context, userID uuid.UUID,
|
|||||||
return nil, Error.Wrap(err)
|
return nil, Error.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user, err := accounts.service.usersDB.Get(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Error.Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
for _, project := range projects {
|
for _, project := range projects {
|
||||||
usage, err := accounts.service.usageDB.GetProjectTotal(ctx, project.ID, since, before)
|
usage, err := accounts.service.usageDB.GetProjectTotal(ctx, project.ID, since, before)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return charges, Error.Wrap(err)
|
return charges, Error.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
projectPrice := accounts.service.calculateProjectUsagePrice(usage.Egress, usage.Storage, usage.SegmentCount)
|
pricing := accounts.GetProjectUsagePriceModel(user.UserAgent)
|
||||||
|
projectPrice := accounts.service.calculateProjectUsagePrice(usage.Egress, usage.Storage, usage.SegmentCount, pricing)
|
||||||
|
|
||||||
charges = append(charges, payments.ProjectCharge{
|
charges = append(charges, payments.ProjectCharge{
|
||||||
ProjectUsage: *usage,
|
ProjectUsage: *usage,
|
||||||
@ -140,6 +147,25 @@ func (accounts *accounts) ProjectCharges(ctx context.Context, userID uuid.UUID,
|
|||||||
return charges, nil
|
return charges, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetProjectUsagePriceModel returns the project usage price model for a user agent.
|
||||||
|
// If the user agent is malformed or does not contain a valid partner ID, the default
|
||||||
|
// price model is returned.
|
||||||
|
func (accounts *accounts) GetProjectUsagePriceModel(userAgent []byte) payments.ProjectUsagePriceModel {
|
||||||
|
if userAgent == nil {
|
||||||
|
return accounts.service.usagePrices
|
||||||
|
}
|
||||||
|
entries, err := useragent.ParseEntries(userAgent)
|
||||||
|
if err != nil {
|
||||||
|
return accounts.service.usagePrices
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if override, ok := accounts.service.usagePriceOverrides[entry.Product]; ok {
|
||||||
|
return override
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return accounts.service.usagePrices
|
||||||
|
}
|
||||||
|
|
||||||
// CheckProjectInvoicingStatus returns error if for the given project there are outstanding project records and/or usage
|
// CheckProjectInvoicingStatus returns error if for the given project there are outstanding project records and/or usage
|
||||||
// which have not been applied/invoiced yet (meaning sent over to stripe).
|
// which have not been applied/invoiced yet (meaning sent over to stripe).
|
||||||
func (accounts *accounts) CheckProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) (err error) {
|
func (accounts *accounts) CheckProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) (err error) {
|
||||||
|
@ -76,6 +76,7 @@ func TestSignupCouponCodes(t *testing.T) {
|
|||||||
db.Wallets(),
|
db.Wallets(),
|
||||||
db.Billing(),
|
db.Billing(),
|
||||||
db.Console().Projects(),
|
db.Console().Projects(),
|
||||||
|
db.Console().Users(),
|
||||||
db.ProjectAccounting(),
|
db.ProjectAccounting(),
|
||||||
prices,
|
prices,
|
||||||
priceOverrides,
|
priceOverrides,
|
||||||
|
@ -61,11 +61,12 @@ type Service struct {
|
|||||||
billingDB billing.TransactionsDB
|
billingDB billing.TransactionsDB
|
||||||
|
|
||||||
projectsDB console.Projects
|
projectsDB console.Projects
|
||||||
|
usersDB console.Users
|
||||||
usageDB accounting.ProjectAccounting
|
usageDB accounting.ProjectAccounting
|
||||||
stripeClient StripeClient
|
stripeClient StripeClient
|
||||||
|
|
||||||
usagePrices ProjectUsagePriceModel
|
usagePrices payments.ProjectUsagePriceModel
|
||||||
usagePriceOverrides map[string]ProjectUsagePriceModel
|
usagePriceOverrides map[string]payments.ProjectUsagePriceModel
|
||||||
// BonusRate amount of percents
|
// BonusRate amount of percents
|
||||||
BonusRate int64
|
BonusRate int64
|
||||||
// Coupon Values
|
// Coupon Values
|
||||||
@ -79,21 +80,15 @@ type Service struct {
|
|||||||
nowFn func() time.Time
|
nowFn func() time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProjectUsagePriceModel represents price model for project usage.
|
|
||||||
type ProjectUsagePriceModel struct {
|
|
||||||
StorageMBMonthCents decimal.Decimal
|
|
||||||
EgressMBCents decimal.Decimal
|
|
||||||
SegmentMonthCents decimal.Decimal
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewService creates a Service instance.
|
// NewService creates a Service instance.
|
||||||
func NewService(log *zap.Logger, stripeClient StripeClient, config Config, db DB, walletsDB storjscan.WalletsDB, billingDB billing.TransactionsDB, projectsDB console.Projects, usageDB accounting.ProjectAccounting, usagePrices ProjectUsagePriceModel, usagePriceOverrides map[string]ProjectUsagePriceModel, bonusRate int64) (*Service, error) {
|
func NewService(log *zap.Logger, stripeClient StripeClient, config Config, db DB, walletsDB storjscan.WalletsDB, billingDB billing.TransactionsDB, projectsDB console.Projects, usersDB console.Users, usageDB accounting.ProjectAccounting, usagePrices payments.ProjectUsagePriceModel, usagePriceOverrides map[string]payments.ProjectUsagePriceModel, bonusRate int64) (*Service, error) {
|
||||||
return &Service{
|
return &Service{
|
||||||
log: log,
|
log: log,
|
||||||
db: db,
|
db: db,
|
||||||
walletsDB: walletsDB,
|
walletsDB: walletsDB,
|
||||||
billingDB: billingDB,
|
billingDB: billingDB,
|
||||||
projectsDB: projectsDB,
|
projectsDB: projectsDB,
|
||||||
|
usersDB: usersDB,
|
||||||
usageDB: usageDB,
|
usageDB: usageDB,
|
||||||
stripeClient: stripeClient,
|
stripeClient: stripeClient,
|
||||||
usagePrices: usagePrices,
|
usagePrices: usagePrices,
|
||||||
@ -463,7 +458,14 @@ func (service *Service) applyProjectRecords(ctx context.Context, records []Proje
|
|||||||
return 0, errs.Wrap(err)
|
return 0, errs.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if skipped, err := service.createInvoiceItems(ctx, cusID, proj.Name, record); err != nil {
|
owner, err := service.usersDB.Get(ctx, proj.OwnerID)
|
||||||
|
if err != nil {
|
||||||
|
service.log.Error("Owner does not exist for project.", zap.Stringer("Owner ID", proj.OwnerID), zap.Stringer("Project ID", record.ProjectID))
|
||||||
|
return 0, errs.Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pricing := service.Accounts().GetProjectUsagePriceModel(owner.UserAgent)
|
||||||
|
if skipped, err := service.createInvoiceItems(ctx, cusID, proj.Name, record, pricing); err != nil {
|
||||||
return 0, errs.Wrap(err)
|
return 0, errs.Wrap(err)
|
||||||
} else if skipped {
|
} else if skipped {
|
||||||
skipCount++
|
skipCount++
|
||||||
@ -474,7 +476,7 @@ func (service *Service) applyProjectRecords(ctx context.Context, records []Proje
|
|||||||
}
|
}
|
||||||
|
|
||||||
// createInvoiceItems creates invoice line items for stripe customer.
|
// createInvoiceItems creates invoice line items for stripe customer.
|
||||||
func (service *Service) createInvoiceItems(ctx context.Context, cusID, projName string, record ProjectRecord) (skipped bool, err error) {
|
func (service *Service) createInvoiceItems(ctx context.Context, cusID, projName string, record ProjectRecord, priceModel payments.ProjectUsagePriceModel) (skipped bool, err error) {
|
||||||
defer mon.Task()(&ctx)(&err)
|
defer mon.Task()(&ctx)(&err)
|
||||||
|
|
||||||
if err = service.db.ProjectRecords().Consume(ctx, record.ID); err != nil {
|
if err = service.db.ProjectRecords().Consume(ctx, record.ID); err != nil {
|
||||||
@ -485,7 +487,7 @@ func (service *Service) createInvoiceItems(ctx context.Context, cusID, projName
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
items := service.InvoiceItemsFromProjectRecord(projName, record)
|
items := service.InvoiceItemsFromProjectRecord(projName, record, priceModel)
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
item.Currency = stripe.String(string(stripe.CurrencyUSD))
|
item.Currency = stripe.String(string(stripe.CurrencyUSD))
|
||||||
item.Customer = stripe.String(cusID)
|
item.Customer = stripe.String(cusID)
|
||||||
@ -501,25 +503,25 @@ func (service *Service) createInvoiceItems(ctx context.Context, cusID, projName
|
|||||||
}
|
}
|
||||||
|
|
||||||
// InvoiceItemsFromProjectRecord calculates Stripe invoice item from project record.
|
// InvoiceItemsFromProjectRecord calculates Stripe invoice item from project record.
|
||||||
func (service *Service) InvoiceItemsFromProjectRecord(projName string, record ProjectRecord) (result []*stripe.InvoiceItemParams) {
|
func (service *Service) InvoiceItemsFromProjectRecord(projName string, record ProjectRecord, priceModel payments.ProjectUsagePriceModel) (result []*stripe.InvoiceItemParams) {
|
||||||
projectItem := &stripe.InvoiceItemParams{}
|
projectItem := &stripe.InvoiceItemParams{}
|
||||||
projectItem.Description = stripe.String(fmt.Sprintf("Project %s - Segment Storage (MB-Month)", projName))
|
projectItem.Description = stripe.String(fmt.Sprintf("Project %s - Segment Storage (MB-Month)", projName))
|
||||||
projectItem.Quantity = stripe.Int64(storageMBMonthDecimal(record.Storage).IntPart())
|
projectItem.Quantity = stripe.Int64(storageMBMonthDecimal(record.Storage).IntPart())
|
||||||
storagePrice, _ := service.usagePrices.StorageMBMonthCents.Float64()
|
storagePrice, _ := priceModel.StorageMBMonthCents.Float64()
|
||||||
projectItem.UnitAmountDecimal = stripe.Float64(storagePrice)
|
projectItem.UnitAmountDecimal = stripe.Float64(storagePrice)
|
||||||
result = append(result, projectItem)
|
result = append(result, projectItem)
|
||||||
|
|
||||||
projectItem = &stripe.InvoiceItemParams{}
|
projectItem = &stripe.InvoiceItemParams{}
|
||||||
projectItem.Description = stripe.String(fmt.Sprintf("Project %s - Egress Bandwidth (MB)", projName))
|
projectItem.Description = stripe.String(fmt.Sprintf("Project %s - Egress Bandwidth (MB)", projName))
|
||||||
projectItem.Quantity = stripe.Int64(egressMBDecimal(record.Egress).IntPart())
|
projectItem.Quantity = stripe.Int64(egressMBDecimal(record.Egress).IntPart())
|
||||||
egressPrice, _ := service.usagePrices.EgressMBCents.Float64()
|
egressPrice, _ := priceModel.EgressMBCents.Float64()
|
||||||
projectItem.UnitAmountDecimal = stripe.Float64(egressPrice)
|
projectItem.UnitAmountDecimal = stripe.Float64(egressPrice)
|
||||||
result = append(result, projectItem)
|
result = append(result, projectItem)
|
||||||
|
|
||||||
projectItem = &stripe.InvoiceItemParams{}
|
projectItem = &stripe.InvoiceItemParams{}
|
||||||
projectItem.Description = stripe.String(fmt.Sprintf("Project %s - Segment Fee (Segment-Month)", projName))
|
projectItem.Description = stripe.String(fmt.Sprintf("Project %s - Segment Fee (Segment-Month)", projName))
|
||||||
projectItem.Quantity = stripe.Int64(segmentMonthDecimal(record.Segments).IntPart())
|
projectItem.Quantity = stripe.Int64(segmentMonthDecimal(record.Segments).IntPart())
|
||||||
segmentPrice, _ := service.usagePrices.SegmentMonthCents.Float64()
|
segmentPrice, _ := priceModel.SegmentMonthCents.Float64()
|
||||||
projectItem.UnitAmountDecimal = stripe.Float64(segmentPrice)
|
projectItem.UnitAmountDecimal = stripe.Float64(segmentPrice)
|
||||||
result = append(result, projectItem)
|
result = append(result, projectItem)
|
||||||
service.log.Info("invoice items", zap.Any("result", result))
|
service.log.Info("invoice items", zap.Any("result", result))
|
||||||
@ -772,11 +774,11 @@ func (price projectUsagePrice) TotalInt64() int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// calculateProjectUsagePrice calculate project usage price.
|
// calculateProjectUsagePrice calculate project usage price.
|
||||||
func (service *Service) calculateProjectUsagePrice(egress int64, storage, segments float64) projectUsagePrice {
|
func (service *Service) calculateProjectUsagePrice(egress int64, storage, segments float64, pricing payments.ProjectUsagePriceModel) projectUsagePrice {
|
||||||
return projectUsagePrice{
|
return projectUsagePrice{
|
||||||
Storage: service.usagePrices.StorageMBMonthCents.Mul(storageMBMonthDecimal(storage)).Round(0),
|
Storage: pricing.StorageMBMonthCents.Mul(storageMBMonthDecimal(storage)).Round(0),
|
||||||
Egress: service.usagePrices.EgressMBCents.Mul(egressMBDecimal(egress)).Round(0),
|
Egress: pricing.EgressMBCents.Mul(egressMBDecimal(egress)).Round(0),
|
||||||
Segments: service.usagePrices.SegmentMonthCents.Mul(segmentMonthDecimal(segments)).Round(0),
|
Segments: pricing.SegmentMonthCents.Mul(segmentMonthDecimal(segments)).Round(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ package stripecoinpayments_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -25,7 +26,9 @@ import (
|
|||||||
"storj.io/storj/satellite/accounting"
|
"storj.io/storj/satellite/accounting"
|
||||||
"storj.io/storj/satellite/console"
|
"storj.io/storj/satellite/console"
|
||||||
"storj.io/storj/satellite/metabase"
|
"storj.io/storj/satellite/metabase"
|
||||||
|
"storj.io/storj/satellite/payments"
|
||||||
"storj.io/storj/satellite/payments/billing"
|
"storj.io/storj/satellite/payments/billing"
|
||||||
|
"storj.io/storj/satellite/payments/paymentsconfig"
|
||||||
"storj.io/storj/satellite/payments/stripecoinpayments"
|
"storj.io/storj/satellite/payments/stripecoinpayments"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -273,7 +276,8 @@ func TestService_InvoiceItemsFromProjectRecord(t *testing.T) {
|
|||||||
Segments: tc.Segments,
|
Segments: tc.Segments,
|
||||||
}
|
}
|
||||||
|
|
||||||
items := satellite.API.Payments.StripeService.InvoiceItemsFromProjectRecord("project name", record)
|
pricing := satellite.API.Payments.Accounts.GetProjectUsagePriceModel(nil)
|
||||||
|
items := satellite.API.Payments.StripeService.InvoiceItemsFromProjectRecord("project name", record, pricing)
|
||||||
|
|
||||||
require.Equal(t, tc.StorageQuantity, *items[0].Quantity)
|
require.Equal(t, tc.StorageQuantity, *items[0].Quantity)
|
||||||
require.Equal(t, expectedStoragePrice, *items[0].UnitAmountDecimal)
|
require.Equal(t, expectedStoragePrice, *items[0].UnitAmountDecimal)
|
||||||
@ -489,3 +493,89 @@ func generateProjectStorage(ctx context.Context, tb testing.TB, db satellite.DB,
|
|||||||
err = db.ProjectAccounting().SaveTallies(ctx, end, tallies)
|
err = db.ProjectAccounting().SaveTallies(ctx, end, tallies)
|
||||||
require.NoError(tb, err)
|
require.NoError(tb, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProjectUsagePrice(t *testing.T) {
|
||||||
|
var (
|
||||||
|
defaultPrice = paymentsconfig.ProjectUsagePrice{
|
||||||
|
StorageTB: "1",
|
||||||
|
EgressTB: "2",
|
||||||
|
Segment: "3",
|
||||||
|
}
|
||||||
|
partnerName = "partner"
|
||||||
|
partnerPrice = paymentsconfig.ProjectUsagePrice{
|
||||||
|
StorageTB: "4",
|
||||||
|
EgressTB: "5",
|
||||||
|
Segment: "6",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
defaultModel, err := defaultPrice.ToModel()
|
||||||
|
require.NoError(t, err)
|
||||||
|
partnerModel, err := partnerPrice.ToModel()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
testplanet.Run(t, testplanet.Config{
|
||||||
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
||||||
|
Reconfigure: testplanet.Reconfigure{
|
||||||
|
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
||||||
|
config.Payments.UsagePrice = defaultPrice
|
||||||
|
config.Payments.UsagePriceOverrides.SetMap(map[string]paymentsconfig.ProjectUsagePrice{
|
||||||
|
partnerName: partnerPrice,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||||
|
sat := planet.Satellites[0]
|
||||||
|
|
||||||
|
// pick a specific date so that it doesn't fail if it's the last day of the month
|
||||||
|
// keep month + 1 because user needs to be created before calculation
|
||||||
|
period := time.Date(time.Now().Year(), time.Now().Month()+1, 20, 0, 0, 0, 0, time.UTC)
|
||||||
|
sat.API.Payments.StripeService.SetNow(func() time.Time {
|
||||||
|
return time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
})
|
||||||
|
|
||||||
|
for i, tt := range []struct {
|
||||||
|
name string
|
||||||
|
userAgent []byte
|
||||||
|
expectedPrice payments.ProjectUsagePriceModel
|
||||||
|
}{
|
||||||
|
{"default pricing", nil, defaultModel},
|
||||||
|
{"default pricing - user agent is not valid partner ID", []byte("invalid/v0.0"), defaultModel},
|
||||||
|
{"partner pricing - user agent is partner ID", []byte(partnerName), partnerModel},
|
||||||
|
{"partner pricing - user agent includes partner ID", []byte("invalid/v0.0 " + partnerName + " invalid/v0.0"), partnerModel},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
user, err := sat.AddUser(ctx, console.CreateUser{
|
||||||
|
FullName: "Test User",
|
||||||
|
Email: fmt.Sprintf("user%d@mail.test", i),
|
||||||
|
UserAgent: tt.userAgent,
|
||||||
|
}, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
project, err := sat.AddProject(ctx, user.ID, "testproject")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = sat.DB.Orders().UpdateBucketBandwidthSettle(ctx, project.ID, []byte("testbucket"),
|
||||||
|
pb.PieceAction_GET, memory.TB.Int64(), 0, period)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = sat.API.Payments.StripeService.PrepareInvoiceProjectRecords(ctx, period)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = sat.API.Payments.StripeService.InvoiceApplyProjectRecords(ctx, period)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cusID, err := sat.DB.StripeCoinPayments().Customers().GetCustomerID(ctx, user.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
items := getCustomerInvoiceItems(sat.API.Payments.StripeClient, cusID)
|
||||||
|
require.Len(t, items, 3)
|
||||||
|
storage, _ := tt.expectedPrice.StorageMBMonthCents.Float64()
|
||||||
|
require.Equal(t, storage, items[0].UnitAmountDecimal)
|
||||||
|
egress, _ := tt.expectedPrice.EgressMBCents.Float64()
|
||||||
|
require.Equal(t, egress, items[1].UnitAmountDecimal)
|
||||||
|
segment, _ := tt.expectedPrice.SegmentMonthCents.Float64()
|
||||||
|
require.Equal(t, segment, items[2].UnitAmountDecimal)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -528,6 +528,9 @@ func (m *mockInvoiceItems) New(params *stripe.InvoiceItemParams) (*stripe.Invoic
|
|||||||
item := &stripe.InvoiceItem{
|
item := &stripe.InvoiceItem{
|
||||||
Metadata: params.Metadata,
|
Metadata: params.Metadata,
|
||||||
}
|
}
|
||||||
|
if params.UnitAmountDecimal != nil {
|
||||||
|
item.UnitAmountDecimal = *params.UnitAmountDecimal
|
||||||
|
}
|
||||||
m.items[*params.Customer] = append(m.items[*params.Customer], item)
|
m.items[*params.Customer] = append(m.items[*params.Customer], item)
|
||||||
|
|
||||||
return item, nil
|
return item, nil
|
||||||
|
Loading…
Reference in New Issue
Block a user