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