1// Copyright (C) 2020 Storj Labs, Inc. 2// See LICENSE for copying information. 3 4package stripecoinpayments_test 5 6import ( 7 "strconv" 8 "testing" 9 "time" 10 11 "github.com/stretchr/testify/require" 12 "go.uber.org/zap" 13 14 "storj.io/common/memory" 15 "storj.io/common/pb" 16 "storj.io/common/testcontext" 17 "storj.io/storj/private/testplanet" 18 "storj.io/storj/satellite" 19 "storj.io/storj/satellite/accounting" 20 "storj.io/storj/satellite/console" 21 "storj.io/storj/satellite/metabase" 22 "storj.io/storj/satellite/payments/stripecoinpayments" 23) 24 25func TestService_InvoiceElementsProcessing(t *testing.T) { 26 testplanet.Run(t, testplanet.Config{ 27 SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0, 28 Reconfigure: testplanet.Reconfigure{ 29 Satellite: func(log *zap.Logger, index int, config *satellite.Config) { 30 config.Payments.StripeCoinPayments.ListingLimit = 4 31 }, 32 }, 33 }, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { 34 satellite := planet.Satellites[0] 35 36 // pick a specific date so that it doesn't fail if it's the last day of the month 37 // keep month + 1 because user needs to be created before calculation 38 period := time.Date(time.Now().Year(), time.Now().Month()+1, 20, 0, 0, 0, 0, time.UTC) 39 40 numberOfProjects := 19 41 // generate test data, each user has one project and some credits 42 for i := 0; i < numberOfProjects; i++ { 43 user, err := satellite.AddUser(ctx, console.CreateUser{ 44 FullName: "testuser" + strconv.Itoa(i), 45 Email: "user@test" + strconv.Itoa(i), 46 }, 1) 47 require.NoError(t, err) 48 49 project, err := satellite.AddProject(ctx, user.ID, "testproject-"+strconv.Itoa(i)) 50 require.NoError(t, err) 51 52 err = satellite.DB.Orders().UpdateBucketBandwidthSettle(ctx, project.ID, []byte("testbucket"), 53 pb.PieceAction_GET, int64(i+10)*memory.GiB.Int64(), 0, period) 54 require.NoError(t, err) 55 } 56 57 satellite.API.Payments.Service.SetNow(func() time.Time { 58 return time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC) 59 }) 60 err := satellite.API.Payments.Service.PrepareInvoiceProjectRecords(ctx, period) 61 require.NoError(t, err) 62 63 start := time.Date(period.Year(), period.Month(), 1, 0, 0, 0, 0, time.UTC) 64 end := time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC) 65 66 // check if we have project record for each project 67 recordsPage, err := satellite.DB.StripeCoinPayments().ProjectRecords().ListUnapplied(ctx, 0, 40, start, end) 68 require.NoError(t, err) 69 require.Equal(t, numberOfProjects, len(recordsPage.Records)) 70 71 err = satellite.API.Payments.Service.InvoiceApplyProjectRecords(ctx, period) 72 require.NoError(t, err) 73 74 // verify that we applied all unapplied project records 75 recordsPage, err = satellite.DB.StripeCoinPayments().ProjectRecords().ListUnapplied(ctx, 0, 40, start, end) 76 require.NoError(t, err) 77 require.Equal(t, 0, len(recordsPage.Records)) 78 }) 79} 80 81func TestService_InvoiceUserWithManyProjects(t *testing.T) { 82 testplanet.Run(t, testplanet.Config{ 83 SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0, 84 Reconfigure: testplanet.Reconfigure{ 85 Satellite: func(log *zap.Logger, index int, config *satellite.Config) { 86 config.Payments.StripeCoinPayments.ListingLimit = 4 87 }, 88 }, 89 }, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { 90 satellite := planet.Satellites[0] 91 payments := satellite.API.Payments 92 93 // pick a specific date so that it doesn't fail if it's the last day of the month 94 // keep month + 1 because user needs to be created before calculation 95 period := time.Date(time.Now().Year(), time.Now().Month()+1, 20, 0, 0, 0, 0, time.UTC) 96 97 payments.Service.SetNow(func() time.Time { 98 return time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC) 99 }) 100 start := time.Date(period.Year(), period.Month(), 1, 0, 0, 0, 0, time.UTC) 101 end := time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC) 102 103 numberOfProjects := 5 104 storageHours := 24 105 106 user, err := satellite.AddUser(ctx, console.CreateUser{ 107 FullName: "testuser", 108 Email: "user@test", 109 }, numberOfProjects) 110 require.NoError(t, err) 111 112 projects := make([]*console.Project, numberOfProjects) 113 projectsEgress := make([]int64, len(projects)) 114 projectsStorage := make([]int64, len(projects)) 115 for i := 0; i < len(projects); i++ { 116 projects[i], err = satellite.AddProject(ctx, user.ID, "testproject-"+strconv.Itoa(i)) 117 require.NoError(t, err) 118 119 // generate egress 120 projectsEgress[i] = int64(i+10) * memory.GiB.Int64() 121 err = satellite.DB.Orders().UpdateBucketBandwidthSettle(ctx, projects[i].ID, []byte("testbucket"), 122 pb.PieceAction_GET, projectsEgress[i], 0, period) 123 require.NoError(t, err) 124 125 // generate storage 126 // we need at least two tallies across time to calculate storage 127 projectsStorage[i] = int64(i+1) * memory.TiB.Int64() 128 tally := &accounting.BucketTally{ 129 BucketLocation: metabase.BucketLocation{ 130 ProjectID: projects[i].ID, 131 BucketName: "testbucket", 132 }, 133 TotalBytes: projectsStorage[i], 134 TotalSegments: int64(i + 1), 135 } 136 tallies := map[metabase.BucketLocation]*accounting.BucketTally{ 137 {}: tally, 138 } 139 err = satellite.DB.ProjectAccounting().SaveTallies(ctx, period, tallies) 140 require.NoError(t, err) 141 142 err = satellite.DB.ProjectAccounting().SaveTallies(ctx, period.Add(time.Duration(storageHours)*time.Hour), tallies) 143 require.NoError(t, err) 144 145 // verify that projects don't have records yet 146 projectRecord, err := satellite.DB.StripeCoinPayments().ProjectRecords().Get(ctx, projects[i].ID, start, end) 147 require.NoError(t, err) 148 require.Nil(t, projectRecord) 149 } 150 151 err = payments.Service.PrepareInvoiceProjectRecords(ctx, period) 152 require.NoError(t, err) 153 154 for i := 0; i < len(projects); i++ { 155 projectRecord, err := satellite.DB.StripeCoinPayments().ProjectRecords().Get(ctx, projects[i].ID, start, end) 156 require.NoError(t, err) 157 require.NotNil(t, projectRecord) 158 require.Equal(t, projects[i].ID, projectRecord.ProjectID) 159 require.Equal(t, projectsEgress[i], projectRecord.Egress) 160 161 expectedStorage := float64(projectsStorage[i] * int64(storageHours)) 162 require.Equal(t, expectedStorage, projectRecord.Storage) 163 164 expectedSegmentsCount := float64((i + 1) * storageHours) 165 require.Equal(t, expectedSegmentsCount, projectRecord.Segments) 166 } 167 168 // run all parts of invoice generation to see if there are no unexpected errors 169 err = payments.Service.InvoiceApplyProjectRecords(ctx, period) 170 require.NoError(t, err) 171 172 err = payments.Service.CreateInvoices(ctx, period) 173 require.NoError(t, err) 174 }) 175} 176 177func TestService_ProjectsWithMembers(t *testing.T) { 178 testplanet.Run(t, testplanet.Config{ 179 SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0, 180 Reconfigure: testplanet.Reconfigure{ 181 Satellite: func(log *zap.Logger, index int, config *satellite.Config) { 182 config.Payments.StripeCoinPayments.ListingLimit = 4 183 }, 184 }, 185 }, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { 186 satellite := planet.Satellites[0] 187 188 // pick a specific date so that it doesn't fail if it's the last day of the month 189 // keep month + 1 because user needs to be created before calculation 190 period := time.Date(time.Now().Year(), time.Now().Month()+1, 20, 0, 0, 0, 0, time.UTC) 191 192 numberOfUsers := 5 193 users := make([]*console.User, numberOfUsers) 194 projects := make([]*console.Project, numberOfUsers) 195 for i := 0; i < numberOfUsers; i++ { 196 var err error 197 198 users[i], err = satellite.AddUser(ctx, console.CreateUser{ 199 FullName: "testuser" + strconv.Itoa(i), 200 Email: "user@test" + strconv.Itoa(i), 201 }, 1) 202 require.NoError(t, err) 203 204 projects[i], err = satellite.AddProject(ctx, users[i].ID, "testproject-"+strconv.Itoa(i)) 205 require.NoError(t, err) 206 } 207 208 // all users are members in all projects 209 for _, project := range projects { 210 for _, user := range users { 211 if project.OwnerID != user.ID { 212 _, err := satellite.DB.Console().ProjectMembers().Insert(ctx, user.ID, project.ID) 213 require.NoError(t, err) 214 } 215 } 216 } 217 218 satellite.API.Payments.Service.SetNow(func() time.Time { 219 return time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC) 220 }) 221 err := satellite.API.Payments.Service.PrepareInvoiceProjectRecords(ctx, period) 222 require.NoError(t, err) 223 224 start := time.Date(period.Year(), period.Month(), 1, 0, 0, 0, 0, time.UTC) 225 end := time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC) 226 227 recordsPage, err := satellite.DB.StripeCoinPayments().ProjectRecords().ListUnapplied(ctx, 0, 40, start, end) 228 require.NoError(t, err) 229 require.Equal(t, len(projects), len(recordsPage.Records)) 230 }) 231} 232 233func TestService_InvoiceItemsFromProjectRecord(t *testing.T) { 234 testplanet.Run(t, testplanet.Config{ 235 SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0, 236 }, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { 237 satellite := planet.Satellites[0] 238 239 // these numbers are fraction of cents, not of dollars. 240 expectedStoragePrice := 0.001 241 expectedEgressPrice := 0.0045 242 expectedSegmentPrice := 0.00022 243 244 type TestCase struct { 245 Storage float64 246 Egress int64 247 Segments float64 248 249 StorageQuantity int64 250 EgressQuantity int64 251 SegmentsQuantity int64 252 } 253 254 var testCases = []TestCase{ 255 {}, // all zeros 256 { 257 Storage: 10000000000, // Byte-Hours 258 // storage quantity is calculated to Megabyte-Months 259 // (10000000000 / 1000000) Byte-Hours to Megabytes-Hours 260 // round(10000 / 720) Megabytes-Hours to Megabyte-Months, 720 - hours in month 261 StorageQuantity: 14, // Megabyte-Months 262 }, 263 { 264 Egress: 134 * memory.GB.Int64(), // Bytes 265 // egress quantity is calculated to Megabytes 266 // (134000000000 / 1000000) Bytes to Megabytes 267 EgressQuantity: 134000, // Megabytes 268 }, 269 { 270 Segments: 400000, // Segment-Hours 271 // object quantity is calculated to Segment-Months 272 // round(400000 / 720) Segment-Hours to Segment-Months, 720 - hours in month 273 SegmentsQuantity: 556, // Segment-Months 274 }, 275 } 276 277 for _, tc := range testCases { 278 record := stripecoinpayments.ProjectRecord{ 279 Storage: tc.Storage, 280 Egress: tc.Egress, 281 Segments: tc.Segments, 282 } 283 284 items := satellite.API.Payments.Service.InvoiceItemsFromProjectRecord("project name", record) 285 286 require.Equal(t, tc.StorageQuantity, *items[0].Quantity) 287 require.Equal(t, expectedStoragePrice, *items[0].UnitAmountDecimal) 288 289 require.Equal(t, tc.EgressQuantity, *items[1].Quantity) 290 require.Equal(t, expectedEgressPrice, *items[1].UnitAmountDecimal) 291 292 require.Equal(t, tc.SegmentsQuantity, *items[2].Quantity) 293 require.Equal(t, expectedSegmentPrice, *items[2].UnitAmountDecimal) 294 } 295 }) 296} 297