From 7c65c0cea5bf448883a0b2480bbcac72c460f300 Mon Sep 17 00:00:00 2001 From: Wilfred Asomani Date: Tue, 25 Jul 2023 14:08:57 +0000 Subject: [PATCH] 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 --- satellite/console/accountfreezes.go | 31 ++ satellite/payments/accountfreeze/chore.go | 418 ++++++++++-------- .../payments/accountfreeze/chore_test.go | 100 ++++- satellite/payments/invoices.go | 2 +- satellite/payments/stripe/invoices.go | 13 +- satellite/satellitedb/accountfreezeevents.go | 57 ++- satellite/satellitedb/consoledb.go | 2 +- 7 files changed, 437 insertions(+), 186 deletions(-) diff --git a/satellite/console/accountfreezes.go b/satellite/console/accountfreezes.go index c94027477..8b8b8ddc7 100644 --- a/satellite/console/accountfreezes.go +++ b/satellite/console/accountfreezes.go @@ -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 diff --git a/satellite/payments/accountfreeze/chore.go b/satellite/payments/accountfreeze/chore.go index 2e1b880e9..57a2d9378 100644 --- a/satellite/payments/accountfreeze/chore.go +++ b/satellite/payments/accountfreeze/chore.go @@ -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 diff --git a/satellite/payments/accountfreeze/chore_test.go b/satellite/payments/accountfreeze/chore_test.go index 8a544e930..83fc4936e 100644 --- a/satellite/payments/accountfreeze/chore_test.go +++ b/satellite/payments/accountfreeze/chore_test.go @@ -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) }) + }) } diff --git a/satellite/payments/invoices.go b/satellite/payments/invoices.go index cdd7eac34..7ccfeac09 100644 --- a/satellite/payments/invoices.go +++ b/satellite/payments/invoices.go @@ -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. diff --git a/satellite/payments/stripe/invoices.go b/satellite/payments/stripe/invoices.go index 5f7321083..fee106f6d 100644 --- a/satellite/payments/stripe/invoices.go +++ b/satellite/payments/stripe/invoices.go @@ -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) diff --git a/satellite/satellitedb/accountfreezeevents.go b/satellite/satellitedb/accountfreezeevents.go index 2b727f655..af8b8a8ee 100644 --- a/satellite/satellitedb/accountfreezeevents.go +++ b/satellite/satellitedb/accountfreezeevents.go @@ -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) diff --git a/satellite/satellitedb/consoledb.go b/satellite/satellitedb/consoledb.go index ef66782f9..8f5c0b848 100644 --- a/satellite/satellitedb/consoledb.go +++ b/satellite/satellitedb/consoledb.go @@ -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.