storj/satellite/payments/billing/transactions_test.go
dlamarmorgan afe58323f9 satellitedb: remove use of batch payment insert
Removed batch insert of payments since they do not guarantee order. Order of payments sent to the payments DB is important, because the billing chore will request new payments based on the last received payment. If the last payment inserted is not the last payment received, duplicate payments will be inserted into the billing table.

Change-Id: Ic3335c89fa8031f7bc16f417ca23ed83301ef8f6
2022-08-30 14:45:55 -07:00

284 lines
8.9 KiB
Go

// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
package billing_test
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"storj.io/common/testcontext"
"storj.io/common/testrand"
"storj.io/storj/private/blockchain"
"storj.io/storj/satellite"
"storj.io/storj/satellite/payments/billing"
"storj.io/storj/satellite/payments/monetary"
"storj.io/storj/satellite/satellitedb/satellitedbtest"
)
func TestTransactionsDBList(t *testing.T) {
const (
limit = 3
transactionCount = limit * 4
)
// create transactions
userID := testrand.UUID()
var txs []billing.Transaction
var txStatus billing.TransactionStatus
var txType billing.TransactionType
for i := 0; i < transactionCount; i++ {
txSource := "storjscan"
txStatus = billing.TransactionStatusCompleted
txType = billing.TransactionTypeCredit
if i%2 == 0 {
txSource = "stripe"
}
if i%3 == 0 {
txSource = "coinpayments"
}
address, err := blockchain.BytesToAddress(testrand.Bytes(20))
require.NoError(t, err)
metadata, err := json.Marshal(map[string]interface{}{
"ReferenceID": "some stripe invoice ID",
"Wallet": address.Hex(),
})
require.NoError(t, err)
createTX := billing.Transaction{
UserID: userID,
Amount: monetary.AmountFromBaseUnits(4, monetary.USDollars),
Description: "credit from storjscan payment",
Source: txSource,
Status: txStatus,
Type: txType,
Metadata: metadata,
Timestamp: time.Now(),
CreatedAt: time.Now(),
}
txs = append(txs, createTX)
}
t.Run("insert and list transactions", func(t *testing.T) {
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
for _, tx := range txs {
_, err := db.Billing().Insert(ctx, tx)
require.NoError(t, err)
}
storjscanTXs, err := db.Billing().List(ctx, userID)
require.NoError(t, err)
require.Equal(t, 12, len(storjscanTXs))
for _, act := range storjscanTXs {
for _, exp := range txs {
if act.ID == exp.ID {
compareTransactions(t, exp, act)
break
}
}
}
})
})
}
func TestTransactionsDBBalance(t *testing.T) {
tenUSD := monetary.AmountFromBaseUnits(1000, monetary.USDollars)
twentyUSD := monetary.AmountFromBaseUnits(2000, monetary.USDollars)
thirtyUSD := monetary.AmountFromBaseUnits(3000, monetary.USDollars)
fortyUSD := monetary.AmountFromBaseUnits(4000, monetary.USDollars)
negativeTwentyUSD := monetary.AmountFromBaseUnits(-2000, monetary.USDollars)
userID := testrand.UUID()
address, err := blockchain.BytesToAddress(testrand.Bytes(20))
require.NoError(t, err)
metadata, err := json.Marshal(map[string]interface{}{
"ReferenceID": "some stripe invoice ID",
"Wallet": address.Hex(),
})
require.NoError(t, err)
credit10TX := billing.Transaction{
UserID: userID,
Amount: tenUSD,
Description: "credit from storjscan payment",
Source: "storjscan",
Status: billing.TransactionStatusCompleted,
Type: billing.TransactionTypeCredit,
Metadata: metadata,
Timestamp: time.Now().Add(time.Second),
CreatedAt: time.Now(),
}
credit30TX := billing.Transaction{
UserID: userID,
Amount: thirtyUSD,
Description: "credit from storjscan payment",
Source: "storjscan",
Status: billing.TransactionStatusCompleted,
Type: billing.TransactionTypeCredit,
Metadata: metadata,
Timestamp: time.Now().Add(time.Second * 2),
CreatedAt: time.Now(),
}
charge20TX := billing.Transaction{
UserID: userID,
Amount: negativeTwentyUSD,
Description: "charge for storage and bandwidth",
Source: "storjscan",
Status: billing.TransactionStatusCompleted,
Type: billing.TransactionTypeDebit,
Metadata: metadata,
Timestamp: time.Now().Add(time.Second * 3),
CreatedAt: time.Now(),
}
t.Run("add 10 USD to account", func(t *testing.T) {
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
_, err := db.Billing().Insert(ctx, credit10TX)
require.NoError(t, err)
txs, err := db.Billing().List(ctx, userID)
require.NoError(t, err)
require.Len(t, txs, 1)
compareTransactions(t, credit10TX, txs[0])
balance, err := db.Billing().GetBalance(ctx, userID)
require.NoError(t, err)
require.Equal(t, tenUSD.BaseUnits(), balance)
})
})
t.Run("add 10 and 30 USD to account", func(t *testing.T) {
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
_, err := db.Billing().Insert(ctx, credit10TX)
require.NoError(t, err)
_, err = db.Billing().Insert(ctx, credit30TX)
require.NoError(t, err)
txs, err := db.Billing().List(ctx, userID)
require.NoError(t, err)
require.Len(t, txs, 2)
compareTransactions(t, credit30TX, txs[0])
compareTransactions(t, credit10TX, txs[1])
balance, err := db.Billing().GetBalance(ctx, userID)
require.NoError(t, err)
require.Equal(t, fortyUSD.BaseUnits(), balance)
})
})
t.Run("add 10 USD, add 30 USD, subtract 20 USD", func(t *testing.T) {
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
_, err := db.Billing().Insert(ctx, credit10TX)
require.NoError(t, err)
_, err = db.Billing().Insert(ctx, credit30TX)
require.NoError(t, err)
_, err = db.Billing().Insert(ctx, charge20TX)
require.NoError(t, err)
txs, err := db.Billing().List(ctx, userID)
require.NoError(t, err)
require.Len(t, txs, 3)
compareTransactions(t, charge20TX, txs[0])
compareTransactions(t, credit30TX, txs[1])
compareTransactions(t, credit10TX, txs[2])
balance, err := db.Billing().GetBalance(ctx, userID)
require.NoError(t, err)
require.Equal(t, twentyUSD.BaseUnits(), balance)
})
})
}
func TestUpdateTransactions(t *testing.T) {
tenUSD := monetary.AmountFromBaseUnits(1000, monetary.USDollars)
userID := testrand.UUID()
address, err := blockchain.BytesToAddress(testrand.Bytes(20))
require.NoError(t, err)
metadata, err := json.Marshal(map[string]interface{}{
"ReferenceID": "some stripe invoice ID",
"Wallet": address.Hex(),
})
require.NoError(t, err)
credit10TX := billing.Transaction{
UserID: userID,
Amount: tenUSD,
Description: "credit from storjscan payment",
Source: "storjscan",
Status: billing.TransactionStatusCompleted,
Type: billing.TransactionTypeCredit,
Metadata: metadata,
Timestamp: time.Now().Add(time.Second),
CreatedAt: time.Now(),
}
t.Run("update metadata", func(t *testing.T) {
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
txID, err := db.Billing().Insert(ctx, credit10TX)
require.NoError(t, err)
newAddress, err := blockchain.BytesToAddress(testrand.Bytes(20))
require.NoError(t, err)
metadata, err := json.Marshal(map[string]interface{}{
"Wallet": newAddress.Hex(),
})
require.NoError(t, err)
err = db.Billing().UpdateMetadata(ctx, txID, metadata)
require.NoError(t, err)
expMetadata, err := json.Marshal(map[string]interface{}{
"ReferenceID": "some stripe invoice ID",
"Wallet": newAddress.Hex(),
})
require.NoError(t, err)
credit10TX.Metadata = expMetadata
tx, err := db.Billing().List(ctx, userID)
require.NoError(t, err)
compareTransactions(t, credit10TX, tx[0])
})
})
t.Run("update status", func(t *testing.T) {
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
txID, err := db.Billing().Insert(ctx, credit10TX)
require.NoError(t, err)
err = db.Billing().UpdateStatus(ctx, txID, billing.TransactionStatusCancelled)
require.NoError(t, err)
credit10TX.Status = billing.TransactionStatusCancelled
tx, err := db.Billing().List(ctx, userID)
require.NoError(t, err)
compareTransactions(t, credit10TX, tx[0])
})
})
}
func TestUpdateMetadata(t *testing.T) {
}
// compareTransactions is a helper method to compare tx used to create db entry,
// with the tx returned from the db. Method doesn't compare created at field, but
// ensures that is not empty.
func compareTransactions(t *testing.T, exp, act billing.Transaction) {
assert.Equal(t, exp.UserID, act.UserID)
assert.Equal(t, exp.Amount, act.Amount)
assert.Equal(t, exp.Description, act.Description)
assert.Equal(t, exp.Status, act.Status)
assert.Equal(t, exp.Source, act.Source)
assert.Equal(t, exp.Type, act.Type)
var expUpdatedMetadata map[string]string
var actUpdatedMetadata map[string]string
err := json.Unmarshal(exp.Metadata, &expUpdatedMetadata)
require.NoError(t, err)
err = json.Unmarshal(act.Metadata, &actUpdatedMetadata)
require.NoError(t, err)
assert.Equal(t, expUpdatedMetadata["ReferenceID"], actUpdatedMetadata["ReferenceID"])
assert.Equal(t, expUpdatedMetadata["Wallet"], actUpdatedMetadata["Wallet"])
assert.WithinDuration(t, exp.Timestamp, act.Timestamp, time.Microsecond) // database timestamps use microsecond precision
assert.False(t, act.CreatedAt.IsZero())
}