satellite/{payment,console} add pending deletion user status

This change introduces a new user status, PendingDeletion to be used to
mark users before they're actually deleted. It also skips users with
this status or Deleted status when generating invoices.

Issue: https://github.com/storj/storj/issues/6302

Change-Id: I6a80d0ed1fe4f223ae00e0961f18f2f62f9b5213
This commit is contained in:
Wilfred Asomani 2023-09-27 10:31:05 +00:00 committed by Storj Robot
parent a14a18185b
commit 46ee1c1414
3 changed files with 266 additions and 4 deletions

View File

@ -144,6 +144,8 @@ const (
Active UserStatus = 1
// Deleted is a user status that he receives after deleting account.
Deleted UserStatus = 2
// PendingDeletion is a user status that he receives before deleting account.
PendingDeletion UserStatus = 3
)
// User is a database object that describes User entity.

View File

@ -172,6 +172,12 @@ func (service *Service) PrepareInvoiceProjectRecords(ctx context.Context, period
func (service *Service) processCustomers(ctx context.Context, customers []Customer, start, end time.Time) (int, error) {
var allRecords []CreateProjectRecord
for _, customer := range customers {
if inactive, err := service.isUserInactive(ctx, customer.UserID); err != nil {
return 0, err
} else if inactive {
continue
}
projects, err := service.projectsDB.GetOwn(ctx, customer.UserID)
if err != nil {
return 0, err
@ -431,6 +437,15 @@ func (service *Service) applyProjectRecords(ctx context.Context, records []Proje
return 0, errs.Wrap(err)
}
if inactive, err := service.isUserInactive(ctx, proj.OwnerID); err != nil {
return 0, errs.Wrap(err)
} else if inactive {
mu.Lock()
skipCount++
mu.Unlock()
continue
}
cusID, err := service.db.Customers().GetCustomerID(ctx, proj.OwnerID)
if err != nil {
if errors.Is(err, ErrNoCustomer) {
@ -649,6 +664,15 @@ func (service *Service) ApplyFreeTierCoupons(ctx context.Context) (err error) {
for _, c := range customersPage.Customers {
cusID := c.ID
limiter.Go(ctx, func() {
if inactive, err := service.isUserInactive(ctx, c.UserID); err != nil {
mu.Lock()
failedUsers = append(failedUsers, cusID)
mu.Unlock()
return
} else if inactive {
return
}
applied, err := service.applyFreeTierCoupon(ctx, cusID)
if err != nil {
mu.Lock()
@ -803,6 +827,15 @@ func (service *Service) createInvoices(ctx context.Context, customers []Customer
for _, cus := range customers {
cusID := cus.ID
limiter.Go(ctx, func() {
if inactive, err := service.isUserInactive(ctx, cus.UserID); err != nil {
mu.Lock()
errGrp.Add(err)
mu.Unlock()
return
} else if inactive {
return
}
inv, err := service.createInvoice(ctx, cusID, period)
if err != nil {
mu.Lock()
@ -925,6 +958,17 @@ func (service *Service) CreateBalanceInvoiceItems(ctx context.Context) (err erro
if itr.Customer().Balance <= 0 {
continue
}
userID, err := service.db.Customers().GetUserID(ctx, itr.Customer().ID)
if err != nil {
return err
}
if inactive, err := service.isUserInactive(ctx, userID); err != nil {
return err
} else if inactive {
continue
}
service.log.Info("Creating invoice item for customer prior balance", zap.String("CustomerID", itr.Customer().ID))
itemParams := &stripe.InvoiceItemParams{
Params: stripe.Params{
@ -1000,11 +1044,22 @@ func (service *Service) FinalizeInvoices(ctx context.Context) (err error) {
invoicesIterator := service.stripeClient.Invoices().List(params)
for invoicesIterator.Next() {
stripeInvoice := invoicesIterator.Invoice()
userID, err := service.db.Customers().GetUserID(ctx, stripeInvoice.Customer.ID)
if err != nil {
return Error.Wrap(err)
}
if inactive, err := service.isUserInactive(ctx, userID); err != nil {
return Error.Wrap(err)
} else if inactive {
continue
}
if stripeInvoice.AutoAdvance {
continue
}
err := service.finalizeInvoice(ctx, stripeInvoice.ID)
err = service.finalizeInvoice(ctx, stripeInvoice.ID)
if err != nil {
return Error.Wrap(err)
}
@ -1063,6 +1118,16 @@ func (service *Service) PayInvoices(ctx context.Context, createdOnAfter time.Tim
func (service *Service) PayCustomerInvoices(ctx context.Context, customerID string) (err error) {
defer mon.Task()(&ctx)(&err)
userID, err := service.db.Customers().GetUserID(ctx, customerID)
if err != nil {
return Error.Wrap(err)
}
if inactive, err := service.isUserInactive(ctx, userID); err != nil {
return Error.Wrap(err)
} else if inactive {
return Error.New("customer %s is inactive", customerID)
}
customerInvoices, err := service.getInvoices(ctx, customerID, time.Unix(0, 0))
if err != nil {
return Error.New("error getting invoices for stripe customer %s", customerID)
@ -1171,6 +1236,15 @@ func (service *Service) payInvoicesWithTokenBalance(ctx context.Context, cusID s
return errGrp.Err()
}
// isUserInactive checks whether a user has a status of console.Deleted or console.PendingDeletion.
func (service *Service) isUserInactive(ctx context.Context, userID uuid.UUID) (bool, error) {
user, err := service.usersDB.Get(ctx, userID)
if err != nil {
return false, err
}
return user.Status == console.Deleted || user.Status == console.PendingDeletion, nil
}
// projectUsagePrice represents pricing for project usage.
type projectUsagePrice struct {
Storage decimal.Decimal

View File

@ -381,6 +381,42 @@ func TestService_BalanceInvoiceItems(t *testing.T) {
})
require.NoError(t, err)
require.Equal(t, int64(0), cus.Balance)
// Deactivate the users and give them balances
statusPending := console.PendingDeletion
statusDeleted := console.Deleted
for i, user := range users {
cusID, err = satellite.DB.StripeCoinPayments().Customers().GetCustomerID(ctx, user.ID)
require.NoError(t, err)
_, err = payments.StripeClient.Customers().Update(cusID, &stripe.CustomerParams{
Params: stripe.Params{
Context: ctx,
},
Balance: stripe.Int64(1000),
})
require.NoError(t, err)
var status *console.UserStatus
if i%2 == 0 {
status = &statusDeleted
} else {
status = &statusPending
}
err := satellite.DB.Console().Users().Update(ctx, user.ID, console.UpdateUserRequest{
Status: status,
})
require.NoError(t, err)
}
// try to convert the stripe balance into an invoice item
require.NoError(t, payments.StripeService.CreateBalanceInvoiceItems(ctx))
// check no invoice item was created since all users are deactivated
itr = payments.StripeClient.InvoiceItems().List(&stripe.InvoiceItemListParams{
Customer: stripe.String(cusID),
})
require.NoError(t, itr.Err())
require.False(t, itr.Next())
})
}
@ -400,6 +436,10 @@ func TestService_InvoiceElementsProcessing(t *testing.T) {
period := time.Date(time.Now().Year(), time.Now().Month()+1, 20, 0, 0, 0, 0, time.UTC)
numberOfProjects := 19
numberOfInactiveUsers := 5
pendingDeletionStatus := console.PendingDeletion
// user to be deactivated later
var activeUser console.User
// generate test data, each user has one project and some credits
for i := 0; i < numberOfProjects; i++ {
user, err := satellite.AddUser(ctx, console.CreateUser{
@ -414,6 +454,15 @@ func TestService_InvoiceElementsProcessing(t *testing.T) {
err = satellite.DB.Orders().UpdateBucketBandwidthSettle(ctx, project.ID, []byte("testbucket"),
pb.PieceAction_GET, int64(i+10)*memory.GiB.Int64(), 0, period)
require.NoError(t, err)
if i < numberOfProjects-numberOfInactiveUsers {
activeUser = *user
continue
}
err = satellite.DB.Console().Users().Update(ctx, user.ID, console.UpdateUserRequest{
Status: &pendingDeletionStatus,
})
require.NoError(t, err)
}
satellite.API.Payments.StripeService.SetNow(func() time.Time {
@ -425,10 +474,16 @@ func TestService_InvoiceElementsProcessing(t *testing.T) {
start := time.Date(period.Year(), period.Month(), 1, 0, 0, 0, 0, time.UTC)
end := time.Date(period.Year(), period.Month()+1, 1, 0, 0, 0, 0, time.UTC)
// check if we have project record for each project
// check if we have project record for each project, except for inactive users
recordsPage, err := satellite.DB.StripeCoinPayments().ProjectRecords().ListUnapplied(ctx, uuid.UUID{}, 40, start, end)
require.NoError(t, err)
require.Equal(t, numberOfProjects, len(recordsPage.Records))
require.Equal(t, numberOfProjects-numberOfInactiveUsers, len(recordsPage.Records))
// deactivate user
err = satellite.DB.Console().Users().Update(ctx, activeUser.ID, console.UpdateUserRequest{
Status: &pendingDeletionStatus,
})
require.NoError(t, err)
err = satellite.API.Payments.StripeService.InvoiceApplyProjectRecords(ctx, period)
require.NoError(t, err)
@ -436,7 +491,8 @@ func TestService_InvoiceElementsProcessing(t *testing.T) {
// verify that we applied all unapplied project records
recordsPage, err = satellite.DB.StripeCoinPayments().ProjectRecords().ListUnapplied(ctx, uuid.UUID{}, 40, start, end)
require.NoError(t, err)
require.Equal(t, 0, len(recordsPage.Records))
// the 1 remaining record is for the now inactive user
require.Equal(t, 1, len(recordsPage.Records))
})
}
@ -515,9 +571,116 @@ func TestService_InvoiceUserWithManyProjects(t *testing.T) {
err = payments.StripeService.InvoiceApplyProjectRecords(ctx, period)
require.NoError(t, err)
// deactivate user
pendingDeletionStatus := console.PendingDeletion
activeStatus := console.Active
err = satellite.DB.Console().Users().Update(ctx, user.ID, console.UpdateUserRequest{
Status: &pendingDeletionStatus,
})
require.NoError(t, err)
err = payments.StripeService.CreateInvoices(ctx, period)
require.NoError(t, err)
// invoice wasn't created because user is deactivated
itr := payments.StripeClient.Invoices().List(&stripe.InvoiceListParams{})
require.False(t, itr.Next())
require.NoError(t, itr.Err())
err = satellite.DB.Console().Users().Update(ctx, user.ID, console.UpdateUserRequest{
Status: &activeStatus,
})
require.NoError(t, err)
err = payments.StripeService.CreateInvoices(ctx, period)
require.NoError(t, err)
// invoice was created because user is active
itr = payments.StripeClient.Invoices().List(&stripe.InvoiceListParams{})
require.True(t, itr.Next())
require.NoError(t, itr.Err())
})
}
func TestService_FinalizeInvoices(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
satellite := planet.Satellites[0]
stripeClient := satellite.API.Payments.StripeClient
user, err := satellite.AddUser(ctx, console.CreateUser{
FullName: "testuser",
Email: "user@test",
}, 1)
require.NoError(t, err)
customer, err := satellite.DB.StripeCoinPayments().Customers().GetCustomerID(ctx, user.ID)
require.NoError(t, err)
// create invoice item
invItem, err := satellite.API.Payments.StripeClient.InvoiceItems().New(&stripe.InvoiceItemParams{
Params: stripe.Params{Context: ctx},
Amount: stripe.Int64(1000),
Currency: stripe.String(string(stripe.CurrencyUSD)),
Customer: &customer,
})
require.NoError(t, err)
InvItems := make([]*stripe.InvoiceUpcomingInvoiceItemParams, 0, 1)
InvItems = append(InvItems, &stripe.InvoiceUpcomingInvoiceItemParams{
InvoiceItem: &invItem.ID,
Amount: &invItem.Amount,
Currency: stripe.String(string(stripe.CurrencyUSD)),
})
// create invoice
_, err = satellite.API.Payments.StripeClient.Invoices().New(&stripe.InvoiceParams{
Params: stripe.Params{Context: ctx},
Customer: &customer,
InvoiceItems: InvItems,
})
require.NoError(t, err)
itr := stripeClient.Invoices().List(&stripe.InvoiceListParams{
Customer: &customer,
})
require.True(t, itr.Next())
require.NoError(t, itr.Err())
require.Equal(t, stripe.InvoiceStatusDraft, itr.Invoice().Status)
// deactivate user
pendingDeletionStatus := console.PendingDeletion
err = satellite.DB.Console().Users().Update(ctx, user.ID, console.UpdateUserRequest{
Status: &pendingDeletionStatus,
})
require.NoError(t, err)
err = satellite.API.Payments.StripeService.FinalizeInvoices(ctx)
require.NoError(t, err)
itr = stripeClient.Invoices().List(&stripe.InvoiceListParams{
Customer: &customer,
})
require.True(t, itr.Next())
require.NoError(t, itr.Err())
// finalizing did not work because user is deactivated
require.Equal(t, stripe.InvoiceStatusDraft, itr.Invoice().Status)
activeStatus := console.Active
err = satellite.DB.Console().Users().Update(ctx, user.ID, console.UpdateUserRequest{
Status: &activeStatus,
})
require.NoError(t, err)
err = satellite.API.Payments.StripeService.FinalizeInvoices(ctx)
require.NoError(t, err)
itr = stripeClient.Invoices().List(&stripe.InvoiceListParams{
Customer: &customer,
})
require.True(t, itr.Next())
require.NoError(t, itr.Err())
require.Equal(t, stripe.InvoiceStatusOpen, itr.Invoice().Status)
})
}
@ -1074,6 +1237,29 @@ func TestService_PayMultipleInvoiceForCustomer(t *testing.T) {
require.NoError(t, err)
require.False(t, balance.IsNegative())
require.Zero(t, balance.BaseUnits())
// create another invoice
_, err = satellite.API.Payments.StripeClient.Invoices().New(&stripe.InvoiceParams{
Params: stripe.Params{Context: ctx},
Customer: &customer,
InvoiceItems: Inv2Items,
DefaultPaymentMethod: stripe.String(stripe1.MockInvoicesPaySuccess),
})
require.NoError(t, err)
err = satellite.API.Payments.StripeService.FinalizeInvoices(ctx)
require.NoError(t, err)
// deactivate user
status := console.PendingDeletion
err = satellite.DB.Console().Users().Update(ctx, user.ID, console.UpdateUserRequest{
Status: &status,
})
require.NoError(t, err)
// attempt to pay user invoices should not succeed since the user is now deactivated.
err = satellite.API.Payments.StripeService.PayCustomerInvoices(ctx, customer)
require.Error(t, err)
})
}