satellite/payments: remove migare-credits billing command
This was a one-time command and it has been already executed on all production satellites. Change-Id: Ic58a151c3e5da9c139df875f6517f37275a1ebf5
This commit is contained in:
parent
fd740295ec
commit
12a15e5a6a
@ -225,12 +225,6 @@ var (
|
||||
Long: "Ensures that we have a stripe customer for every satellite user.",
|
||||
RunE: cmdStripeCustomer,
|
||||
}
|
||||
migrateCreditsCmd = &cobra.Command{
|
||||
Use: "migrate-credits",
|
||||
Short: "Migrates credits to Stripe",
|
||||
Long: "Migrates credits received for STORJ token deposits from Satellite DB to Stripe balance.",
|
||||
RunE: cmdMigrateCredits,
|
||||
}
|
||||
|
||||
runCfg Satellite
|
||||
setupCfg Satellite
|
||||
@ -301,7 +295,6 @@ func init() {
|
||||
billingCmd.AddCommand(createCustomerInvoicesCmd)
|
||||
billingCmd.AddCommand(finalizeCustomerInvoicesCmd)
|
||||
billingCmd.AddCommand(stripeCustomerCmd)
|
||||
billingCmd.AddCommand(migrateCreditsCmd)
|
||||
process.Bind(runCmd, &runCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
|
||||
process.Bind(runMigrationCmd, &runCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
|
||||
process.Bind(runAPICmd, &runCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
|
||||
@ -324,7 +317,6 @@ func init() {
|
||||
process.Bind(createCustomerInvoicesCmd, &runCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
|
||||
process.Bind(finalizeCustomerInvoicesCmd, &runCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
|
||||
process.Bind(stripeCustomerCmd, &runCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
|
||||
process.Bind(migrateCreditsCmd, &runCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
|
||||
}
|
||||
|
||||
func cmdRun(cmd *cobra.Command, args []string) (err error) {
|
||||
@ -723,14 +715,6 @@ func cmdStripeCustomer(cmd *cobra.Command, args []string) (err error) {
|
||||
return generateStripeCustomers(ctx)
|
||||
}
|
||||
|
||||
func cmdMigrateCredits(cmd *cobra.Command, args []string) (err error) {
|
||||
ctx, _ := process.Ctx(cmd)
|
||||
|
||||
return runBillingCmd(func(payments *stripecoinpayments.Service, _ *dbx.DB) error {
|
||||
return payments.MigrateCredits(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
process.ExecCustomDebug(rootCmd)
|
||||
}
|
||||
|
@ -1,245 +0,0 @@
|
||||
// Copyright (C) 2020 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package stripecoinpayments
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/stripe/stripe-go"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
)
|
||||
|
||||
type migrationStats struct {
|
||||
processedCustomers int
|
||||
customersWithCreditRecords int
|
||||
customersWithPositiveBalance int
|
||||
migratedBalanceAmount int64
|
||||
migratedHistoryAmount int64
|
||||
migratedCreditRecords int
|
||||
}
|
||||
|
||||
// MigrateCredits migrates credits from Satellite DB to Stripe balance.
|
||||
func (service *Service) MigrateCredits(ctx context.Context) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
now := time.Now().UTC()
|
||||
stats := &migrationStats{}
|
||||
|
||||
cusPage, err := service.db.Customers().List(ctx, 0, service.listingLimit, now)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
for _, cus := range cusPage.Customers {
|
||||
if err = ctx.Err(); err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
if err = service.migrateCredits(ctx, cus, stats); err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
for cusPage.Next {
|
||||
if err = ctx.Err(); err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
cusPage, err = service.db.Customers().List(ctx, cusPage.NextOffset, service.listingLimit, now)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
for _, cus := range cusPage.Customers {
|
||||
if err = ctx.Err(); err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
if err = service.migrateCredits(ctx, cus, stats); err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
service.log.Info("Migration complete.",
|
||||
zap.Int("Processed Customers", stats.processedCustomers),
|
||||
zap.Int("Customers With Credit Records", stats.customersWithCreditRecords),
|
||||
zap.Int("Customers With Positive Credit Balance", stats.customersWithPositiveBalance),
|
||||
zap.Int64("Balance Amount in Cents Migrated to Stripe", stats.migratedBalanceAmount),
|
||||
zap.Int("Credit History Records Migrated to Stripe", stats.migratedCreditRecords),
|
||||
zap.Int64("Credit History Amount in Cents Migrated to Stripe", stats.migratedHistoryAmount),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) migrateCredits(ctx context.Context, customer Customer, stats *migrationStats) error {
|
||||
stats.processedCustomers++
|
||||
|
||||
err := service.migrateCreditsBalance(ctx, customer, stats)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
err = service.migrateCreditsHistory(ctx, customer, stats)
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
func (service *Service) migrateCreditsBalance(ctx context.Context, customer Customer, stats *migrationStats) error {
|
||||
balance, err := service.db.Credits().Balance(ctx, customer.UserID)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
if balance <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
stats.customersWithPositiveBalance++
|
||||
|
||||
service.log.Info("Found positive credit balance.",
|
||||
zap.Int64("Balance", balance),
|
||||
zap.Stringer("User ID", customer.UserID),
|
||||
zap.String("Customer ID", customer.ID),
|
||||
)
|
||||
|
||||
// Check for Stripe balance transactions created from previous failed attempt
|
||||
var txDone bool
|
||||
it := service.stripeClient.CustomerBalanceTransactions().List(&stripe.CustomerBalanceTransactionListParams{Customer: stripe.String(customer.ID)})
|
||||
for it.Next() {
|
||||
cbt := it.CustomerBalanceTransaction()
|
||||
|
||||
if cbt.Type != stripe.CustomerBalanceTransactionTypeAdjustment {
|
||||
continue
|
||||
}
|
||||
|
||||
if cbt.Description != StripeMigratedDepositBonusTransactionDescription {
|
||||
continue
|
||||
}
|
||||
|
||||
if cbt.Amount != -balance {
|
||||
return Error.New("amount mismatch in found balance transaction, want: %d, got: %d", -balance, cbt.Amount)
|
||||
}
|
||||
|
||||
service.log.Warn("Found balance transaction in Stripe from previous attempt.",
|
||||
zap.Int64("Amount", cbt.Amount),
|
||||
zap.Time("Created At", time.Unix(cbt.Created, 0)),
|
||||
zap.Stringer("User ID", customer.UserID),
|
||||
zap.String("Customer ID", customer.ID),
|
||||
)
|
||||
|
||||
txDone = true
|
||||
}
|
||||
|
||||
// Add the unspent credits to Stripe balance
|
||||
if !txDone {
|
||||
params := &stripe.CustomerBalanceTransactionParams{
|
||||
Amount: stripe.Int64(-balance),
|
||||
Customer: stripe.String(customer.ID),
|
||||
Currency: stripe.String(string(stripe.CurrencyUSD)),
|
||||
Description: stripe.String(StripeMigratedDepositBonusTransactionDescription),
|
||||
}
|
||||
|
||||
stats.migratedBalanceAmount += balance
|
||||
|
||||
service.log.Info("Crediting Stripe balance.",
|
||||
zap.Int64("Amount", *params.Amount),
|
||||
zap.Stringer("User ID", customer.UserID),
|
||||
zap.String("Customer ID", customer.ID),
|
||||
)
|
||||
|
||||
_, err = service.stripeClient.CustomerBalanceTransactions().New(params)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the credits balance in the satellite DB
|
||||
creditSpendingID, err := uuid.New()
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
service.log.Info("Issuing a credit spending to clear balance in satellite DB.",
|
||||
zap.Int64("Amount", balance),
|
||||
zap.Stringer("User ID", customer.UserID),
|
||||
zap.String("Customer ID", customer.ID),
|
||||
)
|
||||
|
||||
err = service.db.Credits().InsertCreditsSpending(ctx, CreditsSpending{
|
||||
ID: creditSpendingID,
|
||||
Amount: balance,
|
||||
UserID: customer.UserID,
|
||||
Status: CreditsSpendingStatusApplied,
|
||||
Period: time.Now().UTC(),
|
||||
})
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
func (service *Service) migrateCreditsHistory(ctx context.Context, customer Customer, stats *migrationStats) error {
|
||||
credits, err := service.db.Credits().ListCredits(ctx, customer.UserID)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
if len(credits) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
stats.customersWithCreditRecords++
|
||||
|
||||
service.log.Info("Found credit records in satellite DB.",
|
||||
zap.Int("Count", len(credits)),
|
||||
zap.Stringer("User ID", customer.UserID),
|
||||
zap.String("Customer ID", customer.ID),
|
||||
)
|
||||
|
||||
stripeCustomer, err := service.stripeClient.Customers().Get(customer.ID, nil)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
metadata := stripeCustomer.Metadata
|
||||
|
||||
for _, credit := range credits {
|
||||
metadataKey := "credit_" + credit.TransactionID.String()
|
||||
_, ok := metadata[metadataKey]
|
||||
if ok {
|
||||
// the credit record already exist in metadata
|
||||
continue
|
||||
}
|
||||
|
||||
b, err := json.Marshal(credit)
|
||||
if err != nil {
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
|
||||
metadataValue := string(b)
|
||||
|
||||
stats.migratedCreditRecords++
|
||||
stats.migratedHistoryAmount += credit.Amount
|
||||
|
||||
service.log.Info("Adding credit record to the metadata of Stripe customer.",
|
||||
zap.String("Key", metadataKey),
|
||||
zap.String("Value", metadataValue),
|
||||
zap.Stringer("User ID", customer.UserID),
|
||||
zap.String("Customer ID", customer.ID),
|
||||
)
|
||||
|
||||
if metadata == nil {
|
||||
metadata = make(map[string]string)
|
||||
}
|
||||
metadata[metadataKey] = metadataValue
|
||||
}
|
||||
|
||||
params := stripe.CustomerParams{}
|
||||
params.Metadata = metadata
|
||||
|
||||
_, err = service.stripeClient.Customers().Update(customer.ID, ¶ms)
|
||||
return Error.Wrap(err)
|
||||
}
|
@ -1,150 +0,0 @@
|
||||
// Copyright (C) 2020 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package stripecoinpayments_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stripe/stripe-go"
|
||||
|
||||
"storj.io/common/testcontext"
|
||||
"storj.io/common/testrand"
|
||||
"storj.io/storj/private/testplanet"
|
||||
"storj.io/storj/satellite/payments"
|
||||
"storj.io/storj/satellite/payments/coinpayments"
|
||||
"storj.io/storj/satellite/payments/stripecoinpayments"
|
||||
)
|
||||
|
||||
func TestService_MigrateCredits(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]
|
||||
|
||||
for i, tt := range []struct {
|
||||
credits []int64
|
||||
spedings []int64
|
||||
}{
|
||||
{ // user with no credits and spendings
|
||||
},
|
||||
{ // user with a single credit, but no spendings
|
||||
credits: []int64{1000},
|
||||
},
|
||||
{ // user with a single credit and spending, some remaining balances
|
||||
credits: []int64{1000},
|
||||
spedings: []int64{700},
|
||||
},
|
||||
{ // user with a single credit and spending, no remaining balances
|
||||
credits: []int64{1000},
|
||||
spedings: []int64{1000},
|
||||
},
|
||||
{ // user with a single credit and spending, negative balances
|
||||
credits: []int64{1000},
|
||||
spedings: []int64{2000},
|
||||
},
|
||||
{ // user with a few credits and spendings, some remaining balance
|
||||
credits: []int64{100, 200, 300},
|
||||
spedings: []int64{50, 150},
|
||||
},
|
||||
{ // user with a few credits and spendings, no remaining balance
|
||||
credits: []int64{100, 200, 300},
|
||||
spedings: []int64{50, 150, 400},
|
||||
},
|
||||
{ // user with a few credits and spendings, negative remaining balance
|
||||
credits: []int64{100, 200},
|
||||
spedings: []int64{50, 150, 200},
|
||||
},
|
||||
} {
|
||||
errTag := fmt.Sprintf("%d. %+v", i, tt)
|
||||
|
||||
user, err := satellite.AddUser(ctx, "testuser"+strconv.Itoa(i), "test@test"+strconv.Itoa(i), 1)
|
||||
require.NoError(t, err, errTag)
|
||||
|
||||
project, err := satellite.AddProject(ctx, user.ID, "testproject-"+strconv.Itoa(i))
|
||||
require.NoError(t, err, errTag)
|
||||
|
||||
// Keep track of the balance when adding all credits and spendings to the database
|
||||
var initialBalance int64
|
||||
|
||||
// Add the credits to the database
|
||||
for i, credit := range tt.credits {
|
||||
initialBalance += credit
|
||||
err = satellite.DB.StripeCoinPayments().Credits().InsertCredit(ctx, payments.Credit{
|
||||
UserID: user.ID,
|
||||
TransactionID: coinpayments.TransactionID(user.ID.String() + "-" + strconv.Itoa(i)),
|
||||
Amount: credit,
|
||||
Created: time.Now().UTC(),
|
||||
})
|
||||
require.NoError(t, err, errTag)
|
||||
}
|
||||
|
||||
// Add the credit spendings to the database
|
||||
for _, spending := range tt.spedings {
|
||||
initialBalance -= spending
|
||||
err = satellite.DB.StripeCoinPayments().Credits().InsertCreditsSpending(ctx, stripecoinpayments.CreditsSpending{
|
||||
ID: testrand.UUID(),
|
||||
ProjectID: project.ID,
|
||||
UserID: user.ID,
|
||||
Amount: spending,
|
||||
Status: stripecoinpayments.CreditsSpendingStatusApplied,
|
||||
Period: time.Now().UTC(),
|
||||
Created: time.Now().UTC(),
|
||||
})
|
||||
require.NoError(t, err, errTag)
|
||||
}
|
||||
|
||||
// Check that the initial credits balance is as expected
|
||||
balance, err := satellite.DB.StripeCoinPayments().Credits().Balance(ctx, user.ID)
|
||||
require.NoError(t, err, errTag)
|
||||
require.EqualValues(t, initialBalance, balance, errTag)
|
||||
|
||||
// Migrate the credits to Stripe
|
||||
err = satellite.API.Payments.Service.MigrateCredits(ctx)
|
||||
require.NoError(t, err, errTag)
|
||||
|
||||
// Check that the credits balance in database is now zero.
|
||||
balance, err = satellite.DB.StripeCoinPayments().Credits().Balance(ctx, user.ID)
|
||||
require.NoError(t, err, errTag)
|
||||
if initialBalance > 0 {
|
||||
require.Zero(t, balance, errTag)
|
||||
} else {
|
||||
// If the initial balance was negative (should not be possible in reality) then after migration it remains the same
|
||||
require.Equal(t, initialBalance, balance, errTag)
|
||||
}
|
||||
|
||||
customerID, err := satellite.DB.StripeCoinPayments().Customers().GetCustomerID(ctx, user.ID)
|
||||
require.NoError(t, err, errTag)
|
||||
|
||||
it := satellite.API.Payments.Stripe.CustomerBalanceTransactions().List(&stripe.CustomerBalanceTransactionListParams{Customer: stripe.String(customerID)})
|
||||
if initialBalance > 0 {
|
||||
// Check that we have only one balance transaction in Stripe, which is the one adding the unspent credits balance
|
||||
require.True(t, it.Next(), errTag)
|
||||
cbt := it.CustomerBalanceTransaction()
|
||||
require.Equal(t, stripe.CustomerBalanceTransactionTypeAdjustment, cbt.Type, errTag)
|
||||
require.Equal(t, stripecoinpayments.StripeMigratedDepositBonusTransactionDescription, cbt.Description, errTag)
|
||||
require.EqualValues(t, -initialBalance, cbt.Amount, errTag)
|
||||
}
|
||||
require.False(t, it.Next(), errTag)
|
||||
|
||||
customer, err := satellite.API.Payments.Stripe.Customers().Get(customerID, nil)
|
||||
require.NoError(t, err, errTag)
|
||||
if len(tt.credits) == 0 {
|
||||
require.Nil(t, customer.Metadata, errTag)
|
||||
} else {
|
||||
// Check that the Stripe customer metadata contains the history of added credits
|
||||
require.NotNil(t, customer.Metadata, errTag)
|
||||
require.Len(t, customer.Metadata, len(tt.credits), errTag)
|
||||
for i, credit := range tt.credits {
|
||||
txID := user.ID.String() + "-" + strconv.Itoa(i)
|
||||
require.Contains(t, customer.Metadata, "credit_"+txID, errTag)
|
||||
require.Contains(t, customer.Metadata["credit_"+txID], `"credit":`+strconv.FormatInt(credit, 10), errTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user