2020-05-19 08:42:07 +01:00
|
|
|
// Copyright (C) 2020 Storj Labs, Inc.
|
|
|
|
// See LICENSE for copying information.
|
|
|
|
|
2023-04-06 12:41:14 +01:00
|
|
|
package stripe_test
|
2020-05-19 08:42:07 +01:00
|
|
|
|
|
|
|
import (
|
2022-12-14 13:35:53 +00:00
|
|
|
"context"
|
2023-02-23 16:27:37 +00:00
|
|
|
"fmt"
|
|
|
|
"math"
|
2023-03-23 15:38:07 +00:00
|
|
|
"sort"
|
2020-05-19 08:42:07 +01:00
|
|
|
"strconv"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/require"
|
2022-09-27 09:48:38 +01:00
|
|
|
"github.com/stripe/stripe-go/v72"
|
2020-05-19 08:42:07 +01:00
|
|
|
"go.uber.org/zap"
|
|
|
|
|
2022-09-06 13:43:09 +01:00
|
|
|
"storj.io/common/currency"
|
2020-05-19 08:42:07 +01:00
|
|
|
"storj.io/common/memory"
|
|
|
|
"storj.io/common/pb"
|
2023-02-23 16:27:37 +00:00
|
|
|
"storj.io/common/storj"
|
2020-05-19 08:42:07 +01:00
|
|
|
"storj.io/common/testcontext"
|
2022-05-10 20:19:53 +01:00
|
|
|
"storj.io/common/testrand"
|
2022-12-14 13:35:53 +00:00
|
|
|
"storj.io/common/uuid"
|
2022-05-10 20:19:53 +01:00
|
|
|
"storj.io/storj/private/blockchain"
|
2020-05-19 08:42:07 +01:00
|
|
|
"storj.io/storj/private/testplanet"
|
|
|
|
"storj.io/storj/satellite"
|
2020-05-26 16:09:43 +01:00
|
|
|
"storj.io/storj/satellite/accounting"
|
|
|
|
"storj.io/storj/satellite/console"
|
2021-04-21 13:42:57 +01:00
|
|
|
"storj.io/storj/satellite/metabase"
|
2023-02-23 16:27:37 +00:00
|
|
|
"storj.io/storj/satellite/payments"
|
2022-05-10 20:19:53 +01:00
|
|
|
"storj.io/storj/satellite/payments/billing"
|
2023-02-23 16:27:37 +00:00
|
|
|
"storj.io/storj/satellite/payments/paymentsconfig"
|
2023-04-06 12:41:14 +01:00
|
|
|
stripe1 "storj.io/storj/satellite/payments/stripe"
|
2020-05-19 08:42:07 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
func TestService_InvoiceElementsProcessing(t *testing.T) {
|
|
|
|
testplanet.Run(t, testplanet.Config{
|
2020-05-26 16:09:43 +01:00
|
|
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
2020-05-19 08:42:07 +01:00
|
|
|
Reconfigure: testplanet.Reconfigure{
|
|
|
|
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
|
|
|
config.Payments.StripeCoinPayments.ListingLimit = 4
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
|
|
satellite := planet.Satellites[0]
|
|
|
|
|
2020-05-30 18:38:15 +01:00
|
|
|
// 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
|
2021-01-04 09:47:31 +00:00
|
|
|
period := time.Date(time.Now().Year(), time.Now().Month()+1, 20, 0, 0, 0, 0, time.UTC)
|
2020-05-30 18:38:15 +01:00
|
|
|
|
2020-05-19 08:42:07 +01:00
|
|
|
numberOfProjects := 19
|
2021-08-27 01:51:26 +01:00
|
|
|
// generate test data, each user has one project and some credits
|
2020-05-19 08:42:07 +01:00
|
|
|
for i := 0; i < numberOfProjects; i++ {
|
2020-07-24 10:40:17 +01:00
|
|
|
user, err := satellite.AddUser(ctx, console.CreateUser{
|
|
|
|
FullName: "testuser" + strconv.Itoa(i),
|
|
|
|
Email: "user@test" + strconv.Itoa(i),
|
|
|
|
}, 1)
|
2020-05-19 08:42:07 +01:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
project, err := satellite.AddProject(ctx, user.ID, "testproject-"+strconv.Itoa(i))
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
err = satellite.DB.Orders().UpdateBucketBandwidthSettle(ctx, project.ID, []byte("testbucket"),
|
2021-05-29 23:16:12 +01:00
|
|
|
pb.PieceAction_GET, int64(i+10)*memory.GiB.Int64(), 0, period)
|
2020-05-19 08:42:07 +01:00
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
|
|
|
|
2022-04-28 03:54:56 +01:00
|
|
|
satellite.API.Payments.StripeService.SetNow(func() time.Time {
|
2020-05-19 08:42:07 +01:00
|
|
|
return time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
})
|
2022-04-28 03:54:56 +01:00
|
|
|
err := satellite.API.Payments.StripeService.PrepareInvoiceProjectRecords(ctx, period)
|
2020-05-19 08:42:07 +01:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
start := time.Date(period.Year(), period.Month(), 1, 0, 0, 0, 0, time.UTC)
|
2021-08-25 20:35:57 +01:00
|
|
|
end := time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC)
|
2020-05-19 08:42:07 +01:00
|
|
|
|
|
|
|
// check if we have project record for each project
|
|
|
|
recordsPage, err := satellite.DB.StripeCoinPayments().ProjectRecords().ListUnapplied(ctx, 0, 40, start, end)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, numberOfProjects, len(recordsPage.Records))
|
|
|
|
|
2022-04-28 03:54:56 +01:00
|
|
|
err = satellite.API.Payments.StripeService.InvoiceApplyProjectRecords(ctx, period)
|
2020-05-19 08:42:07 +01:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// verify that we applied all unapplied project records
|
|
|
|
recordsPage, err = satellite.DB.StripeCoinPayments().ProjectRecords().ListUnapplied(ctx, 0, 40, start, end)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 0, len(recordsPage.Records))
|
|
|
|
})
|
|
|
|
}
|
2020-05-26 16:09:43 +01:00
|
|
|
|
|
|
|
func TestService_InvoiceUserWithManyProjects(t *testing.T) {
|
|
|
|
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.StripeCoinPayments.ListingLimit = 4
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
|
|
satellite := planet.Satellites[0]
|
|
|
|
payments := satellite.API.Payments
|
|
|
|
|
2020-05-30 18:38:15 +01:00
|
|
|
// 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
|
2021-01-04 09:47:31 +00:00
|
|
|
period := time.Date(time.Now().Year(), time.Now().Month()+1, 20, 0, 0, 0, 0, time.UTC)
|
2020-05-30 18:38:15 +01:00
|
|
|
|
2022-04-28 03:54:56 +01:00
|
|
|
payments.StripeService.SetNow(func() time.Time {
|
2020-05-26 16:09:43 +01:00
|
|
|
return time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
})
|
|
|
|
start := time.Date(period.Year(), period.Month(), 1, 0, 0, 0, 0, time.UTC)
|
2021-08-25 20:35:57 +01:00
|
|
|
end := time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC)
|
2020-05-26 16:09:43 +01:00
|
|
|
|
|
|
|
numberOfProjects := 5
|
|
|
|
storageHours := 24
|
|
|
|
|
2020-07-24 10:40:17 +01:00
|
|
|
user, err := satellite.AddUser(ctx, console.CreateUser{
|
|
|
|
FullName: "testuser",
|
|
|
|
Email: "user@test",
|
|
|
|
}, numberOfProjects)
|
2020-05-26 16:09:43 +01:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
projects := make([]*console.Project, numberOfProjects)
|
|
|
|
projectsEgress := make([]int64, len(projects))
|
|
|
|
projectsStorage := make([]int64, len(projects))
|
|
|
|
for i := 0; i < len(projects); i++ {
|
|
|
|
projects[i], err = satellite.AddProject(ctx, user.ID, "testproject-"+strconv.Itoa(i))
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
projectsEgress[i] = int64(i+10) * memory.GiB.Int64()
|
|
|
|
projectsStorage[i] = int64(i+1) * memory.TiB.Int64()
|
2022-12-14 13:35:53 +00:00
|
|
|
totalSegments := int64(i + 1)
|
|
|
|
generateProjectStorage(ctx, t, satellite.DB,
|
|
|
|
projects[i].ID,
|
|
|
|
period,
|
|
|
|
period.Add(time.Duration(storageHours)*time.Hour),
|
|
|
|
projectsEgress[i],
|
|
|
|
projectsStorage[i],
|
|
|
|
totalSegments)
|
2020-05-26 16:09:43 +01:00
|
|
|
// verify that projects don't have records yet
|
|
|
|
projectRecord, err := satellite.DB.StripeCoinPayments().ProjectRecords().Get(ctx, projects[i].ID, start, end)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Nil(t, projectRecord)
|
|
|
|
}
|
|
|
|
|
2022-04-28 03:54:56 +01:00
|
|
|
err = payments.StripeService.PrepareInvoiceProjectRecords(ctx, period)
|
2020-05-26 16:09:43 +01:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
for i := 0; i < len(projects); i++ {
|
|
|
|
projectRecord, err := satellite.DB.StripeCoinPayments().ProjectRecords().Get(ctx, projects[i].ID, start, end)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.NotNil(t, projectRecord)
|
|
|
|
require.Equal(t, projects[i].ID, projectRecord.ProjectID)
|
|
|
|
require.Equal(t, projectsEgress[i], projectRecord.Egress)
|
|
|
|
|
|
|
|
expectedStorage := float64(projectsStorage[i] * int64(storageHours))
|
|
|
|
require.Equal(t, expectedStorage, projectRecord.Storage)
|
|
|
|
|
2021-10-20 23:54:34 +01:00
|
|
|
expectedSegmentsCount := float64((i + 1) * storageHours)
|
|
|
|
require.Equal(t, expectedSegmentsCount, projectRecord.Segments)
|
2020-05-26 16:09:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// run all parts of invoice generation to see if there are no unexpected errors
|
2022-04-28 03:54:56 +01:00
|
|
|
err = payments.StripeService.InvoiceApplyProjectRecords(ctx, period)
|
2020-05-26 16:09:43 +01:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2022-04-28 03:54:56 +01:00
|
|
|
err = payments.StripeService.CreateInvoices(ctx, period)
|
2020-05-26 16:09:43 +01:00
|
|
|
require.NoError(t, err)
|
2022-05-10 20:19:53 +01:00
|
|
|
|
2020-05-26 16:09:43 +01:00
|
|
|
})
|
|
|
|
}
|
2020-05-29 11:29:03 +01:00
|
|
|
|
2020-06-03 18:01:54 +01:00
|
|
|
func TestService_ProjectsWithMembers(t *testing.T) {
|
|
|
|
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.StripeCoinPayments.ListingLimit = 4
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
|
|
satellite := 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
|
2021-01-04 09:47:31 +00:00
|
|
|
period := time.Date(time.Now().Year(), time.Now().Month()+1, 20, 0, 0, 0, 0, time.UTC)
|
2020-06-03 18:01:54 +01:00
|
|
|
|
|
|
|
numberOfUsers := 5
|
|
|
|
users := make([]*console.User, numberOfUsers)
|
|
|
|
projects := make([]*console.Project, numberOfUsers)
|
|
|
|
for i := 0; i < numberOfUsers; i++ {
|
|
|
|
var err error
|
2020-07-24 10:40:17 +01:00
|
|
|
|
|
|
|
users[i], err = satellite.AddUser(ctx, console.CreateUser{
|
|
|
|
FullName: "testuser" + strconv.Itoa(i),
|
|
|
|
Email: "user@test" + strconv.Itoa(i),
|
|
|
|
}, 1)
|
2020-06-03 18:01:54 +01:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
projects[i], err = satellite.AddProject(ctx, users[i].ID, "testproject-"+strconv.Itoa(i))
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// all users are members in all projects
|
|
|
|
for _, project := range projects {
|
|
|
|
for _, user := range users {
|
|
|
|
if project.OwnerID != user.ID {
|
|
|
|
_, err := satellite.DB.Console().ProjectMembers().Insert(ctx, user.ID, project.ID)
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-28 03:54:56 +01:00
|
|
|
satellite.API.Payments.StripeService.SetNow(func() time.Time {
|
2020-06-03 18:01:54 +01:00
|
|
|
return time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
})
|
2022-04-28 03:54:56 +01:00
|
|
|
err := satellite.API.Payments.StripeService.PrepareInvoiceProjectRecords(ctx, period)
|
2020-06-03 18:01:54 +01:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
start := time.Date(period.Year(), period.Month(), 1, 0, 0, 0, 0, time.UTC)
|
2021-08-25 20:35:57 +01:00
|
|
|
end := time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC)
|
2020-06-03 18:01:54 +01:00
|
|
|
|
|
|
|
recordsPage, err := satellite.DB.StripeCoinPayments().ProjectRecords().ListUnapplied(ctx, 0, 40, start, end)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, len(projects), len(recordsPage.Records))
|
|
|
|
})
|
|
|
|
}
|
2020-05-27 13:08:37 +01:00
|
|
|
|
2023-02-23 16:27:37 +00:00
|
|
|
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{
|
2023-04-07 10:57:54 +01:00
|
|
|
StorageTB: "4",
|
|
|
|
EgressTB: "5",
|
|
|
|
Segment: "6",
|
|
|
|
EgressDiscountRatio: 0.5,
|
2023-02-23 16:27:37 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
defaultModel, err := defaultPrice.ToModel()
|
|
|
|
require.NoError(t, err)
|
|
|
|
partnerModel, err := partnerPrice.ToModel()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2023-01-27 05:34:08 +00:00
|
|
|
testplanet.Run(t, testplanet.Config{
|
|
|
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
2023-02-23 16:27:37 +00:00
|
|
|
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,
|
|
|
|
})
|
|
|
|
},
|
|
|
|
},
|
2023-01-27 05:34:08 +00:00
|
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
2023-02-23 16:27:37 +00:00
|
|
|
usage := map[string]accounting.ProjectUsage{
|
|
|
|
"": {
|
|
|
|
Storage: 10000000000, // Byte-hours
|
|
|
|
Egress: 123 * memory.GB.Int64(), // Bytes
|
|
|
|
SegmentCount: 200000, // Segment-Hours
|
2023-01-27 05:34:08 +00:00
|
|
|
},
|
2023-02-23 16:27:37 +00:00
|
|
|
partnerName: {
|
|
|
|
Storage: 20000000000,
|
|
|
|
Egress: 456 * memory.GB.Int64(),
|
|
|
|
SegmentCount: 400000,
|
2020-05-27 13:08:37 +01:00
|
|
|
},
|
2023-02-23 16:27:37 +00:00
|
|
|
noOverridePartnerName: {
|
|
|
|
Storage: 30000000000,
|
|
|
|
Egress: 789 * memory.GB.Int64(),
|
|
|
|
SegmentCount: 600000,
|
2020-05-27 13:08:37 +01:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2023-02-23 16:27:37 +00:00
|
|
|
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 + ")"
|
|
|
|
}
|
|
|
|
|
|
|
|
usage := usage[tt.partner]
|
2023-04-07 10:57:54 +01:00
|
|
|
usage.Egress -= int64(math.Round(usage.Storage / hoursPerMonth * tt.priceModel.EgressDiscountRatio))
|
|
|
|
if usage.Egress < 0 {
|
|
|
|
usage.Egress = 0
|
|
|
|
}
|
|
|
|
|
2023-02-23 16:27:37 +00:00
|
|
|
expectedStorageQuantity := int64(math.Round(usage.Storage / float64(byteHoursPerMBMonth)))
|
|
|
|
expectedEgressQuantity := int64(math.Round(float64(usage.Egress) / float64(bytesPerMegabyte)))
|
|
|
|
expectedSegmentQuantity := int64(math.Round(usage.SegmentCount / hoursPerMonth))
|
2023-01-27 05:34:08 +00:00
|
|
|
|
2023-02-23 16:27:37 +00:00
|
|
|
items := items[i*3 : (i*3)+3]
|
|
|
|
for _, item := range items {
|
|
|
|
require.NotNil(t, item)
|
|
|
|
}
|
2020-05-27 13:08:37 +01:00
|
|
|
|
2023-02-23 16:27:37 +00:00
|
|
|
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)
|
2020-05-27 13:08:37 +01:00
|
|
|
|
2023-02-23 16:27:37 +00:00
|
|
|
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)
|
2020-05-27 13:08:37 +01:00
|
|
|
|
2023-02-23 16:27:37 +00:00
|
|
|
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)
|
|
|
|
})
|
2020-05-27 13:08:37 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2022-05-10 20:19:53 +01:00
|
|
|
|
|
|
|
func TestService_InvoiceItemsFromZeroTokenBalance(t *testing.T) {
|
|
|
|
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.StripeCoinPayments.ListingLimit = 4
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
|
|
satellite := planet.Satellites[0]
|
|
|
|
payments := satellite.API.Payments
|
|
|
|
|
|
|
|
user, err := satellite.AddUser(ctx, console.CreateUser{
|
|
|
|
FullName: "testuser",
|
|
|
|
Email: "user@test",
|
|
|
|
}, 1)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// setup storjscan wallet
|
|
|
|
address, err := blockchain.BytesToAddress(testrand.Bytes(20))
|
|
|
|
require.NoError(t, err)
|
|
|
|
userID := user.ID
|
|
|
|
err = satellite.DB.Wallets().Add(ctx, userID, address)
|
|
|
|
require.NoError(t, err)
|
|
|
|
_, err = satellite.DB.Billing().Insert(ctx, billing.Transaction{
|
|
|
|
UserID: userID,
|
2022-09-06 13:43:09 +01:00
|
|
|
Amount: currency.AmountFromBaseUnits(1000, currency.USDollars),
|
2022-05-10 20:19:53 +01:00
|
|
|
Description: "token payment credit",
|
2023-03-28 02:42:26 +01:00
|
|
|
Source: billing.StorjScanSource,
|
2022-05-10 20:19:53 +01:00
|
|
|
Status: billing.TransactionStatusCompleted,
|
|
|
|
Type: billing.TransactionTypeCredit,
|
|
|
|
Metadata: nil,
|
|
|
|
Timestamp: time.Now(),
|
|
|
|
CreatedAt: time.Now(),
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// run apply token balance to see if there are no unexpected errors
|
2022-09-28 18:41:41 +01:00
|
|
|
err = payments.StripeService.InvoiceApplyTokenBalance(ctx, time.Time{})
|
2022-05-10 20:19:53 +01:00
|
|
|
require.NoError(t, err)
|
|
|
|
})
|
|
|
|
}
|
2022-09-27 09:48:38 +01:00
|
|
|
|
|
|
|
func TestService_GenerateInvoice(t *testing.T) {
|
2022-12-14 13:35:53 +00:00
|
|
|
for _, testCase := range []struct {
|
|
|
|
desc string
|
|
|
|
skipEmptyInvoices bool
|
|
|
|
addProjectUsage bool
|
|
|
|
expectInvoice bool
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
desc: "invoice with non-empty usage created if not configured to skip",
|
|
|
|
skipEmptyInvoices: false,
|
|
|
|
addProjectUsage: true,
|
|
|
|
expectInvoice: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
desc: "invoice with non-empty usage created if configured to skip",
|
|
|
|
skipEmptyInvoices: true,
|
|
|
|
addProjectUsage: true,
|
|
|
|
expectInvoice: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
desc: "invoice with empty usage created if not configured to skip",
|
|
|
|
skipEmptyInvoices: false,
|
|
|
|
addProjectUsage: false,
|
|
|
|
expectInvoice: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
desc: "invoice with empty usage not created if configured to skip",
|
|
|
|
skipEmptyInvoices: true,
|
|
|
|
addProjectUsage: false,
|
|
|
|
expectInvoice: false,
|
|
|
|
},
|
|
|
|
} {
|
|
|
|
t.Run(testCase.desc, func(t *testing.T) {
|
|
|
|
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.StripeCoinPayments.SkipEmptyInvoices = testCase.skipEmptyInvoices
|
2023-04-06 12:41:14 +01:00
|
|
|
config.Payments.StripeCoinPayments.StripeFreeTierCouponID = stripe1.MockCouponID1
|
2022-12-14 13:35:53 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
|
|
satellite := planet.Satellites[0]
|
|
|
|
payments := satellite.API.Payments
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
|
|
payments.StripeService.SetNow(func() time.Time {
|
|
|
|
return time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
})
|
|
|
|
start := time.Date(period.Year(), period.Month(), 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
end := time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
|
|
|
|
user, err := satellite.AddUser(ctx, console.CreateUser{
|
|
|
|
FullName: "Test User",
|
|
|
|
Email: "test@mail.test",
|
|
|
|
}, 1)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
proj, err := satellite.AddProject(ctx, user.ID, "testproject")
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// optionally add some usage for the project
|
|
|
|
if testCase.addProjectUsage {
|
|
|
|
generateProjectStorage(ctx, t, satellite.DB,
|
|
|
|
proj.ID,
|
|
|
|
period,
|
|
|
|
period.Add(24*time.Hour),
|
|
|
|
100000,
|
|
|
|
200000,
|
|
|
|
99)
|
|
|
|
}
|
2022-09-27 09:48:38 +01:00
|
|
|
|
2022-12-14 13:35:53 +00:00
|
|
|
require.NoError(t, payments.StripeService.GenerateInvoices(ctx, start))
|
2022-09-27 09:48:38 +01:00
|
|
|
|
2022-12-14 13:35:53 +00:00
|
|
|
// ensure free tier coupon was applied
|
|
|
|
cusID, err := satellite.DB.StripeCoinPayments().Customers().GetCustomerID(ctx, user.ID)
|
|
|
|
require.NoError(t, err)
|
2022-09-27 09:48:38 +01:00
|
|
|
|
2023-03-14 02:59:24 +00:00
|
|
|
params := &stripe.CustomerParams{Params: stripe.Params{Context: ctx}}
|
|
|
|
stripeUser, err := payments.StripeClient.Customers().Get(cusID, params)
|
2022-12-14 13:35:53 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.NotNil(t, stripeUser.Discount)
|
|
|
|
require.NotNil(t, stripeUser.Discount.Coupon)
|
|
|
|
require.Equal(t, payments.StripeService.StripeFreeTierCouponID, stripeUser.Discount.Coupon.ID)
|
2022-09-27 09:48:38 +01:00
|
|
|
|
2022-12-14 13:35:53 +00:00
|
|
|
// ensure project record was generated
|
|
|
|
err = satellite.DB.StripeCoinPayments().ProjectRecords().Check(ctx, proj.ID, start, end)
|
2023-04-06 12:41:14 +01:00
|
|
|
require.ErrorIs(t, stripe1.ErrProjectRecordExists, err)
|
2022-09-27 09:48:38 +01:00
|
|
|
|
2022-12-14 13:35:53 +00:00
|
|
|
rec, err := satellite.DB.StripeCoinPayments().ProjectRecords().Get(ctx, proj.ID, start, end)
|
|
|
|
require.NotNil(t, rec)
|
|
|
|
require.NoError(t, err)
|
2022-09-27 09:48:38 +01:00
|
|
|
|
2023-03-14 02:59:24 +00:00
|
|
|
invoice, hasInvoice := getCustomerInvoice(ctx, payments.StripeClient, cusID)
|
|
|
|
invoiceItems := getCustomerInvoiceItems(ctx, payments.StripeClient, cusID)
|
2022-09-27 09:48:38 +01:00
|
|
|
|
2022-12-14 13:35:53 +00:00
|
|
|
// If invoicing empty usage invoices was skipped, then we don't
|
|
|
|
// expect an invoice or invoice items.
|
|
|
|
if !testCase.expectInvoice {
|
|
|
|
require.False(t, hasInvoice, "expected no invoice but got one")
|
|
|
|
require.Empty(t, invoiceItems, "not expecting any invoice items")
|
|
|
|
return
|
|
|
|
}
|
2022-09-27 09:48:38 +01:00
|
|
|
|
2022-12-14 13:35:53 +00:00
|
|
|
// Otherwise, we expect one or more line items that have been
|
|
|
|
// associated with the newly created invoice.
|
|
|
|
require.True(t, hasInvoice, "expected invoice but did not get one")
|
|
|
|
require.NotZero(t, len(invoiceItems), "expecting one or more invoice items")
|
|
|
|
for _, item := range invoiceItems {
|
|
|
|
require.Contains(t, item.Metadata, "projectID")
|
|
|
|
require.Equal(t, item.Metadata["projectID"], proj.ID.String())
|
|
|
|
require.NotNil(t, invoice, item.Invoice)
|
|
|
|
require.Equal(t, invoice.ID, item.Invoice.ID)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2022-09-27 09:48:38 +01:00
|
|
|
|
2023-04-06 12:41:14 +01:00
|
|
|
func getCustomerInvoice(ctx context.Context, stripeClient stripe1.Client, cusID string) (*stripe.Invoice, bool) {
|
2023-03-14 02:59:24 +00:00
|
|
|
iter := stripeClient.Invoices().List(&stripe.InvoiceListParams{
|
|
|
|
ListParams: stripe.ListParams{Context: ctx},
|
|
|
|
Customer: &cusID,
|
|
|
|
})
|
2022-12-14 13:35:53 +00:00
|
|
|
if iter.Next() {
|
|
|
|
return iter.Invoice(), true
|
|
|
|
}
|
|
|
|
return nil, false
|
|
|
|
}
|
2022-09-27 09:48:38 +01:00
|
|
|
|
2023-04-06 12:41:14 +01:00
|
|
|
func getCustomerInvoiceItems(ctx context.Context, stripeClient stripe1.Client, cusID string) (items []*stripe.InvoiceItem) {
|
2023-03-14 02:59:24 +00:00
|
|
|
iter := stripeClient.InvoiceItems().List(&stripe.InvoiceItemListParams{
|
|
|
|
ListParams: stripe.ListParams{Context: ctx},
|
|
|
|
Customer: &cusID,
|
|
|
|
})
|
2022-12-14 13:35:53 +00:00
|
|
|
for iter.Next() {
|
|
|
|
items = append(items, iter.InvoiceItem())
|
|
|
|
}
|
|
|
|
return items
|
|
|
|
}
|
|
|
|
|
|
|
|
func generateProjectStorage(ctx context.Context, tb testing.TB, db satellite.DB, projectID uuid.UUID, start, end time.Time, egress, totalBytes, totalSegments int64) {
|
|
|
|
// generate egress
|
|
|
|
err := db.Orders().UpdateBucketBandwidthSettle(ctx, projectID, []byte("testbucket"),
|
|
|
|
pb.PieceAction_GET, egress, 0, start)
|
|
|
|
require.NoError(tb, err)
|
|
|
|
|
|
|
|
// generate storage
|
|
|
|
tallies := map[metabase.BucketLocation]*accounting.BucketTally{
|
|
|
|
{}: {
|
|
|
|
BucketLocation: metabase.BucketLocation{
|
|
|
|
ProjectID: projectID,
|
|
|
|
BucketName: "testbucket",
|
|
|
|
},
|
|
|
|
TotalBytes: totalBytes,
|
|
|
|
TotalSegments: totalSegments,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
err = db.ProjectAccounting().SaveTallies(ctx, start, tallies)
|
|
|
|
require.NoError(tb, err)
|
|
|
|
|
|
|
|
err = db.ProjectAccounting().SaveTallies(ctx, end, tallies)
|
|
|
|
require.NoError(tb, err)
|
2022-09-27 09:48:38 +01:00
|
|
|
}
|
2023-01-12 03:41:14 +00:00
|
|
|
|
2023-02-23 16:27:37 +00:00
|
|
|
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 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{
|
|
|
|
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)
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
2023-03-14 02:59:24 +00:00
|
|
|
items := getCustomerInvoiceItems(ctx, sat.API.Payments.StripeClient, cusID)
|
2023-02-23 16:27:37 +00:00
|
|
|
require.Len(t, items, 3)
|
2023-03-23 15:38:07 +00:00
|
|
|
sort.Slice(items, func(i, j int) bool {
|
|
|
|
return items[i].Description < items[j].Description
|
|
|
|
})
|
2023-02-23 16:27:37 +00:00
|
|
|
egress, _ := tt.expectedPrice.EgressMBCents.Float64()
|
2023-03-23 15:38:07 +00:00
|
|
|
require.Equal(t, egress, items[0].UnitAmountDecimal)
|
2023-02-23 16:27:37 +00:00
|
|
|
segment, _ := tt.expectedPrice.SegmentMonthCents.Float64()
|
2023-03-23 15:38:07 +00:00
|
|
|
require.Equal(t, segment, items[1].UnitAmountDecimal)
|
|
|
|
storage, _ := tt.expectedPrice.StorageMBMonthCents.Float64()
|
|
|
|
require.Equal(t, storage, items[2].UnitAmountDecimal)
|
2023-02-23 16:27:37 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-01-20 20:56:12 +00:00
|
|
|
func TestPayInvoicesSkipDue(t *testing.T) {
|
|
|
|
testplanet.Run(t, testplanet.Config{
|
|
|
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
|
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
|
|
satellite := planet.Satellites[0]
|
|
|
|
|
|
|
|
cus1 := "cus_1"
|
|
|
|
cus2 := "cus_2"
|
|
|
|
amount := int64(100)
|
|
|
|
curr := string(stripe.CurrencyUSD)
|
|
|
|
due := time.Now().Add(14 * 24 * time.Hour).Unix()
|
|
|
|
|
|
|
|
_, err := satellite.API.Payments.StripeClient.InvoiceItems().New(&stripe.InvoiceItemParams{
|
2023-03-14 02:59:24 +00:00
|
|
|
Params: stripe.Params{Context: ctx},
|
2023-01-20 20:56:12 +00:00
|
|
|
Amount: &amount,
|
|
|
|
Currency: &curr,
|
|
|
|
Customer: &cus1,
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
_, err = satellite.API.Payments.StripeClient.InvoiceItems().New(&stripe.InvoiceItemParams{
|
2023-03-14 02:59:24 +00:00
|
|
|
Params: stripe.Params{Context: ctx},
|
2023-01-20 20:56:12 +00:00
|
|
|
Amount: &amount,
|
|
|
|
Currency: &curr,
|
|
|
|
Customer: &cus2,
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
inv, err := satellite.API.Payments.StripeClient.Invoices().New(&stripe.InvoiceParams{
|
2023-03-14 02:59:24 +00:00
|
|
|
Params: stripe.Params{Context: ctx},
|
2023-01-20 20:56:12 +00:00
|
|
|
Customer: &cus1,
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
2023-02-13 17:32:39 +00:00
|
|
|
|
2023-03-14 02:59:24 +00:00
|
|
|
finalizeParams := &stripe.InvoiceFinalizeParams{Params: stripe.Params{Context: ctx}}
|
|
|
|
|
|
|
|
inv, err = satellite.API.Payments.StripeClient.Invoices().FinalizeInvoice(inv.ID, finalizeParams)
|
2023-02-13 17:32:39 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, stripe.InvoiceStatusOpen, inv.Status)
|
|
|
|
|
2023-01-20 20:56:12 +00:00
|
|
|
invWithDue, err := satellite.API.Payments.StripeClient.Invoices().New(&stripe.InvoiceParams{
|
2023-03-14 02:59:24 +00:00
|
|
|
Params: stripe.Params{Context: ctx},
|
2023-01-20 20:56:12 +00:00
|
|
|
Customer: &cus2,
|
|
|
|
DueDate: &due,
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2023-03-14 02:59:24 +00:00
|
|
|
invWithDue, err = satellite.API.Payments.StripeClient.Invoices().FinalizeInvoice(invWithDue.ID, finalizeParams)
|
2023-02-13 17:32:39 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, stripe.InvoiceStatusOpen, invWithDue.Status)
|
|
|
|
|
2023-01-20 20:56:12 +00:00
|
|
|
err = satellite.API.Payments.StripeService.PayInvoices(ctx, time.Time{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2023-03-14 02:59:24 +00:00
|
|
|
iter := satellite.API.Payments.StripeClient.Invoices().List(&stripe.InvoiceListParams{
|
|
|
|
ListParams: stripe.ListParams{Context: ctx},
|
|
|
|
})
|
2023-01-20 20:56:12 +00:00
|
|
|
for iter.Next() {
|
|
|
|
i := iter.Invoice()
|
|
|
|
if i.ID == inv.ID {
|
|
|
|
require.Equal(t, stripe.InvoiceStatusPaid, i.Status)
|
|
|
|
}
|
|
|
|
// when due date is set invoice should not be paid
|
|
|
|
if i.ID == invWithDue.ID {
|
2023-02-13 17:32:39 +00:00
|
|
|
require.Equal(t, stripe.InvoiceStatusOpen, i.Status)
|
2023-01-20 20:56:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|