satellite/{db,console,payments}: unfreeze user with no failed invoices

This change extends the autofreeze chore to go through users who have
been warned/frozen to check if they have no failed invoices. If they do
not, this extension unwarns/unfreezes them.

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

Change-Id: I570b1d4b2e29574bd8b9ae37eb2d4fb41d178336
This commit is contained in:
Wilfred Asomani 2023-07-25 14:08:57 +00:00 committed by Storj Robot
parent e78658d174
commit 7c65c0cea5
7 changed files with 437 additions and 186 deletions

View File

@ -26,6 +26,8 @@ type AccountFreezeEvents interface {
Upsert(ctx context.Context, event *AccountFreezeEvent) (*AccountFreezeEvent, error)
// Get is a method for querying account freeze event from the database by user ID and event type.
Get(ctx context.Context, userID uuid.UUID, eventType AccountFreezeEventType) (*AccountFreezeEvent, error)
// GetAllEvents is a method for querying all account freeze events from the database.
GetAllEvents(ctx context.Context, cursor FreezeEventsCursor) (events *FreezeEventsPage, err error)
// GetAll is a method for querying all account freeze events from the database by user ID.
GetAll(ctx context.Context, userID uuid.UUID) (freeze *AccountFreezeEvent, warning *AccountFreezeEvent, err error)
// DeleteAllByUserID is a method for deleting all account freeze events from the database by user ID.
@ -48,6 +50,23 @@ type AccountFreezeEventLimits struct {
Projects map[uuid.UUID]UsageLimits `json:"projects"`
}
// FreezeEventsCursor holds info for freeze events
// cursor pagination.
type FreezeEventsCursor struct {
Limit int
// StartingAfter is the last user ID of the previous page.
// The next page will start after this user ID.
StartingAfter *uuid.UUID
}
// FreezeEventsPage returns paginated freeze events.
type FreezeEventsPage struct {
Events []AccountFreezeEvent
// Next indicates whether there are more events to retrieve.
Next bool
}
// AccountFreezeEventType is used to indicate the account freeze event's type.
type AccountFreezeEventType int
@ -272,6 +291,18 @@ func (s *AccountFreezeService) GetAll(ctx context.Context, userID uuid.UUID) (fr
return freeze, warning, nil
}
// GetAllEvents returns all events.
func (s *AccountFreezeService) GetAllEvents(ctx context.Context, cursor FreezeEventsCursor) (events *FreezeEventsPage, err error) {
defer mon.Task()(&ctx)(&err)
events, err = s.freezeEventsDB.GetAllEvents(ctx, cursor)
if err != nil {
return nil, ErrAccountFreeze.Wrap(err)
}
return events, nil
}
// TestChangeFreezeTracker changes the freeze tracker service for tests.
func (s *AccountFreezeService) TestChangeFreezeTracker(t analytics.FreezeTracker) {
s.tracker = t

View File

@ -73,187 +73,255 @@ func (chore *Chore) Run(ctx context.Context) (err error) {
defer mon.Task()(&ctx)(&err)
return chore.Loop.Run(ctx, func(ctx context.Context) (err error) {
invoices, err := chore.payments.Invoices().ListFailed(ctx)
if err != nil {
chore.log.Error("Could not list invoices", zap.Error(Error.Wrap(err)))
return nil
}
chore.log.Debug("failed invoices found", zap.Int("count", len(invoices)))
chore.attemptFreezeWarn(ctx)
userMap := make(map[uuid.UUID]struct{})
frozenMap := make(map[uuid.UUID]struct{})
warnedMap := make(map[uuid.UUID]struct{})
bypassedLargeMap := make(map[uuid.UUID]struct{})
bypassedTokenMap := make(map[uuid.UUID]struct{})
checkInvPaid := func(invID string) (bool, error) {
inv, err := chore.payments.Invoices().Get(ctx, invID)
if err != nil {
return false, err
}
return inv.Status == payments.InvoiceStatusPaid, nil
}
for _, invoice := range invoices {
userID, err := chore.accounts.Customers().GetUserID(ctx, invoice.CustomerID)
if err != nil {
chore.log.Error("Could not get userID",
zap.String("invoiceID", invoice.ID),
zap.String("customerID", invoice.CustomerID),
zap.Error(Error.Wrap(err)),
)
continue
}
debugLog := func(message string) {
chore.log.Debug(message,
zap.String("invoiceID", invoice.ID),
zap.String("customerID", invoice.CustomerID),
zap.Any("userID", userID),
)
}
errorLog := func(message string, err error) {
chore.log.Error(message,
zap.String("invoiceID", invoice.ID),
zap.String("customerID", invoice.CustomerID),
zap.Any("userID", userID),
zap.Error(Error.Wrap(err)),
)
}
userMap[userID] = struct{}{}
user, err := chore.usersDB.Get(ctx, userID)
if err != nil {
errorLog("Could not get user", err)
continue
}
if invoice.Amount > chore.config.PriceThreshold {
if _, ok := bypassedLargeMap[userID]; ok {
continue
}
bypassedLargeMap[userID] = struct{}{}
debugLog("Ignoring invoice; amount exceeds threshold")
chore.analytics.TrackLargeUnpaidInvoice(invoice.ID, userID, user.Email)
continue
}
if chore.config.ExcludeStorjscan {
if _, ok := bypassedTokenMap[userID]; ok {
continue
}
wallet, err := chore.walletsDB.GetWallet(ctx, user.ID)
if err != nil && !errs.Is(err, billing.ErrNoWallet) {
errorLog("Could not get wallets for user", err)
continue
}
// if there is no error, the user has a wallet and we can check for transactions
if err == nil {
cachedPayments, err := chore.paymentsDB.ListWallet(ctx, wallet, 1, 0)
if err != nil && !errs.Is(err, billing.ErrNoTransactions) {
errorLog("Could not get payments for user", err)
continue
}
if len(cachedPayments) > 0 {
bypassedTokenMap[userID] = struct{}{}
debugLog("Ignoring invoice; TX exists in storjscan")
chore.analytics.TrackStorjscanUnpaidInvoice(invoice.ID, userID, user.Email)
continue
}
}
}
freeze, warning, err := chore.freezeService.GetAll(ctx, userID)
if err != nil {
errorLog("Could not get freeze status", err)
continue
}
// try to pay the invoice before freezing/warning.
err = chore.payments.Invoices().AttemptPayOverdueInvoices(ctx, userID)
if err == nil {
debugLog("Ignoring invoice; Payment attempt successful")
if warning != nil {
err = chore.freezeService.UnWarnUser(ctx, userID)
if err != nil {
errorLog("Could not remove warning event", err)
}
}
if freeze != nil {
err = chore.freezeService.UnfreezeUser(ctx, userID)
if err != nil {
errorLog("Could not remove freeze event", err)
}
}
continue
} else {
errorLog("Could not attempt payment", err)
}
if freeze != nil {
debugLog("Ignoring invoice; account already frozen")
continue
}
if warning == nil {
// check if the invoice has been paid by the time the chore gets here.
isPaid, err := checkInvPaid(invoice.ID)
if err != nil {
errorLog("Could not verify invoice status", err)
continue
}
if isPaid {
debugLog("Ignoring invoice; payment already made")
continue
}
err = chore.freezeService.WarnUser(ctx, userID)
if err != nil {
errorLog("Could not add warning event", err)
continue
}
debugLog("user warned")
warnedMap[userID] = struct{}{}
continue
}
if chore.nowFn().Sub(warning.CreatedAt) > chore.config.GracePeriod {
// check if the invoice has been paid by the time the chore gets here.
isPaid, err := checkInvPaid(invoice.ID)
if err != nil {
errorLog("Could not verify invoice status", err)
continue
}
if isPaid {
debugLog("Ignoring invoice; payment already made")
continue
}
err = chore.freezeService.FreezeUser(ctx, userID)
if err != nil {
errorLog("Could not freeze account", err)
continue
}
debugLog("user frozen")
frozenMap[userID] = struct{}{}
}
}
chore.log.Debug("chore executed",
zap.Int("total invoices", len(invoices)),
zap.Int("user total", len(userMap)),
zap.Int("total warned", len(warnedMap)),
zap.Int("total frozen", len(frozenMap)),
zap.Int("total bypassed due to size of invoice", len(bypassedLargeMap)),
zap.Int("total bypassed due to storjscan payments", len(bypassedTokenMap)),
)
chore.attemptUnfreezeUnwarn(ctx)
return nil
})
}
func (chore *Chore) attemptFreezeWarn(ctx context.Context) {
invoices, err := chore.payments.Invoices().ListFailed(ctx, nil)
if err != nil {
chore.log.Error("Could not list invoices", zap.Error(Error.Wrap(err)))
return
}
chore.log.Debug("failed invoices found", zap.Int("count", len(invoices)))
userMap := make(map[uuid.UUID]struct{})
frozenMap := make(map[uuid.UUID]struct{})
warnedMap := make(map[uuid.UUID]struct{})
bypassedLargeMap := make(map[uuid.UUID]struct{})
bypassedTokenMap := make(map[uuid.UUID]struct{})
checkInvPaid := func(invID string) (bool, error) {
inv, err := chore.payments.Invoices().Get(ctx, invID)
if err != nil {
return false, err
}
return inv.Status == payments.InvoiceStatusPaid, nil
}
for _, invoice := range invoices {
userID, err := chore.accounts.Customers().GetUserID(ctx, invoice.CustomerID)
if err != nil {
chore.log.Error("Could not get userID",
zap.String("invoiceID", invoice.ID),
zap.String("customerID", invoice.CustomerID),
zap.Error(Error.Wrap(err)),
)
continue
}
debugLog := func(message string) {
chore.log.Debug(message,
zap.String("invoiceID", invoice.ID),
zap.String("customerID", invoice.CustomerID),
zap.Any("userID", userID),
)
}
errorLog := func(message string, err error) {
chore.log.Error(message,
zap.String("invoiceID", invoice.ID),
zap.String("customerID", invoice.CustomerID),
zap.Any("userID", userID),
zap.Error(Error.Wrap(err)),
)
}
userMap[userID] = struct{}{}
user, err := chore.usersDB.Get(ctx, userID)
if err != nil {
errorLog("Could not get user", err)
continue
}
if invoice.Amount > chore.config.PriceThreshold {
if _, ok := bypassedLargeMap[userID]; ok {
continue
}
bypassedLargeMap[userID] = struct{}{}
debugLog("Ignoring invoice; amount exceeds threshold")
chore.analytics.TrackLargeUnpaidInvoice(invoice.ID, userID, user.Email)
continue
}
if chore.config.ExcludeStorjscan {
if _, ok := bypassedTokenMap[userID]; ok {
continue
}
wallet, err := chore.walletsDB.GetWallet(ctx, user.ID)
if err != nil && !errs.Is(err, billing.ErrNoWallet) {
errorLog("Could not get wallets for user", err)
continue
}
// if there is no error, the user has a wallet and we can check for transactions
if err == nil {
cachedPayments, err := chore.paymentsDB.ListWallet(ctx, wallet, 1, 0)
if err != nil && !errs.Is(err, billing.ErrNoTransactions) {
errorLog("Could not get payments for user", err)
continue
}
if len(cachedPayments) > 0 {
bypassedTokenMap[userID] = struct{}{}
debugLog("Ignoring invoice; TX exists in storjscan")
chore.analytics.TrackStorjscanUnpaidInvoice(invoice.ID, userID, user.Email)
continue
}
}
}
freeze, warning, err := chore.freezeService.GetAll(ctx, userID)
if err != nil {
errorLog("Could not get freeze status", err)
continue
}
// try to pay the invoice before freezing/warning.
err = chore.payments.Invoices().AttemptPayOverdueInvoices(ctx, userID)
if err == nil {
debugLog("Ignoring invoice; Payment attempt successful")
if warning != nil {
err = chore.freezeService.UnWarnUser(ctx, userID)
if err != nil {
errorLog("Could not remove warning event", err)
}
}
if freeze != nil {
err = chore.freezeService.UnfreezeUser(ctx, userID)
if err != nil {
errorLog("Could not remove freeze event", err)
}
}
continue
} else {
errorLog("Could not attempt payment", err)
}
if freeze != nil {
debugLog("Ignoring invoice; account already frozen")
continue
}
if warning == nil {
// check if the invoice has been paid by the time the chore gets here.
isPaid, err := checkInvPaid(invoice.ID)
if err != nil {
errorLog("Could not verify invoice status", err)
continue
}
if isPaid {
debugLog("Ignoring invoice; payment already made")
continue
}
err = chore.freezeService.WarnUser(ctx, userID)
if err != nil {
errorLog("Could not add warning event", err)
continue
}
debugLog("user warned")
warnedMap[userID] = struct{}{}
continue
}
if chore.nowFn().Sub(warning.CreatedAt) > chore.config.GracePeriod {
// check if the invoice has been paid by the time the chore gets here.
isPaid, err := checkInvPaid(invoice.ID)
if err != nil {
errorLog("Could not verify invoice status", err)
continue
}
if isPaid {
debugLog("Ignoring invoice; payment already made")
continue
}
err = chore.freezeService.FreezeUser(ctx, userID)
if err != nil {
errorLog("Could not freeze account", err)
continue
}
debugLog("user frozen")
frozenMap[userID] = struct{}{}
}
}
chore.log.Debug("freezing/warning executed",
zap.Int("total invoices", len(invoices)),
zap.Int("user total", len(userMap)),
zap.Int("total warned", len(warnedMap)),
zap.Int("total frozen", len(frozenMap)),
zap.Int("total bypassed due to size of invoice", len(bypassedLargeMap)),
zap.Int("total bypassed due to storjscan payments", len(bypassedTokenMap)),
)
}
func (chore *Chore) attemptUnfreezeUnwarn(ctx context.Context) {
cursor := console.FreezeEventsCursor{
Limit: 100,
}
hasNext := true
usersCount := 0
unwarnedCount := 0
unfrozenCount := 0
getEvents := func(c console.FreezeEventsCursor) (events *console.FreezeEventsPage, err error) {
events, err = chore.freezeService.GetAllEvents(ctx, c)
if err != nil {
return nil, err
}
return events, err
}
for hasNext {
events, err := getEvents(cursor)
if err != nil {
return
}
for _, event := range events.Events {
usersCount++
invoices, err := chore.payments.Invoices().ListFailed(ctx, &event.UserID)
if err != nil {
chore.log.Error("Could not get failed invoices for user", zap.Error(Error.Wrap(err)))
continue
}
if len(invoices) > 0 {
continue
}
if event.Type == console.Freeze {
err = chore.freezeService.UnfreezeUser(ctx, event.UserID)
if err != nil {
chore.log.Error("Could not unfreeze user", zap.Error(Error.Wrap(err)))
}
unfrozenCount++
} else {
err = chore.freezeService.UnWarnUser(ctx, event.UserID)
if err != nil {
chore.log.Error("Could not unwarn user", zap.Error(Error.Wrap(err)))
}
unwarnedCount++
}
}
hasNext = events.Next
if length := len(events.Events); length > 0 {
cursor.StartingAfter = &events.Events[length-1].UserID
}
}
chore.log.Debug("unfreezing/unwarning executed",
zap.Int("user total", usersCount),
zap.Int("total unwarned", unwarnedCount),
zap.Int("total unfrozen", unfrozenCount),
)
}
// TestSetNow sets nowFn on chore for testing.
func (chore *Chore) TestSetNow(f func() time.Time) {
chore.nowFn = f

View File

@ -88,7 +88,7 @@ func TestAutoFreezeChore(t *testing.T) {
require.NoError(t, err)
require.Equal(t, stripe.InvoiceStatusPaid, inv.Status)
failed, err := invoicesDB.ListFailed(ctx)
failed, err := invoicesDB.ListFailed(ctx, nil)
require.NoError(t, err)
require.Equal(t, 0, len(failed))
@ -148,7 +148,7 @@ func TestAutoFreezeChore(t *testing.T) {
require.Error(t, err)
require.Equal(t, stripe.InvoiceStatusOpen, inv.Status)
failed, err := invoicesDB.ListFailed(ctx)
failed, err := invoicesDB.ListFailed(ctx, nil)
require.NoError(t, err)
require.Equal(t, 1, len(failed))
require.Equal(t, inv.ID, failed[0].ID)
@ -217,7 +217,7 @@ func TestAutoFreezeChore(t *testing.T) {
require.NoError(t, err)
require.Equal(t, stripe.InvoiceStatusOpen, inv.Status)
failed, err := invoicesDB.ListFailed(ctx)
failed, err := invoicesDB.ListFailed(ctx, nil)
require.NoError(t, err)
require.Equal(t, 1, len(failed))
require.Equal(t, inv.ID, failed[0].ID)
@ -225,7 +225,7 @@ func TestAutoFreezeChore(t *testing.T) {
chore.Loop.TriggerWait()
// Payment should have succeeded in the chore.
failed, err = invoicesDB.ListFailed(ctx)
failed, err = invoicesDB.ListFailed(ctx, nil)
require.NoError(t, err)
require.Equal(t, 0, len(failed))
@ -235,6 +235,95 @@ func TestAutoFreezeChore(t *testing.T) {
require.Nil(t, freeze)
})
t.Run("User unfrozen/unwarned for no failed invoices", func(t *testing.T) {
// AnalyticsMock tests that events are sent once.
service.TestChangeFreezeTracker(newFreezeTrackerMock(t))
// reset chore clock
chore.TestSetNow(time.Now)
user2, err := sat.AddUser(ctx, console.CreateUser{
FullName: "Test User",
Email: "user2@mail.test",
}, 1)
require.NoError(t, err)
cus2, err := customerDB.GetCustomerID(ctx, user2.ID)
require.NoError(t, err)
item, err := stripeClient.InvoiceItems().New(&stripe.InvoiceItemParams{
Params: stripe.Params{Context: ctx},
Amount: &amount,
Currency: &curr,
Customer: &cus2,
})
require.NoError(t, err)
items := make([]*stripe.InvoiceUpcomingInvoiceItemParams, 0, 1)
items = append(items, &stripe.InvoiceUpcomingInvoiceItemParams{
InvoiceItem: &item.ID,
Amount: &amount,
Currency: &curr,
})
inv, err := stripeClient.Invoices().New(&stripe.InvoiceParams{
Params: stripe.Params{Context: ctx},
Customer: &cus2,
InvoiceItems: items,
})
require.NoError(t, err)
paymentMethod := stripe1.MockInvoicesPayFailure
inv, err = stripeClient.Invoices().Pay(inv.ID, &stripe.InvoicePayParams{
Params: stripe.Params{Context: ctx},
PaymentMethod: &paymentMethod,
})
require.Error(t, err)
require.Equal(t, stripe.InvoiceStatusOpen, inv.Status)
failed, err := invoicesDB.ListFailed(ctx, nil)
require.NoError(t, err)
require.Equal(t, 1, len(failed))
err = service.FreezeUser(ctx, user.ID)
require.NoError(t, err)
err = service.FreezeUser(ctx, user2.ID)
require.NoError(t, err)
chore.Loop.TriggerWait()
// user(1) should be unfrozen because they have no failed invoices
freeze, _, err := service.GetAll(ctx, user.ID)
require.NoError(t, err)
require.Nil(t, freeze)
// user2 should still be frozen because they have failed invoices
freeze, _, err = service.GetAll(ctx, user2.ID)
require.NoError(t, err)
require.NotNil(t, freeze)
// warn user though they have no failed invoices
err = service.WarnUser(ctx, user.ID)
require.NoError(t, err)
chore.Loop.TriggerWait()
// warned status should be reset
_, warning, err := service.GetAll(ctx, user.ID)
require.NoError(t, err)
require.Nil(t, warning)
// Pay invoice so it doesn't show up in the next test.
inv, err = stripeClient.Invoices().Pay(inv.ID, &stripe.InvoicePayParams{
Params: stripe.Params{Context: ctx},
PaymentMethod: stripe.String(stripe1.MockInvoicesPaySuccess),
})
require.NoError(t, err)
require.Equal(t, stripe.InvoiceStatusPaid, inv.Status)
// unfreeze user so they're not frozen in the next test.
err = service.UnfreezeUser(ctx, user2.ID)
require.NoError(t, err)
})
t.Run("Storjscan exceptions", func(t *testing.T) {
// AnalyticsMock tests that events are sent once.
service.TestChangeFreezeTracker(newFreezeTrackerMock(t))
@ -297,7 +386,7 @@ func TestAutoFreezeChore(t *testing.T) {
require.Error(t, err)
require.Equal(t, stripe.InvoiceStatusOpen, inv.Status)
failed, err := invoicesDB.ListFailed(ctx)
failed, err := invoicesDB.ListFailed(ctx, nil)
require.NoError(t, err)
require.Equal(t, 1, len(failed))
invFound := false
@ -317,6 +406,7 @@ func TestAutoFreezeChore(t *testing.T) {
require.Nil(t, warning)
require.Nil(t, freeze)
})
})
}

View File

@ -36,7 +36,7 @@ type Invoices interface {
// List returns a list of invoices for a given payment account.
List(ctx context.Context, userID uuid.UUID) ([]Invoice, error)
// ListFailed returns a list of failed invoices.
ListFailed(ctx context.Context) ([]Invoice, error)
ListFailed(ctx context.Context, userID *uuid.UUID) ([]Invoice, error)
// ListWithDiscounts returns a list of invoices and coupon usages for a given payment account.
ListWithDiscounts(ctx context.Context, userID uuid.UUID) ([]Invoice, []CouponUsage, error)
// CheckPendingItems returns if pending invoice items for a given payment account exist.

View File

@ -219,13 +219,20 @@ func (invoices *invoices) List(ctx context.Context, userID uuid.UUID) (invoicesL
return invoicesList, nil
}
func (invoices *invoices) ListFailed(ctx context.Context) (invoicesList []payments.Invoice, err error) {
func (invoices *invoices) ListFailed(ctx context.Context, userID *uuid.UUID) (invoicesList []payments.Invoice, err error) {
defer mon.Task()(&ctx)(&err)
status := string(stripe.InvoiceStatusOpen)
params := &stripe.InvoiceListParams{
ListParams: stripe.ListParams{Context: ctx},
Status: &status,
Status: stripe.String(string(stripe.InvoiceStatusOpen)),
}
if userID != nil {
customerID, err := invoices.service.db.Customers().GetCustomerID(ctx, *userID)
if err != nil {
return nil, Error.Wrap(err)
}
params.Customer = &customerID
}
invoicesIterator := invoices.service.stripeClient.Invoices().List(params)

View File

@ -7,6 +7,8 @@ import (
"context"
"encoding/json"
"github.com/zeebo/errs"
"storj.io/common/uuid"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/satellitedb/dbx"
@ -17,7 +19,7 @@ var _ console.AccountFreezeEvents = (*accountFreezeEvents)(nil)
// accountFreezeEvents is an implementation of console.AccountFreezeEvents.
type accountFreezeEvents struct {
db dbx.Methods
db *satelliteDB
}
// Upsert is a method for updating an account freeze event if it exists and inserting it otherwise.
@ -64,6 +66,59 @@ func (events *accountFreezeEvents) Get(ctx context.Context, userID uuid.UUID, ev
return fromDBXAccountFreezeEvent(dbxEvent)
}
// GetAllEvents is a method for querying all account freeze events from the database.
func (events *accountFreezeEvents) GetAllEvents(ctx context.Context, cursor console.FreezeEventsCursor) (freezeEvents *console.FreezeEventsPage, err error) {
defer mon.Task()(&ctx)(&err)
if cursor.Limit <= 0 {
return nil, errs.New("limit cannot be zero or less")
}
page := console.FreezeEventsPage{
Events: make([]console.AccountFreezeEvent, 0, cursor.Limit),
}
if cursor.StartingAfter == nil {
cursor.StartingAfter = &uuid.UUID{}
}
rows, err := events.db.Query(ctx, events.db.Rebind(`
SELECT user_id, event
FROM account_freeze_events
WHERE user_id > ?
ORDER BY user_id LIMIT ?
`), cursor.StartingAfter, cursor.Limit+1)
if err != nil {
return nil, Error.Wrap(err)
}
defer func() { err = errs.Combine(err, rows.Close()) }()
count := 0
for rows.Next() {
count++
if count > cursor.Limit {
// we are done with this page; do not include this event
page.Next = true
break
}
var event dbx.AccountFreezeEvent
err = rows.Scan(&event.UserId, &event.Event)
if err != nil {
return nil, err
}
eventToSend, err := fromDBXAccountFreezeEvent(&event)
if err != nil {
return nil, err
}
page.Events = append(page.Events, *eventToSend)
}
return &page, rows.Err()
}
// GetAll is a method for querying all account freeze events from the database by user ID.
func (events *accountFreezeEvents) GetAll(ctx context.Context, userID uuid.UUID) (freeze *console.AccountFreezeEvent, warning *console.AccountFreezeEvent, err error) {
defer mon.Task()(&ctx)(&err)

View File

@ -83,7 +83,7 @@ func (db *ConsoleDB) WebappSessions() consoleauth.WebappSessions {
// AccountFreezeEvents is a getter for AccountFreezeEvents repository.
func (db *ConsoleDB) AccountFreezeEvents() console.AccountFreezeEvents {
return &accountFreezeEvents{db.methods}
return &accountFreezeEvents{db.db}
}
// WithTx is a method for executing and retrying transaction.