satellite/payments/stripecoinpayments: make price overrides per-bucket
This change causes the bucket's partner info to be used rather than the user's when calculating project usage prices. This ensures that users who own differently-partnered buckets will be charged correctly for usage based on the specific bucket they are utilizing. according to the bucket's partner. Related to storj/storj-private#90 Change-Id: Ieeedfcc5451e254216918dcc9f096758be6a8961
This commit is contained in:
parent
0596651580
commit
091ed29935
@ -71,7 +71,6 @@ func setupPayments(log *zap.Logger, db satellite.DB) (*stripecoinpayments.Servic
|
||||
db.Wallets(),
|
||||
db.Billing(),
|
||||
db.Console().Projects(),
|
||||
db.Console().Users(),
|
||||
db.ProjectAccounting(),
|
||||
prices,
|
||||
priceOverrides,
|
||||
|
@ -236,6 +236,9 @@ type ProjectAccounting interface {
|
||||
GetProjectLimits(ctx context.Context, projectID uuid.UUID) (ProjectLimits, error)
|
||||
// GetProjectTotal returns project usage summary for specified period of time.
|
||||
GetProjectTotal(ctx context.Context, projectID uuid.UUID, since, before time.Time) (*ProjectUsage, error)
|
||||
// GetProjectTotalByPartner retrieves project usage for a given period categorized by partner name.
|
||||
// Unpartnered usage or usage for a partner not present in partnerNames is mapped to the empty string.
|
||||
GetProjectTotalByPartner(ctx context.Context, projectID uuid.UUID, partnerNames []string, since, before time.Time) (usages map[string]ProjectUsage, err error)
|
||||
// GetProjectObjectsSegments returns project objects and segments for specified period of time.
|
||||
GetProjectObjectsSegments(ctx context.Context, projectID uuid.UUID) (*ProjectObjectsSegments, error)
|
||||
// GetBucketUsageRollups returns usage rollup per each bucket for specified period of time.
|
||||
|
@ -168,7 +168,6 @@ func NewAdmin(log *zap.Logger, full *identity.FullIdentity, db DB, metabaseDB *m
|
||||
peer.DB.Wallets(),
|
||||
peer.DB.Billing(),
|
||||
peer.DB.Console().Projects(),
|
||||
peer.DB.Console().Users(),
|
||||
peer.DB.ProjectAccounting(),
|
||||
prices,
|
||||
priceOverrides,
|
||||
|
@ -548,7 +548,6 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
|
||||
peer.DB.Wallets(),
|
||||
peer.DB.Billing(),
|
||||
peer.DB.Console().Projects(),
|
||||
peer.DB.Console().Users(),
|
||||
peer.DB.ProjectAccounting(),
|
||||
prices,
|
||||
priceOverrides,
|
||||
|
@ -92,7 +92,6 @@ func TestGraphqlMutation(t *testing.T) {
|
||||
db.Wallets(),
|
||||
db.Billing(),
|
||||
db.Console().Projects(),
|
||||
db.Console().Users(),
|
||||
db.ProjectAccounting(),
|
||||
prices,
|
||||
priceOverrides,
|
||||
|
@ -76,7 +76,6 @@ func TestGraphqlQuery(t *testing.T) {
|
||||
db.Wallets(),
|
||||
db.Billing(),
|
||||
db.Console().Projects(),
|
||||
db.Console().Users(),
|
||||
db.ProjectAccounting(),
|
||||
prices,
|
||||
priceOverrides,
|
||||
|
@ -3044,7 +3044,7 @@ func (payment Payments) GetProjectUsagePriceModel(ctx context.Context) (_ *payme
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
model := payment.service.accounts.GetProjectUsagePriceModel(user.UserAgent)
|
||||
model := payment.service.accounts.GetProjectUsagePriceModel(string(user.UserAgent))
|
||||
return &model, nil
|
||||
}
|
||||
|
||||
|
@ -542,7 +542,6 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB,
|
||||
peer.DB.Wallets(),
|
||||
peer.DB.Billing(),
|
||||
peer.DB.Console().Projects(),
|
||||
peer.DB.Console().Users(),
|
||||
peer.DB.ProjectAccounting(),
|
||||
prices,
|
||||
priceOverrides,
|
||||
|
@ -29,10 +29,8 @@ type Accounts interface {
|
||||
// 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)
|
||||
|
||||
// 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
|
||||
// GetProjectUsagePriceModel returns the project usage price model for a partner name.
|
||||
GetProjectUsagePriceModel(partner string) ProjectUsagePriceModel
|
||||
|
||||
// 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).
|
||||
|
@ -10,8 +10,8 @@ import (
|
||||
"github.com/stripe/stripe-go/v72"
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
"storj.io/common/useragent"
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/storj/satellite/accounting"
|
||||
"storj.io/storj/satellite/payments"
|
||||
)
|
||||
|
||||
@ -120,49 +120,48 @@ func (accounts *accounts) ProjectCharges(ctx context.Context, userID uuid.UUID,
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
user, err := accounts.service.usersDB.Get(ctx, userID)
|
||||
for _, project := range projects {
|
||||
totalUsage := accounting.ProjectUsage{Since: since, Before: before}
|
||||
|
||||
usages, err := accounts.service.usageDB.GetProjectTotalByPartner(ctx, project.ID, accounts.service.partnerNames, since, before)
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
for _, project := range projects {
|
||||
usage, err := accounts.service.usageDB.GetProjectTotal(ctx, project.ID, since, before)
|
||||
if err != nil {
|
||||
return charges, Error.Wrap(err)
|
||||
var totalPrice projectUsagePrice
|
||||
|
||||
for partner, usage := range usages {
|
||||
priceModel := accounts.GetProjectUsagePriceModel(partner)
|
||||
price := accounts.service.calculateProjectUsagePrice(usage.Egress, usage.Storage, usage.SegmentCount, priceModel)
|
||||
|
||||
totalPrice.Egress = totalPrice.Egress.Add(price.Egress)
|
||||
totalPrice.Segments = totalPrice.Segments.Add(price.Segments)
|
||||
totalPrice.Storage = totalPrice.Storage.Add(price.Storage)
|
||||
|
||||
totalUsage.Egress += usage.Egress
|
||||
totalUsage.ObjectCount += usage.ObjectCount
|
||||
totalUsage.SegmentCount += usage.SegmentCount
|
||||
totalUsage.Storage += usage.Storage
|
||||
}
|
||||
|
||||
pricing := accounts.GetProjectUsagePriceModel(user.UserAgent)
|
||||
projectPrice := accounts.service.calculateProjectUsagePrice(usage.Egress, usage.Storage, usage.SegmentCount, pricing)
|
||||
|
||||
charges = append(charges, payments.ProjectCharge{
|
||||
ProjectUsage: *usage,
|
||||
ProjectUsage: totalUsage,
|
||||
|
||||
ProjectID: project.ID,
|
||||
Egress: projectPrice.Egress.IntPart(),
|
||||
SegmentCount: projectPrice.Segments.IntPart(),
|
||||
StorageGbHrs: projectPrice.Storage.IntPart(),
|
||||
Egress: totalPrice.Egress.IntPart(),
|
||||
SegmentCount: totalPrice.Segments.IntPart(),
|
||||
StorageGbHrs: totalPrice.Storage.IntPart(),
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
// GetProjectUsagePriceModel returns the project usage price model for a partner name.
|
||||
func (accounts *accounts) GetProjectUsagePriceModel(partner string) payments.ProjectUsagePriceModel {
|
||||
if override, ok := accounts.service.usagePriceOverrides[partner]; ok {
|
||||
return override
|
||||
}
|
||||
}
|
||||
return accounts.service.usagePrices
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,6 @@ func TestSignupCouponCodes(t *testing.T) {
|
||||
db.Wallets(),
|
||||
db.Billing(),
|
||||
db.Console().Projects(),
|
||||
db.Console().Users(),
|
||||
db.ProjectAccounting(),
|
||||
prices,
|
||||
priceOverrides,
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -61,12 +62,12 @@ type Service struct {
|
||||
billingDB billing.TransactionsDB
|
||||
|
||||
projectsDB console.Projects
|
||||
usersDB console.Users
|
||||
usageDB accounting.ProjectAccounting
|
||||
stripeClient StripeClient
|
||||
|
||||
usagePrices payments.ProjectUsagePriceModel
|
||||
usagePriceOverrides map[string]payments.ProjectUsagePriceModel
|
||||
partnerNames []string
|
||||
// BonusRate amount of percents
|
||||
BonusRate int64
|
||||
// Coupon Values
|
||||
@ -81,18 +82,23 @@ type Service struct {
|
||||
}
|
||||
|
||||
// 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, usersDB console.Users, usageDB accounting.ProjectAccounting, usagePrices payments.ProjectUsagePriceModel, usagePriceOverrides map[string]payments.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, usageDB accounting.ProjectAccounting, usagePrices payments.ProjectUsagePriceModel, usagePriceOverrides map[string]payments.ProjectUsagePriceModel, bonusRate int64) (*Service, error) {
|
||||
var partners []string
|
||||
for partner := range usagePriceOverrides {
|
||||
partners = append(partners, partner)
|
||||
}
|
||||
|
||||
return &Service{
|
||||
log: log,
|
||||
db: db,
|
||||
walletsDB: walletsDB,
|
||||
billingDB: billingDB,
|
||||
projectsDB: projectsDB,
|
||||
usersDB: usersDB,
|
||||
usageDB: usageDB,
|
||||
stripeClient: stripeClient,
|
||||
usagePrices: usagePrices,
|
||||
usagePriceOverrides: usagePriceOverrides,
|
||||
partnerNames: partners,
|
||||
BonusRate: bonusRate,
|
||||
StripeFreeTierCouponID: config.StripeFreeTierCouponID,
|
||||
AutoAdvance: config.AutoAdvance,
|
||||
@ -458,14 +464,7 @@ func (service *Service) applyProjectRecords(ctx context.Context, records []Proje
|
||||
return 0, errs.Wrap(err)
|
||||
}
|
||||
|
||||
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 {
|
||||
if skipped, err := service.createInvoiceItems(ctx, cusID, proj.Name, record); err != nil {
|
||||
return 0, errs.Wrap(err)
|
||||
} else if skipped {
|
||||
skipCount++
|
||||
@ -476,7 +475,7 @@ func (service *Service) applyProjectRecords(ctx context.Context, records []Proje
|
||||
}
|
||||
|
||||
// createInvoiceItems creates invoice line items for stripe customer.
|
||||
func (service *Service) createInvoiceItems(ctx context.Context, cusID, projName string, record ProjectRecord, priceModel payments.ProjectUsagePriceModel) (skipped bool, err error) {
|
||||
func (service *Service) createInvoiceItems(ctx context.Context, cusID, projName string, record ProjectRecord) (skipped bool, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
if err = service.db.ProjectRecords().Consume(ctx, record.ID); err != nil {
|
||||
@ -487,7 +486,12 @@ func (service *Service) createInvoiceItems(ctx context.Context, cusID, projName
|
||||
return true, nil
|
||||
}
|
||||
|
||||
items := service.InvoiceItemsFromProjectRecord(projName, record, priceModel)
|
||||
usages, err := service.usageDB.GetProjectTotalByPartner(ctx, record.ProjectID, service.partnerNames, record.PeriodStart, record.PeriodEnd)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
items := service.InvoiceItemsFromProjectUsage(projName, usages)
|
||||
for _, item := range items {
|
||||
item.Currency = stripe.String(string(stripe.CurrencyUSD))
|
||||
item.Customer = stripe.String(cusID)
|
||||
@ -502,28 +506,50 @@ func (service *Service) createInvoiceItems(ctx context.Context, cusID, projName
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// InvoiceItemsFromProjectRecord calculates Stripe invoice item from project record.
|
||||
func (service *Service) InvoiceItemsFromProjectRecord(projName string, record ProjectRecord, priceModel payments.ProjectUsagePriceModel) (result []*stripe.InvoiceItemParams) {
|
||||
// InvoiceItemsFromProjectUsage calculates Stripe invoice item from project usage.
|
||||
func (service *Service) InvoiceItemsFromProjectUsage(projName string, partnerUsages map[string]accounting.ProjectUsage) (result []*stripe.InvoiceItemParams) {
|
||||
var partners []string
|
||||
if len(partnerUsages) == 0 {
|
||||
partners = []string{""}
|
||||
partnerUsages = map[string]accounting.ProjectUsage{"": {}}
|
||||
} else {
|
||||
for partner := range partnerUsages {
|
||||
partners = append(partners, partner)
|
||||
}
|
||||
sort.Strings(partners)
|
||||
}
|
||||
|
||||
for _, partner := range partners {
|
||||
usage := partnerUsages[partner]
|
||||
priceModel := service.Accounts().GetProjectUsagePriceModel(partner)
|
||||
|
||||
prefix := "Project " + projName
|
||||
if partner != "" {
|
||||
prefix += " (" + partner + ")"
|
||||
}
|
||||
|
||||
projectItem := &stripe.InvoiceItemParams{}
|
||||
projectItem.Description = stripe.String(fmt.Sprintf("Project %s - Segment Storage (MB-Month)", projName))
|
||||
projectItem.Quantity = stripe.Int64(storageMBMonthDecimal(record.Storage).IntPart())
|
||||
projectItem.Description = stripe.String(prefix + " - Segment Storage (MB-Month)")
|
||||
projectItem.Quantity = stripe.Int64(storageMBMonthDecimal(usage.Storage).IntPart())
|
||||
storagePrice, _ := priceModel.StorageMBMonthCents.Float64()
|
||||
projectItem.UnitAmountDecimal = stripe.Float64(storagePrice)
|
||||
result = append(result, projectItem)
|
||||
|
||||
projectItem = &stripe.InvoiceItemParams{}
|
||||
projectItem.Description = stripe.String(fmt.Sprintf("Project %s - Egress Bandwidth (MB)", projName))
|
||||
projectItem.Quantity = stripe.Int64(egressMBDecimal(record.Egress).IntPart())
|
||||
projectItem.Description = stripe.String(prefix + " - Egress Bandwidth (MB)")
|
||||
projectItem.Quantity = stripe.Int64(egressMBDecimal(usage.Egress).IntPart())
|
||||
egressPrice, _ := priceModel.EgressMBCents.Float64()
|
||||
projectItem.UnitAmountDecimal = stripe.Float64(egressPrice)
|
||||
result = append(result, projectItem)
|
||||
|
||||
projectItem = &stripe.InvoiceItemParams{}
|
||||
projectItem.Description = stripe.String(fmt.Sprintf("Project %s - Segment Fee (Segment-Month)", projName))
|
||||
projectItem.Quantity = stripe.Int64(segmentMonthDecimal(record.Segments).IntPart())
|
||||
projectItem.Description = stripe.String(prefix + " - Segment Fee (Segment-Month)")
|
||||
projectItem.Quantity = stripe.Int64(segmentMonthDecimal(usage.SegmentCount).IntPart())
|
||||
segmentPrice, _ := priceModel.SegmentMonthCents.Float64()
|
||||
projectItem.UnitAmountDecimal = stripe.Float64(segmentPrice)
|
||||
result = append(result, projectItem)
|
||||
}
|
||||
|
||||
service.log.Info("invoice items", zap.Any("result", result))
|
||||
|
||||
return result
|
||||
|
@ -6,6 +6,7 @@ package stripecoinpayments_test
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
@ -17,6 +18,7 @@ import (
|
||||
"storj.io/common/currency"
|
||||
"storj.io/common/memory"
|
||||
"storj.io/common/pb"
|
||||
"storj.io/common/storj"
|
||||
"storj.io/common/testcontext"
|
||||
"storj.io/common/testrand"
|
||||
"storj.io/common/uuid"
|
||||
@ -225,68 +227,106 @@ func TestService_ProjectsWithMembers(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_InvoiceItemsFromProjectRecord(t *testing.T) {
|
||||
func TestService_InvoiceItemsFromProjectUsage(t *testing.T) {
|
||||
const (
|
||||
projectName = "my-project"
|
||||
partnerName = "partner"
|
||||
noOverridePartnerName = "no-override"
|
||||
|
||||
hoursPerMonth = 24 * 30
|
||||
bytesPerMegabyte = int64(memory.MB / memory.B)
|
||||
byteHoursPerMBMonth = hoursPerMonth * bytesPerMegabyte
|
||||
)
|
||||
|
||||
var (
|
||||
defaultPrice = paymentsconfig.ProjectUsagePrice{
|
||||
StorageTB: "1",
|
||||
EgressTB: "2",
|
||||
Segment: "3",
|
||||
}
|
||||
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) {
|
||||
satellite := planet.Satellites[0]
|
||||
|
||||
// these numbers are fraction of cents, not of dollars.
|
||||
expectedStoragePrice := 0.001
|
||||
expectedEgressPrice := 0.0045
|
||||
expectedSegmentPrice := 0.00022
|
||||
|
||||
type TestCase struct {
|
||||
Storage float64
|
||||
Egress int64
|
||||
Segments float64
|
||||
|
||||
StorageQuantity int64
|
||||
EgressQuantity int64
|
||||
SegmentsQuantity int64
|
||||
}
|
||||
|
||||
testCases := []TestCase{
|
||||
{}, // all zeros
|
||||
{
|
||||
Storage: 10000000000, // Byte-Hours
|
||||
// storage quantity is calculated to Megabyte-Months
|
||||
// (10000000000 / 1000000) Byte-Hours to Megabytes-Hours
|
||||
// round(10000 / 720) Megabytes-Hours to Megabyte-Months, 720 - hours in month
|
||||
StorageQuantity: 14, // Megabyte-Months
|
||||
usage := map[string]accounting.ProjectUsage{
|
||||
"": {
|
||||
Storage: 10000000000, // Byte-hours
|
||||
Egress: 123 * memory.GB.Int64(), // Bytes
|
||||
SegmentCount: 200000, // Segment-Hours
|
||||
},
|
||||
{
|
||||
Egress: 134 * memory.GB.Int64(), // Bytes
|
||||
// egress quantity is calculated to Megabytes
|
||||
// (134000000000 / 1000000) Bytes to Megabytes
|
||||
EgressQuantity: 134000, // Megabytes
|
||||
partnerName: {
|
||||
Storage: 20000000000,
|
||||
Egress: 456 * memory.GB.Int64(),
|
||||
SegmentCount: 400000,
|
||||
},
|
||||
{
|
||||
Segments: 400000, // Segment-Hours
|
||||
// object quantity is calculated to Segment-Months
|
||||
// round(400000 / 720) Segment-Hours to Segment-Months, 720 - hours in month
|
||||
SegmentsQuantity: 556, // Segment-Months
|
||||
noOverridePartnerName: {
|
||||
Storage: 30000000000,
|
||||
Egress: 789 * memory.GB.Int64(),
|
||||
SegmentCount: 600000,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
record := stripecoinpayments.ProjectRecord{
|
||||
Storage: tc.Storage,
|
||||
Egress: tc.Egress,
|
||||
Segments: tc.Segments,
|
||||
items := planet.Satellites[0].API.Payments.StripeService.InvoiceItemsFromProjectUsage(projectName, usage)
|
||||
require.Len(t, items, len(usage)*3)
|
||||
|
||||
for i, tt := range []struct {
|
||||
name string
|
||||
partner string
|
||||
priceModel payments.ProjectUsagePriceModel
|
||||
}{
|
||||
{"default pricing - no partner", "", defaultModel},
|
||||
{"default pricing - no override for partner", noOverridePartnerName, defaultModel},
|
||||
{"partner pricing", partnerName, partnerModel},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
prefix := "Project " + projectName
|
||||
if tt.partner != "" {
|
||||
prefix += " (" + tt.partner + ")"
|
||||
}
|
||||
|
||||
pricing := satellite.API.Payments.Accounts.GetProjectUsagePriceModel(nil)
|
||||
items := satellite.API.Payments.StripeService.InvoiceItemsFromProjectRecord("project name", record, pricing)
|
||||
usage := usage[tt.partner]
|
||||
expectedStorageQuantity := int64(math.Round(usage.Storage / float64(byteHoursPerMBMonth)))
|
||||
expectedEgressQuantity := int64(math.Round(float64(usage.Egress) / float64(bytesPerMegabyte)))
|
||||
expectedSegmentQuantity := int64(math.Round(usage.SegmentCount / hoursPerMonth))
|
||||
|
||||
require.Equal(t, tc.StorageQuantity, *items[0].Quantity)
|
||||
require.Equal(t, expectedStoragePrice, *items[0].UnitAmountDecimal)
|
||||
items := items[i*3 : (i*3)+3]
|
||||
for _, item := range items {
|
||||
require.NotNil(t, item)
|
||||
}
|
||||
|
||||
require.Equal(t, tc.EgressQuantity, *items[1].Quantity)
|
||||
require.Equal(t, expectedEgressPrice, *items[1].UnitAmountDecimal)
|
||||
require.Equal(t, prefix+" - Segment Storage (MB-Month)", *items[0].Description)
|
||||
require.Equal(t, expectedStorageQuantity, *items[0].Quantity)
|
||||
storage, _ := tt.priceModel.StorageMBMonthCents.Float64()
|
||||
require.Equal(t, storage, *items[0].UnitAmountDecimal)
|
||||
|
||||
require.Equal(t, tc.SegmentsQuantity, *items[2].Quantity)
|
||||
require.Equal(t, expectedSegmentPrice, *items[2].UnitAmountDecimal)
|
||||
require.Equal(t, prefix+" - Egress Bandwidth (MB)", *items[1].Description)
|
||||
require.Equal(t, expectedEgressQuantity, *items[1].Quantity)
|
||||
egress, _ := tt.priceModel.EgressMBCents.Float64()
|
||||
require.Equal(t, egress, *items[1].UnitAmountDecimal)
|
||||
|
||||
require.Equal(t, prefix+" - Segment Fee (Segment-Month)", *items[2].Description)
|
||||
require.Equal(t, expectedSegmentQuantity, *items[2].Quantity)
|
||||
segment, _ := tt.priceModel.SegmentMonthCents.Float64()
|
||||
require.Equal(t, segment, *items[2].UnitAmountDecimal)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -539,9 +579,9 @@ func TestProjectUsagePrice(t *testing.T) {
|
||||
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},
|
||||
{"default pricing - user agent is not valid partner name", []byte("invalid/v0.0"), defaultModel},
|
||||
{"partner pricing - user agent is partner name", []byte(partnerName), partnerModel},
|
||||
{"partner pricing - user agent prefixed with partner name", []byte(partnerName + " invalid/v0.0"), partnerModel},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
user, err := sat.AddUser(ctx, console.CreateUser{
|
||||
@ -554,7 +594,15 @@ func TestProjectUsagePrice(t *testing.T) {
|
||||
project, err := sat.AddProject(ctx, user.ID, "testproject")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = sat.DB.Orders().UpdateBucketBandwidthSettle(ctx, project.ID, []byte("testbucket"),
|
||||
bucket, err := sat.DB.Buckets().CreateBucket(ctx, storj.Bucket{
|
||||
ID: testrand.UUID(),
|
||||
Name: testrand.BucketName(),
|
||||
ProjectID: project.ID,
|
||||
UserAgent: tt.userAgent,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = sat.DB.Orders().UpdateBucketBandwidthSettle(ctx, project.ID, []byte(bucket.Name),
|
||||
pb.PieceAction_GET, memory.TB.Int64(), 0, period)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -98,6 +98,12 @@ read one (
|
||||
where bucket_metainfo.name = ?
|
||||
)
|
||||
|
||||
read one (
|
||||
select bucket_metainfo.user_agent
|
||||
where bucket_metainfo.project_id = ?
|
||||
where bucket_metainfo.name = ?
|
||||
)
|
||||
|
||||
read has (
|
||||
select bucket_metainfo
|
||||
where bucket_metainfo.project_id = ?
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,7 @@ import (
|
||||
|
||||
"storj.io/common/memory"
|
||||
"storj.io/common/pb"
|
||||
"storj.io/common/useragent"
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/private/dbutil"
|
||||
"storj.io/private/dbutil/pgutil"
|
||||
@ -505,7 +506,21 @@ func (db *ProjectAccounting) GetProjectSegmentLimit(ctx context.Context, project
|
||||
}
|
||||
|
||||
// GetProjectTotal retrieves project usage for a given period.
|
||||
func (db *ProjectAccounting) GetProjectTotal(ctx context.Context, projectID uuid.UUID, since, before time.Time) (usage *accounting.ProjectUsage, err error) {
|
||||
func (db *ProjectAccounting) GetProjectTotal(ctx context.Context, projectID uuid.UUID, since, before time.Time) (_ *accounting.ProjectUsage, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
usages, err := db.GetProjectTotalByPartner(ctx, projectID, nil, since, before)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if usage, ok := usages[""]; ok {
|
||||
return &usage, nil
|
||||
}
|
||||
return &accounting.ProjectUsage{Since: since, Before: before}, nil
|
||||
}
|
||||
|
||||
// GetProjectTotalByPartner retrieves project usage for a given period categorized by partner name.
|
||||
// Unpartnered usage or usage for a partner not present in partnerNames is mapped to the empty string.
|
||||
func (db *ProjectAccounting) GetProjectTotalByPartner(ctx context.Context, projectID uuid.UUID, partnerNames []string, since, before time.Time) (usages map[string]accounting.ProjectUsage, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
since = timeTruncateDown(since)
|
||||
bucketNames, err := db.getBucketsSinceAndBefore(ctx, projectID, since, before)
|
||||
@ -531,16 +546,54 @@ func (db *ProjectAccounting) GetProjectTotal(ctx context.Context, projectID uuid
|
||||
ORDER BY bucket_storage_tallies.interval_start DESC
|
||||
`)
|
||||
|
||||
bucketsTallies := make(map[string][]*accounting.BucketStorageTally)
|
||||
totalEgressQuery := db.db.Rebind(`
|
||||
SELECT
|
||||
COALESCE(SUM(settled) + SUM(inline), 0)
|
||||
FROM
|
||||
bucket_bandwidth_rollups
|
||||
WHERE
|
||||
bucket_name = ? AND
|
||||
project_id = ? AND
|
||||
interval_start >= ? AND
|
||||
interval_start < ? AND
|
||||
action = ?;
|
||||
`)
|
||||
|
||||
usages = make(map[string]accounting.ProjectUsage)
|
||||
|
||||
for _, bucket := range bucketNames {
|
||||
storageTallies := make([]*accounting.BucketStorageTally, 0)
|
||||
userAgentRow, err := db.db.Get_BucketMetainfo_UserAgent_By_ProjectId_And_Name(ctx,
|
||||
dbx.BucketMetainfo_ProjectId(projectID[:]),
|
||||
dbx.BucketMetainfo_Name([]byte(bucket)))
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var partner string
|
||||
if userAgentRow != nil && userAgentRow.UserAgent != nil {
|
||||
entries, err := useragent.ParseEntries(userAgentRow.UserAgent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, iterPartner := range partnerNames {
|
||||
if entries[0].Product == iterPartner {
|
||||
partner = iterPartner
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, ok := usages[partner]; !ok {
|
||||
usages[partner] = accounting.ProjectUsage{Since: since, Before: before}
|
||||
}
|
||||
usage := usages[partner]
|
||||
|
||||
storageTalliesRows, err := db.db.QueryContext(ctx, storageQuery, projectID[:], []byte(bucket), since, before)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// generating tallies for each bucket name.
|
||||
|
||||
var prevTally *accounting.BucketStorageTally
|
||||
for storageTalliesRows.Next() {
|
||||
tally := accounting.BucketStorageTally{}
|
||||
|
||||
@ -553,8 +606,17 @@ func (db *ProjectAccounting) GetProjectTotal(ctx context.Context, projectID uuid
|
||||
tally.TotalBytes = inline + remote
|
||||
}
|
||||
|
||||
tally.BucketName = bucket
|
||||
storageTallies = append(storageTallies, &tally)
|
||||
if prevTally == nil {
|
||||
prevTally = &tally
|
||||
continue
|
||||
}
|
||||
|
||||
hours := prevTally.IntervalStart.Sub(tally.IntervalStart).Hours()
|
||||
usage.Storage += memory.Size(tally.TotalBytes).Float64() * hours
|
||||
usage.SegmentCount += float64(tally.TotalSegmentCount) * hours
|
||||
usage.ObjectCount += float64(tally.ObjectCount) * hours
|
||||
|
||||
prevTally = &tally
|
||||
}
|
||||
|
||||
err = errs.Combine(storageTalliesRows.Err(), storageTalliesRows.Close())
|
||||
@ -562,53 +624,21 @@ func (db *ProjectAccounting) GetProjectTotal(ctx context.Context, projectID uuid
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bucketsTallies[bucket] = storageTallies
|
||||
}
|
||||
|
||||
totalEgress, err := db.getTotalEgress(ctx, projectID, since, before)
|
||||
totalEgressRow := db.db.QueryRowContext(ctx, totalEgressQuery, []byte(bucket), projectID[:], since, before, pb.PieceAction_GET)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
usage = new(accounting.ProjectUsage)
|
||||
usage.Egress = memory.Size(totalEgress).Int64()
|
||||
// sum up storage, objects, and segments
|
||||
for _, tallies := range bucketsTallies {
|
||||
for i := len(tallies) - 1; i > 0; i-- {
|
||||
current := (tallies)[i]
|
||||
hours := (tallies)[i-1].IntervalStart.Sub(current.IntervalStart).Hours()
|
||||
usage.Storage += memory.Size(current.Bytes()).Float64() * hours
|
||||
usage.SegmentCount += float64(current.TotalSegmentCount) * hours
|
||||
usage.ObjectCount += float64(current.ObjectCount) * hours
|
||||
var egress int64
|
||||
if err = totalEgressRow.Scan(&egress); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usage.Egress += egress
|
||||
|
||||
usages[partner] = usage
|
||||
}
|
||||
|
||||
usage.Since = since
|
||||
usage.Before = before
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
// getTotalEgress returns total egress (settled + inline) of each bucket_bandwidth_rollup
|
||||
// in selected time period, project id.
|
||||
// only process PieceAction_GET.
|
||||
func (db *ProjectAccounting) getTotalEgress(ctx context.Context, projectID uuid.UUID, since, before time.Time) (totalEgress int64, err error) {
|
||||
totalEgressQuery := db.db.Rebind(`
|
||||
SELECT
|
||||
COALESCE(SUM(settled) + SUM(inline), 0)
|
||||
FROM
|
||||
bucket_bandwidth_rollups
|
||||
WHERE
|
||||
project_id = ? AND
|
||||
interval_start >= ? AND
|
||||
interval_start < ? AND
|
||||
action = ?;
|
||||
`)
|
||||
|
||||
totalEgressRow := db.db.QueryRowContext(ctx, totalEgressQuery, projectID[:], since, before, pb.PieceAction_GET)
|
||||
|
||||
err = totalEgressRow.Scan(&totalEgress)
|
||||
|
||||
return totalEgress, err
|
||||
return usages, nil
|
||||
}
|
||||
|
||||
// GetBucketUsageRollups retrieves summed usage rollups for every bucket of particular project for a given period.
|
||||
@ -1010,31 +1040,45 @@ func (db *ProjectAccounting) archiveRollupsBeforeByAction(ctx context.Context, a
|
||||
}
|
||||
|
||||
// getBucketsSinceAndBefore lists distinct bucket names for a project within a specific timeframe.
|
||||
func (db *ProjectAccounting) getBucketsSinceAndBefore(ctx context.Context, projectID uuid.UUID, since, before time.Time) (_ []string, err error) {
|
||||
func (db *ProjectAccounting) getBucketsSinceAndBefore(ctx context.Context, projectID uuid.UUID, since, before time.Time) (buckets []string, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
bucketsQuery := db.db.Rebind(`SELECT DISTINCT bucket_name
|
||||
FROM bucket_storage_tallies
|
||||
|
||||
queryFormat := `SELECT DISTINCT bucket_name
|
||||
FROM %s
|
||||
WHERE project_id = ?
|
||||
AND interval_start >= ?
|
||||
AND interval_start < ?`)
|
||||
bucketRows, err := db.db.QueryContext(ctx, bucketsQuery, projectID[:], since, before)
|
||||
AND interval_start < ?`
|
||||
|
||||
bucketMap := make(map[string]struct{})
|
||||
|
||||
for _, tableName := range []string{"bucket_storage_tallies", "bucket_bandwidth_rollups"} {
|
||||
query := db.db.Rebind(fmt.Sprintf(queryFormat, tableName))
|
||||
|
||||
rows, err := db.db.QueryContext(ctx, query, projectID[:], since, before)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { err = errs.Combine(err, bucketRows.Close()) }()
|
||||
|
||||
var buckets []string
|
||||
for bucketRows.Next() {
|
||||
for rows.Next() {
|
||||
var bucket string
|
||||
err = bucketRows.Scan(&bucket)
|
||||
err = rows.Scan(&bucket)
|
||||
if err != nil {
|
||||
return nil, errs.Combine(err, rows.Close())
|
||||
}
|
||||
bucketMap[bucket] = struct{}{}
|
||||
}
|
||||
|
||||
err = errs.Combine(rows.Err(), rows.Close())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for bucket := range bucketMap {
|
||||
buckets = append(buckets, bucket)
|
||||
}
|
||||
|
||||
return buckets, bucketRows.Err()
|
||||
return buckets, nil
|
||||
}
|
||||
|
||||
// timeTruncateDown truncates down to the hour before to be in sync with orders endpoint.
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
|
||||
"storj.io/common/memory"
|
||||
"storj.io/common/pb"
|
||||
"storj.io/common/storj"
|
||||
"storj.io/common/testcontext"
|
||||
"storj.io/common/testrand"
|
||||
"storj.io/storj/private/testplanet"
|
||||
@ -174,74 +175,169 @@ func Test_GetSingleBucketRollup(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
func Test_GetProjectTotal(t *testing.T) {
|
||||
func Test_GetProjectTotalByPartner(t *testing.T) {
|
||||
const (
|
||||
epsilon = 1e-8
|
||||
usagePeriod = time.Hour
|
||||
tallyRollupCount = 2
|
||||
)
|
||||
since := time.Time{}
|
||||
before := since.Add(2 * usagePeriod)
|
||||
|
||||
testplanet.Run(t, testplanet.Config{SatelliteCount: 1, StorageNodeCount: 1},
|
||||
func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
bucketName := testrand.BucketName()
|
||||
projectID := testrand.UUID()
|
||||
sat := planet.Satellites[0]
|
||||
|
||||
db := planet.Satellites[0].DB
|
||||
user, err := sat.AddUser(ctx, console.CreateUser{
|
||||
FullName: "Test User",
|
||||
Email: "user@mail.test",
|
||||
}, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The 3rd tally is only present to prevent CreateStorageTally from skipping the 2nd.
|
||||
var tallies []accounting.BucketStorageTally
|
||||
for i := 0; i < 3; i++ {
|
||||
project, err := sat.AddProject(ctx, user.ID, "testproject")
|
||||
require.NoError(t, err)
|
||||
|
||||
type expectedTotal struct {
|
||||
storage float64
|
||||
segments float64
|
||||
objects float64
|
||||
egress int64
|
||||
}
|
||||
expectedTotals := make(map[string]expectedTotal)
|
||||
var beforeTotal expectedTotal
|
||||
|
||||
requireTotal := func(t *testing.T, expected expectedTotal, actual accounting.ProjectUsage) {
|
||||
require.InDelta(t, expected.storage, actual.Storage, epsilon)
|
||||
require.InDelta(t, expected.segments, actual.SegmentCount, epsilon)
|
||||
require.InDelta(t, expected.objects, actual.ObjectCount, epsilon)
|
||||
require.Equal(t, expected.egress, actual.Egress)
|
||||
require.Equal(t, since, actual.Since)
|
||||
require.Equal(t, before, actual.Before)
|
||||
}
|
||||
|
||||
partnerNames := []string{"", "partner1", "partner2"}
|
||||
for _, name := range partnerNames {
|
||||
total := expectedTotal{}
|
||||
|
||||
bucket := storj.Bucket{
|
||||
ID: testrand.UUID(),
|
||||
Name: testrand.BucketName(),
|
||||
ProjectID: project.ID,
|
||||
}
|
||||
if name != "" {
|
||||
bucket.UserAgent = []byte(name)
|
||||
}
|
||||
_, err := sat.DB.Buckets().CreateBucket(ctx, bucket)
|
||||
require.NoError(t, err)
|
||||
|
||||
// We use multiple tallies and rollups to ensure that
|
||||
// GetProjectTotalByPartner is capable of summing them.
|
||||
for i := 0; i <= tallyRollupCount; i++ {
|
||||
tally := accounting.BucketStorageTally{
|
||||
BucketName: bucketName,
|
||||
ProjectID: projectID,
|
||||
IntervalStart: time.Time{}.Add(time.Duration(i) * time.Hour),
|
||||
BucketName: bucket.Name,
|
||||
ProjectID: project.ID,
|
||||
IntervalStart: since.Add(time.Duration(i) * usagePeriod / tallyRollupCount),
|
||||
TotalBytes: int64(testrand.Intn(1000)),
|
||||
ObjectCount: int64(testrand.Intn(1000)),
|
||||
TotalSegmentCount: int64(testrand.Intn(1000)),
|
||||
}
|
||||
tallies = append(tallies, tally)
|
||||
require.NoError(t, db.ProjectAccounting().CreateStorageTally(ctx, tally))
|
||||
require.NoError(t, sat.DB.ProjectAccounting().CreateStorageTally(ctx, tally))
|
||||
|
||||
// The last tally's usage data is unused.
|
||||
usageHours := (usagePeriod / tallyRollupCount).Hours()
|
||||
if i < tallyRollupCount {
|
||||
total.storage += float64(tally.Bytes()) * usageHours
|
||||
total.objects += float64(tally.ObjectCount) * usageHours
|
||||
total.segments += float64(tally.TotalSegmentCount) * usageHours
|
||||
}
|
||||
|
||||
if i < tallyRollupCount-1 {
|
||||
beforeTotal.storage += float64(tally.Bytes()) * usageHours
|
||||
beforeTotal.objects += float64(tally.ObjectCount) * usageHours
|
||||
beforeTotal.segments += float64(tally.TotalSegmentCount) * usageHours
|
||||
}
|
||||
}
|
||||
|
||||
var rollups []orders.BucketBandwidthRollup
|
||||
var expectedEgress int64
|
||||
for i := 0; i < 2; i++ {
|
||||
for i := 0; i < tallyRollupCount; i++ {
|
||||
rollup := orders.BucketBandwidthRollup{
|
||||
ProjectID: projectID,
|
||||
BucketName: bucketName,
|
||||
BucketName: bucket.Name,
|
||||
ProjectID: project.ID,
|
||||
Action: pb.PieceAction_GET,
|
||||
IntervalStart: tallies[i].IntervalStart,
|
||||
IntervalStart: since.Add(time.Duration(i) * usagePeriod / tallyRollupCount),
|
||||
Inline: int64(testrand.Intn(1000)),
|
||||
Settled: int64(testrand.Intn(1000)),
|
||||
}
|
||||
rollups = append(rollups, rollup)
|
||||
expectedEgress += rollup.Inline + rollup.Settled
|
||||
total.egress += rollup.Inline + rollup.Settled
|
||||
|
||||
if i < tallyRollupCount {
|
||||
beforeTotal.egress += rollup.Inline + rollup.Settled
|
||||
}
|
||||
require.NoError(t, db.Orders().UpdateBandwidthBatch(ctx, rollups))
|
||||
}
|
||||
require.NoError(t, sat.DB.Orders().UpdateBandwidthBatch(ctx, rollups))
|
||||
|
||||
usage, err := db.ProjectAccounting().GetProjectTotal(ctx, projectID, tallies[0].IntervalStart, tallies[2].IntervalStart.Add(time.Minute))
|
||||
expectedTotals[name] = total
|
||||
}
|
||||
|
||||
t.Run("sum all partner usages", func(t *testing.T) {
|
||||
usages, err := sat.DB.ProjectAccounting().GetProjectTotalByPartner(ctx, project.ID, nil, since, before)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, usages, 1)
|
||||
require.Contains(t, usages, "")
|
||||
|
||||
const epsilon = 1e-8
|
||||
require.InDelta(t, usage.Storage, float64(tallies[0].Bytes()+tallies[1].Bytes()), epsilon)
|
||||
require.InDelta(t, usage.SegmentCount, float64(tallies[0].TotalSegmentCount+tallies[1].TotalSegmentCount), epsilon)
|
||||
require.InDelta(t, usage.ObjectCount, float64(tallies[0].ObjectCount+tallies[1].ObjectCount), epsilon)
|
||||
require.Equal(t, usage.Egress, expectedEgress)
|
||||
require.Equal(t, usage.Since, tallies[0].IntervalStart)
|
||||
require.Equal(t, usage.Before, tallies[2].IntervalStart.Add(time.Minute))
|
||||
var summedTotal expectedTotal
|
||||
for _, total := range expectedTotals {
|
||||
summedTotal.storage += total.storage
|
||||
summedTotal.segments += total.segments
|
||||
summedTotal.objects += total.objects
|
||||
summedTotal.egress += total.egress
|
||||
}
|
||||
|
||||
// Ensure that GetProjectTotal treats the 'before' arg as exclusive
|
||||
usage, err = db.ProjectAccounting().GetProjectTotal(ctx, projectID, tallies[0].IntervalStart, tallies[2].IntervalStart)
|
||||
requireTotal(t, summedTotal, usages[""])
|
||||
})
|
||||
|
||||
t.Run("individual partner usages", func(t *testing.T) {
|
||||
usages, err := sat.DB.ProjectAccounting().GetProjectTotalByPartner(ctx, project.ID, partnerNames, since, before)
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, usage.Storage, float64(tallies[0].Bytes()), epsilon)
|
||||
require.InDelta(t, usage.SegmentCount, float64(tallies[0].TotalSegmentCount), epsilon)
|
||||
require.InDelta(t, usage.ObjectCount, float64(tallies[0].ObjectCount), epsilon)
|
||||
require.Equal(t, usage.Egress, expectedEgress)
|
||||
require.Equal(t, usage.Since, tallies[0].IntervalStart)
|
||||
require.Equal(t, usage.Before, tallies[2].IntervalStart)
|
||||
require.Len(t, usages, len(expectedTotals))
|
||||
for _, name := range partnerNames {
|
||||
require.Contains(t, usages, name)
|
||||
}
|
||||
|
||||
usage, err = db.ProjectAccounting().GetProjectTotal(ctx, projectID, rollups[0].IntervalStart, rollups[1].IntervalStart)
|
||||
for partner, usage := range usages {
|
||||
requireTotal(t, expectedTotals[partner], usage)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("select one partner usage and sum remaining usages", func(t *testing.T) {
|
||||
partner := partnerNames[len(partnerNames)-1]
|
||||
usages, err := sat.DB.ProjectAccounting().GetProjectTotalByPartner(ctx, project.ID, []string{partner}, since, before)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, usage.Storage)
|
||||
require.Zero(t, usage.SegmentCount)
|
||||
require.Zero(t, usage.ObjectCount)
|
||||
require.Equal(t, usage.Egress, rollups[0].Inline+rollups[0].Settled)
|
||||
require.Equal(t, usage.Since, rollups[0].IntervalStart)
|
||||
require.Equal(t, usage.Before, rollups[1].IntervalStart)
|
||||
require.Len(t, usages, 2)
|
||||
require.Contains(t, usages, "")
|
||||
require.Contains(t, usages, partner)
|
||||
|
||||
var summedTotal expectedTotal
|
||||
for _, partner := range partnerNames[:len(partnerNames)-1] {
|
||||
summedTotal.storage += expectedTotals[partner].storage
|
||||
summedTotal.segments += expectedTotals[partner].segments
|
||||
summedTotal.objects += expectedTotals[partner].objects
|
||||
summedTotal.egress += expectedTotals[partner].egress
|
||||
}
|
||||
|
||||
requireTotal(t, expectedTotals[partner], usages[partner])
|
||||
requireTotal(t, summedTotal, usages[""])
|
||||
})
|
||||
|
||||
t.Run("ensure before is exclusive", func(t *testing.T) {
|
||||
before = since.Add(usagePeriod)
|
||||
usages, err := sat.DB.ProjectAccounting().GetProjectTotalByPartner(ctx, project.ID, nil, since, before)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, usages, 1)
|
||||
require.Contains(t, usages, "")
|
||||
requireTotal(t, beforeTotal, usages[""])
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user