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:
parent
a14a18185b
commit
46ee1c1414
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user