satellite/payments: add STORJ amount and rate to Stripe TX metadata

Jira: https://storjlabs.atlassian.net/browse/USR-968

We want to keep track of the STORJ amount and exchange rate in the
metadata of Stripe Customer Balance Transaction to be able to generate
reports without the need of requesting CoinPayments for this info.

Change-Id: Ia93af95706cd2312cf688f044874495279fe8fa2
This commit is contained in:
Kaloyan Raev 2020-07-17 18:17:21 +03:00
parent 0949731caa
commit a20e85824a
3 changed files with 94 additions and 8 deletions

View File

@ -336,6 +336,8 @@ func (service *Service) applyTransactionBalance(ctx context.Context, tx Transact
Description: stripe.String(StripeDepositTransactionDescription),
}
params.AddMetadata("txID", tx.ID.String())
params.AddMetadata("storj_amount", tx.Amount.String())
params.AddMetadata("storj_usd_rate", rate.String())
_, err = service.stripeClient.CustomerBalanceTransactions().New(params)
if err != nil {
return err

View File

@ -18,7 +18,18 @@ import (
"storj.io/common/uuid"
)
// MockStripeClient Stripe client mock.
// mockClient singleton of mockStripeClient.
//
// The satellite has a Core part and API part which mostly duplicate each
// other. Each of them have a StripeClient instance. This is not a problem in
// production, because the stripeClient implementation is stateless and calls
// the Web API of the same Stripe backend. But it is a problem in test
// environments as the mockStripeClient client is stateful - the data is stored
// in in-memory maps. Therefore, we need it to be a singleton, so the Core and
// API parts share the same state.
var mockClient StripeClient
// mockStripeClient Stripe client mock.
type mockStripeClient struct {
customers *mockCustomers
paymentMethods *mockPaymentMethods
@ -30,14 +41,17 @@ type mockStripeClient struct {
// NewStripeMock creates new Stripe client mock.
func NewStripeMock() StripeClient {
return &mockStripeClient{
customers: newMockCustomers(),
paymentMethods: &mockPaymentMethods{},
invoices: &mockInvoices{},
invoiceItems: &mockInvoiceItems{},
customerBalanceTransactions: newMockCustomerBalanceTransactions(),
charges: &mockCharges{},
if mockClient == nil {
mockClient = &mockStripeClient{
customers: newMockCustomers(),
paymentMethods: &mockPaymentMethods{},
invoices: &mockInvoices{},
invoiceItems: &mockInvoiceItems{},
customerBalanceTransactions: newMockCustomerBalanceTransactions(),
charges: &mockCharges{},
}
}
return mockClient
}
func (m *mockStripeClient) Customers() StripeCustomers {

View File

@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stripe/stripe-go"
"github.com/zeebo/errs"
"storj.io/common/errs2"
@ -20,6 +21,7 @@ import (
"storj.io/common/testcontext"
"storj.io/common/testrand"
"storj.io/common/uuid"
"storj.io/storj/private/testplanet"
"storj.io/storj/satellite"
"storj.io/storj/satellite/payments/coinpayments"
"storj.io/storj/satellite/payments/stripecoinpayments"
@ -339,3 +341,71 @@ func compareTransactions(t *testing.T, exp, act stripecoinpayments.Transaction)
assert.Equal(t, exp.Timeout, act.Timeout)
assert.False(t, act.CreatedAt.IsZero())
}
func TestTransactions_ApplyTransactionBalance(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
satellite := planet.Satellites[0]
transactions := satellite.API.DB.StripeCoinPayments().Transactions()
userID := planet.Uplinks[0].Projects[0].Owner.ID
satellite.Core.Payments.Chore.TransactionCycle.Pause()
satellite.Core.Payments.Chore.AccountBalanceCycle.Pause()
// Emulate a deposit through CoinPayments.
txID := coinpayments.TransactionID("testID")
storjAmount, ok := new(big.Float).SetString("100")
require.True(t, ok)
storjUSDRate, ok := new(big.Float).SetString("0.2")
require.True(t, ok)
createTx := stripecoinpayments.Transaction{
ID: txID,
AccountID: userID,
Address: "testAddress",
Amount: *storjAmount,
Received: *storjAmount,
Status: coinpayments.StatusPending,
Key: "testKey",
Timeout: time.Second * 60,
}
tx, err := transactions.Insert(ctx, createTx)
require.NoError(t, err)
require.NotNil(t, tx)
update := stripecoinpayments.TransactionUpdate{
TransactionID: createTx.ID,
Status: coinpayments.StatusReceived,
Received: *storjAmount,
}
err = transactions.Update(ctx, []stripecoinpayments.TransactionUpdate{update}, coinpayments.TransactionIDList{createTx.ID})
require.NoError(t, err)
// Check that the CoinPayments transaction is waiting to be applied to the Stripe customer balance.
page, err := transactions.ListUnapplied(ctx, 0, 1, time.Now())
require.NoError(t, err)
require.Len(t, page.Transactions, 1)
err = transactions.LockRate(ctx, txID, storjUSDRate)
require.NoError(t, err)
// Trigger the AccountBalanceCycle. This calls Service.applyTransactionBalance()
satellite.Core.Payments.Chore.AccountBalanceCycle.TriggerWait()
cusID, err := satellite.API.DB.StripeCoinPayments().Customers().GetCustomerID(ctx, userID)
require.NoError(t, err)
// Check that the CoinPayments deposit is reflected in the Stripe customer balance.
it := satellite.API.Payments.Stripe.CustomerBalanceTransactions().List(&stripe.CustomerBalanceTransactionListParams{Customer: stripe.String(cusID)})
require.NoError(t, it.Err())
require.True(t, it.Next())
cbt := it.CustomerBalanceTransaction()
require.EqualValues(t, -2000, cbt.Amount)
require.EqualValues(t, txID, cbt.Metadata["txID"])
require.EqualValues(t, "100", cbt.Metadata["storj_amount"])
require.EqualValues(t, "0.2", cbt.Metadata["storj_usd_rate"])
})
}