diff --git a/cmd/satellite/main.go b/cmd/satellite/main.go index 86e8bef06..e2cdae14c 100644 --- a/cmd/satellite/main.go +++ b/cmd/satellite/main.go @@ -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) } diff --git a/satellite/payments/stripecoinpayments/migrate_credits.go b/satellite/payments/stripecoinpayments/migrate_credits.go deleted file mode 100644 index 27e9ae607..000000000 --- a/satellite/payments/stripecoinpayments/migrate_credits.go +++ /dev/null @@ -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) -} diff --git a/satellite/payments/stripecoinpayments/migrate_credits_test.go b/satellite/payments/stripecoinpayments/migrate_credits_test.go deleted file mode 100644 index 62aea5190..000000000 --- a/satellite/payments/stripecoinpayments/migrate_credits_test.go +++ /dev/null @@ -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) - } - } - } - }) -}