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
This commit is contained in:
Wilfred Asomani 2023-10-19 15:57:01 +00:00 committed by Storj Robot
parent e469ee6cce
commit 0f538093af
10 changed files with 125 additions and 39 deletions

View File

@ -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(

View File

@ -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"),

View File

@ -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

View File

@ -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)

View File

@ -30,6 +30,7 @@ type Config struct {
UsageLimits UsageLimitsConfig
Captcha CaptchaConfig
Session SessionConfig
AccountFreeze AccountFreezeConfig
}
// CaptchaConfig contains configurations for login/registration captcha system.

View File

@ -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,
)

View File

@ -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 {

View File

@ -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()

View File

@ -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),

View File

@ -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