From 0f538093af4bea68efbc3660da755869948f7465 Mon Sep 17 00:00:00 2001 From: Wilfred Asomani Date: Thu, 19 Oct 2023 15:57:01 +0000 Subject: [PATCH] satellite/{accountfreeze,console}: use days till escalation values This change updates account freeze to set and use the days till escalation column of the account freezes table. Issue: #6382 Change-Id: I345798e3d53e5ab4a7653723433fb8affa258212 --- satellite/admin.go | 1 + satellite/api.go | 8 +++- satellite/console/accountfreezes.go | 41 +++++++++++++++--- satellite/console/accountfreezes_test.go | 43 ++++++++++++++++--- satellite/console/config.go | 1 + satellite/core.go | 3 +- satellite/payments/accountfreeze/chore.go | 33 ++++++++------ .../payments/accountfreeze/chore_test.go | 20 ++++++--- satellite/payments/billing/chore_test.go | 2 +- scripts/testdata/satellite-config.yaml.lock | 12 +++--- 10 files changed, 125 insertions(+), 39 deletions(-) diff --git a/satellite/admin.go b/satellite/admin.go index 966f57968..7f6d90b25 100644 --- a/satellite/admin.go +++ b/satellite/admin.go @@ -182,6 +182,7 @@ func NewAdmin(log *zap.Logger, full *identity.FullIdentity, db DB, metabaseDB *m db.Console().Users(), db.Console().Projects(), peer.Analytics.Service, + config.Console.AccountFreeze, ) peer.Payments.Service, err = stripe.NewService( diff --git a/satellite/api.go b/satellite/api.go index c16aa0e12..0716d2ce7 100644 --- a/satellite/api.go +++ b/satellite/api.go @@ -596,7 +596,13 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB, return nil, errs.Combine(err, peer.Close()) } - accountFreezeService := console.NewAccountFreezeService(db.Console().AccountFreezeEvents(), db.Console().Users(), db.Console().Projects(), peer.Analytics.Service) + accountFreezeService := console.NewAccountFreezeService( + db.Console().AccountFreezeEvents(), + db.Console().Users(), + db.Console().Projects(), + peer.Analytics.Service, + consoleConfig.AccountFreeze, + ) peer.Console.Endpoint = consoleweb.NewServer( peer.Log.Named("console:endpoint"), diff --git a/satellite/console/accountfreezes.go b/satellite/console/accountfreezes.go index 1619881e9..35c31201a 100644 --- a/satellite/console/accountfreezes.go +++ b/satellite/console/accountfreezes.go @@ -104,21 +104,29 @@ func (et AccountFreezeEventType) String() string { } } +// AccountFreezeConfig contains configurable values for account freeze service. +type AccountFreezeConfig struct { + BillingWarnGracePeriod time.Duration `help:"How long to wait between a billing warning event and billing freezing an account." default:"360h"` + BillingFreezeGracePeriod time.Duration `help:"How long to wait between a billing freeze event and setting pending deletion account status." default:"1440h"` +} + // AccountFreezeService encapsulates operations concerning account freezes. type AccountFreezeService struct { freezeEventsDB AccountFreezeEvents usersDB Users projectsDB Projects tracker analytics.FreezeTracker + config AccountFreezeConfig } // NewAccountFreezeService creates a new account freeze service. -func NewAccountFreezeService(freezeEventsDB AccountFreezeEvents, usersDB Users, projectsDB Projects, tracker analytics.FreezeTracker) *AccountFreezeService { +func NewAccountFreezeService(freezeEventsDB AccountFreezeEvents, usersDB Users, projectsDB Projects, tracker analytics.FreezeTracker, config AccountFreezeConfig) *AccountFreezeService { return &AccountFreezeService{ freezeEventsDB: freezeEventsDB, usersDB: usersDB, projectsDB: projectsDB, tracker: tracker, + config: config, } } @@ -176,11 +184,13 @@ func (s *AccountFreezeService) BillingFreezeUser(ctx context.Context, userID uui Segment: user.ProjectSegmentLimit, } + daysTillEscalation := int(s.config.BillingFreezeGracePeriod.Hours() / 24) billingFreeze := freezes.BillingFreeze if billingFreeze == nil { billingFreeze = &AccountFreezeEvent{ - UserID: userID, - Type: BillingFreeze, + UserID: userID, + Type: BillingFreeze, + DaysTillEscalation: &daysTillEscalation, Limits: &AccountFreezeEventLimits{ User: userLimits, Projects: make(map[uuid.UUID]UsageLimits), @@ -313,9 +323,11 @@ func (s *AccountFreezeService) BillingWarnUser(ctx context.Context, userID uuid. return nil } + daysTillEscalation := int(s.config.BillingWarnGracePeriod.Hours() / 24) _, err = s.freezeEventsDB.Upsert(ctx, &AccountFreezeEvent{ - UserID: userID, - Type: BillingWarning, + UserID: userID, + Type: BillingWarning, + DaysTillEscalation: &daysTillEscalation, }) if err != nil { return ErrAccountFreeze.Wrap(err) @@ -521,6 +533,25 @@ func (s *AccountFreezeService) GetAllEvents(ctx context.Context, cursor FreezeEv return events, nil } +// EscalateBillingFreeze deactivates escalation for this freeze event and sets the user status to pending deletion. +func (s *AccountFreezeService) EscalateBillingFreeze(ctx context.Context, userID uuid.UUID, event AccountFreezeEvent) error { + event.DaysTillEscalation = nil + _, err := s.freezeEventsDB.Upsert(ctx, &event) + if err != nil { + return ErrAccountFreeze.Wrap(err) + } + + status := PendingDeletion + err = s.usersDB.Update(ctx, userID, UpdateUserRequest{ + Status: &status, + }) + if err != nil { + return ErrAccountFreeze.Wrap(err) + } + + return nil +} + // TestChangeFreezeTracker changes the freeze tracker service for tests. func (s *AccountFreezeService) TestChangeFreezeTracker(t analytics.FreezeTracker) { s.tracker = t diff --git a/satellite/console/accountfreezes_test.go b/satellite/console/accountfreezes_test.go index 490aaddba..971c6e7d6 100644 --- a/satellite/console/accountfreezes_test.go +++ b/satellite/console/accountfreezes_test.go @@ -45,7 +45,9 @@ func TestAccountBillingFreeze(t *testing.T) { sat := planet.Satellites[0] usersDB := sat.DB.Console().Users() projectsDB := sat.DB.Console().Projects() - service := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, sat.API.Analytics.Service) + service := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, sat.API.Analytics.Service, sat.Config.Console.AccountFreeze) + + billingFreezeGracePeriod := int(sat.Config.Console.AccountFreeze.BillingFreezeGracePeriod.Hours() / 24) userLimits := randUsageLimits() user, err := sat.AddUser(ctx, console.CreateUser{ @@ -82,6 +84,23 @@ func TestAccountBillingFreeze(t *testing.T) { frozen, err = service.IsUserBillingFrozen(ctx, user.ID) require.NoError(t, err) require.True(t, frozen) + + freezes, err := service.GetAll(ctx, user.ID) + require.NoError(t, err) + require.NotNil(t, freezes.BillingFreeze) + require.Equal(t, &billingFreezeGracePeriod, freezes.BillingFreeze.DaysTillEscalation) + + err = service.EscalateBillingFreeze(ctx, user.ID, *freezes.BillingFreeze) + require.NoError(t, err) + + freezes, err = service.GetAll(ctx, user.ID) + require.NoError(t, err) + require.NotNil(t, freezes.BillingFreeze) + require.Nil(t, freezes.BillingFreeze.DaysTillEscalation) + + user, err = usersDB.Get(ctx, user.ID) + require.NoError(t, err) + require.Equal(t, console.PendingDeletion, user.Status) }) } @@ -92,7 +111,7 @@ func TestAccountBillingUnFreeze(t *testing.T) { sat := planet.Satellites[0] usersDB := sat.DB.Console().Users() projectsDB := sat.DB.Console().Projects() - service := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, sat.API.Analytics.Service) + service := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, sat.API.Analytics.Service, sat.Config.Console.AccountFreeze) userLimits := randUsageLimits() user, err := sat.AddUser(ctx, console.CreateUser{ @@ -144,7 +163,7 @@ func TestAccountViolationFreeze(t *testing.T) { sat := planet.Satellites[0] usersDB := sat.DB.Console().Users() projectsDB := sat.DB.Console().Projects() - service := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, sat.API.Analytics.Service) + service := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, sat.API.Analytics.Service, sat.Config.Console.AccountFreeze) userLimits := randUsageLimits() user, err := sat.AddUser(ctx, console.CreateUser{ @@ -178,6 +197,10 @@ func TestAccountViolationFreeze(t *testing.T) { require.NoError(t, err) require.True(t, frozen) + user, err = usersDB.Get(ctx, user.ID) + require.NoError(t, err) + require.Equal(t, console.PendingDeletion, user.Status) + checkLimits(t) require.NoError(t, service.ViolationUnfreezeUser(ctx, user.ID)) @@ -206,6 +229,11 @@ func TestAccountViolationFreeze(t *testing.T) { require.NoError(t, err) require.True(t, frozen) + freezes, err := service.GetAll(ctx, user.ID) + require.NoError(t, err) + require.NotNil(t, freezes.ViolationFreeze) + require.Nil(t, freezes.ViolationFreeze.DaysTillEscalation) + checkLimits(t) }) } @@ -217,7 +245,9 @@ func TestRemoveAccountBillingWarning(t *testing.T) { sat := planet.Satellites[0] usersDB := sat.DB.Console().Users() projectsDB := sat.DB.Console().Projects() - service := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, sat.API.Analytics.Service) + service := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, sat.API.Analytics.Service, sat.Config.Console.AccountFreeze) + + billingWarnGracePeriod := int(sat.Config.Console.AccountFreeze.BillingWarnGracePeriod.Hours() / 24) user, err := sat.AddUser(ctx, console.CreateUser{ FullName: "Test User", @@ -240,6 +270,7 @@ func TestRemoveAccountBillingWarning(t *testing.T) { freezes, err = service.GetAll(ctx, user.ID) require.NoError(t, err) require.NotNil(t, freezes.BillingWarning) + require.Equal(t, &billingWarnGracePeriod, freezes.BillingWarning.DaysTillEscalation) require.Nil(t, freezes.BillingFreeze) require.Nil(t, freezes.ViolationFreeze) require.NoError(t, service.BillingFreezeUser(ctx, user.ID)) @@ -276,7 +307,7 @@ func TestAccountFreezeAlreadyFrozen(t *testing.T) { sat := planet.Satellites[0] usersDB := sat.DB.Console().Users() projectsDB := sat.DB.Console().Projects() - service := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, sat.API.Analytics.Service) + service := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, sat.API.Analytics.Service, sat.Config.Console.AccountFreeze) userLimits := randUsageLimits() user, err := sat.AddUser(ctx, console.CreateUser{ @@ -382,7 +413,7 @@ func TestFreezeEffects(t *testing.T) { usersDB := sat.DB.Console().Users() projectsDB := sat.DB.Console().Projects() consoleService := sat.API.Console.Service - freezeService := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, sat.API.Analytics.Service) + freezeService := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, sat.API.Analytics.Service, sat.Config.Console.AccountFreeze) uplink1 := planet.Uplinks[0] user1, _, err := consoleService.GetUserByEmailWithUnverified(ctx, uplink1.User[sat.ID()].Email) diff --git a/satellite/console/config.go b/satellite/console/config.go index 7d00a3e62..0f4c46522 100644 --- a/satellite/console/config.go +++ b/satellite/console/config.go @@ -30,6 +30,7 @@ type Config struct { UsageLimits UsageLimitsConfig Captcha CaptchaConfig Session SessionConfig + AccountFreeze AccountFreezeConfig } // CaptchaConfig contains configurations for login/registration captcha system. diff --git a/satellite/core.go b/satellite/core.go index 96fd07960..f45ecadf7 100644 --- a/satellite/core.go +++ b/satellite/core.go @@ -534,6 +534,7 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB, peer.DB.Console().Users(), peer.DB.Console().Projects(), peer.Analytics.Service, + config.Console.AccountFreeze, ), ), } @@ -563,7 +564,7 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB, peer.DB.Console().Users(), peer.DB.Wallets(), peer.DB.StorjscanPayments(), - console.NewAccountFreezeService(db.Console().AccountFreezeEvents(), db.Console().Users(), db.Console().Projects(), peer.Analytics.Service), + console.NewAccountFreezeService(db.Console().AccountFreezeEvents(), db.Console().Users(), db.Console().Projects(), peer.Analytics.Service, config.Console.AccountFreeze), peer.Analytics.Service, config.AccountFreeze, ) diff --git a/satellite/payments/accountfreeze/chore.go b/satellite/payments/accountfreeze/chore.go index c29c5a3af..b0bf41f5e 100644 --- a/satellite/payments/accountfreeze/chore.go +++ b/satellite/payments/accountfreeze/chore.go @@ -29,12 +29,10 @@ var ( // Config contains configurable values for account freeze chore. type Config struct { - Enabled bool `help:"whether to run this chore." default:"false"` - Interval time.Duration `help:"How often to run this chore, which is how often unpaid invoices are checked." default:"24h"` - BillingWarnGracePeriod time.Duration `help:"How long to wait between a billing warning event and billing freezing an account." default:"360h"` - BillingFreezeGracePeriod time.Duration `help:"How long to wait between a billing freeze event and setting pending deletion account status." default:"1440h"` - PriceThreshold int64 `help:"The failed invoice amount (in cents) beyond which an account will not be frozen" default:"100000"` - ExcludeStorjscan bool `help:"whether to exclude storjscan-paying users from automatic warn/freeze" default:"false"` + Enabled bool `help:"whether to run this chore." default:"false"` + Interval time.Duration `help:"How often to run this chore, which is how often unpaid invoices are checked." default:"24h"` + PriceThreshold int64 `help:"The failed invoice amount (in cents) beyond which an account will not be frozen" default:"100000"` + ExcludeStorjscan bool `help:"whether to exclude storjscan-paying users from automatic warn/freeze" default:"false"` } // Chore is a chore that checks for unpaid invoices and potentially freezes corresponding accounts. @@ -194,8 +192,19 @@ func (chore *Chore) attemptBillingFreezeWarn(ctx context.Context) { continue } + shouldEscalate := func(event *console.AccountFreezeEvent) bool { + if event == nil || event.DaysTillEscalation == nil { + return false + } + daysElapsed := int(chore.nowFn().Sub(event.CreatedAt).Hours() / 24) + return daysElapsed > *event.DaysTillEscalation + } + if freezes.BillingFreeze != nil { - if chore.nowFn().Sub(freezes.BillingFreeze.CreatedAt) > chore.config.BillingFreezeGracePeriod { + if freezes.BillingFreeze.DaysTillEscalation == nil { + continue + } + if shouldEscalate(freezes.BillingFreeze) { if user.Status == console.PendingDeletion { infoLog("Ignoring invoice; account already marked for deletion") continue @@ -212,10 +221,7 @@ func (chore *Chore) attemptBillingFreezeWarn(ctx context.Context) { continue } - status := console.PendingDeletion - err = chore.usersDB.Update(ctx, userID, console.UpdateUserRequest{ - Status: &status, - }) + err = chore.freezeService.EscalateBillingFreeze(ctx, userID, *freezes.BillingFreeze) if err != nil { errorLog("Could not mark account for deletion", err) continue @@ -250,7 +256,10 @@ func (chore *Chore) attemptBillingFreezeWarn(ctx context.Context) { continue } - if chore.nowFn().Sub(freezes.BillingWarning.CreatedAt) > chore.config.BillingWarnGracePeriod { + if freezes.BillingWarning.DaysTillEscalation == nil { + continue + } + if shouldEscalate(freezes.BillingWarning) { // check if the invoice has been paid by the time the chore gets here. isPaid, err := checkInvPaid(invoice.ID) if err != nil { diff --git a/satellite/payments/accountfreeze/chore_test.go b/satellite/payments/accountfreeze/chore_test.go index 60ee64067..4f7aef7f0 100644 --- a/satellite/payments/accountfreeze/chore_test.go +++ b/satellite/payments/accountfreeze/chore_test.go @@ -40,7 +40,7 @@ func TestAutoFreezeChore(t *testing.T) { customerDB := sat.Core.DB.StripeCoinPayments().Customers() usersDB := sat.DB.Console().Users() projectsDB := sat.DB.Console().Projects() - service := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, newFreezeTrackerMock(t)) + service := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, newFreezeTrackerMock(t), sat.Config.Console.AccountFreeze) chore := sat.Core.Payments.AccountFreeze chore.Loop.Pause() @@ -112,7 +112,7 @@ func TestAutoFreezeChore(t *testing.T) { // forward date to after the grace period chore.TestSetNow(func() time.Time { - return time.Now().AddDate(0, 0, 50) + return time.Now().Add(sat.Config.Console.AccountFreeze.BillingWarnGracePeriod).Add(24 * time.Hour) }) chore.Loop.TriggerWait() @@ -184,7 +184,7 @@ func TestAutoFreezeChore(t *testing.T) { // forward date to after the grace period chore.TestSetNow(func() time.Time { - return time.Now().AddDate(0, 0, 50) + return time.Now().Add(sat.Config.Console.AccountFreeze.BillingWarnGracePeriod).Add(24 * time.Hour) }) chore.Loop.TriggerWait() @@ -241,7 +241,7 @@ func TestAutoFreezeChore(t *testing.T) { chore.TestSetNow(func() time.Time { // current date is now after billing warn grace period - return time.Now().AddDate(0, 0, 50) + return time.Now().Add(sat.Config.Console.AccountFreeze.BillingWarnGracePeriod).Add(24 * time.Hour) }) chore.Loop.TriggerWait() @@ -252,7 +252,7 @@ func TestAutoFreezeChore(t *testing.T) { chore.TestSetNow(func() time.Time { // current date is now after billing freeze grace period - return time.Now().AddDate(0, 0, 70) + return time.Now().Add(sat.Config.Console.AccountFreeze.BillingFreezeGracePeriod).Add(24 * time.Hour) }) chore.Loop.TriggerWait() @@ -262,6 +262,12 @@ func TestAutoFreezeChore(t *testing.T) { require.NoError(t, err) require.Equal(t, console.PendingDeletion, userPD.Status) + freezes, err = service.GetAll(ctx, user.ID) + require.NoError(t, err) + require.NotNil(t, freezes.BillingFreeze) + // the billing freeze event should have escalation disabled. + require.Nil(t, freezes.BillingFreeze.DaysTillEscalation) + // Pay invoice so user qualifies to be removed from billing freeze. inv, err = stripeClient.Invoices().Pay(inv.ID, &stripe.InvoicePayParams{ Params: stripe.Params{Context: ctx}, @@ -338,7 +344,7 @@ func TestAutoFreezeChore(t *testing.T) { chore.TestSetNow(func() time.Time { // current date is now after billing warn grace period - return time.Now().AddDate(0, 0, 50) + return time.Now().Add(sat.Config.Console.AccountFreeze.BillingWarnGracePeriod).Add(24 * time.Hour) }) chore.Loop.TriggerWait() @@ -455,7 +461,7 @@ func TestAutoFreezeChore_StorjscanExclusion(t *testing.T) { customerDB := sat.Core.DB.StripeCoinPayments().Customers() usersDB := sat.DB.Console().Users() projectsDB := sat.DB.Console().Projects() - service := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, newFreezeTrackerMock(t)) + service := console.NewAccountFreezeService(sat.DB.Console().AccountFreezeEvents(), usersDB, projectsDB, newFreezeTrackerMock(t), sat.Config.Console.AccountFreeze) chore := sat.Core.Payments.AccountFreeze chore.Loop.Pause() diff --git a/satellite/payments/billing/chore_test.go b/satellite/payments/billing/chore_test.go index d5e8936c7..2cc39a64e 100644 --- a/satellite/payments/billing/chore_test.go +++ b/satellite/payments/billing/chore_test.go @@ -272,7 +272,7 @@ func TestChore_PayInvoiceObserver(t *testing.T) { err = sat.DB.Wallets().Add(ctx, userID, address) require.NoError(t, err) - freezeService := console.NewAccountFreezeService(consoleDB.AccountFreezeEvents(), consoleDB.Users(), consoleDB.Projects(), sat.Core.Analytics.Service) + freezeService := console.NewAccountFreezeService(consoleDB.AccountFreezeEvents(), consoleDB.Users(), consoleDB.Projects(), sat.Core.Analytics.Service, sat.Config.Console.AccountFreeze) choreObservers := billing.ChoreObservers{ UpgradeUser: console.NewUpgradeUserObserver(consoleDB, db.Billing(), sat.Config.Console.UsageLimits, sat.Config.Console.UserBalanceForUpgrade), diff --git a/scripts/testdata/satellite-config.yaml.lock b/scripts/testdata/satellite-config.yaml.lock index 4fedc7e35..f62eb0c23 100755 --- a/scripts/testdata/satellite-config.yaml.lock +++ b/scripts/testdata/satellite-config.yaml.lock @@ -1,9 +1,3 @@ -# How long to wait between a billing freeze event and setting pending deletion account status. -# account-freeze.billing-freeze-grace-period: 1440h0m0s - -# How long to wait between a billing warning event and billing freezing an account. -# account-freeze.billing-warn-grace-period: 360h0m0s - # whether to run this chore. # account-freeze.enabled: false @@ -178,6 +172,12 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0 # url link for account activation redirect # console.account-activation-redirect-url: "" +# How long to wait between a billing freeze event and setting pending deletion account status. +# console.account-freeze.billing-freeze-grace-period: 1440h0m0s + +# How long to wait between a billing warning event and billing freezing an account. +# console.account-freeze.billing-warn-grace-period: 360h0m0s + # server address of the http api gateway and frontend app # console.address: :10100