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