Skip to content

Commit

Permalink
satellite/{console,payments}: fix handling for autofreeze flow
Browse files Browse the repository at this point in the history
This change adds an extra step to the auto freeze chore to attempt
payment before freezing/warning a user.
It also attempts payment after modifying user's cards whether the user
is frozen/warned or not.

Issue: storj/storj-private#341

Change-Id: Ia9c0c5a2d37837bca5153fe720fef61f1385cb15
  • Loading branch information
wilfred-asomanii authored and Storj Robot committed Jul 17, 2023
1 parent 47a4d49 commit 0a8115b
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 13 deletions.
17 changes: 8 additions & 9 deletions satellite/console/consoleweb/consoleapi/payments.go
Expand Up @@ -155,8 +155,8 @@ func (p *Payments) ProjectsCharges(w http.ResponseWriter, r *http.Request) {
}
}

// triggerAttemptPaymentIfFrozenOrWarned checks if the account is frozen and if frozen, will trigger attempt to pay outstanding invoices.
func (p *Payments) triggerAttemptPaymentIfFrozenOrWarned(ctx context.Context) (err error) {
// triggerAttemptPayment attempts payment and unfreezes/unwarn user if needed.
func (p *Payments) triggerAttemptPayment(ctx context.Context) (err error) {
defer mon.Task()(&ctx)(&err)

userID, err := p.service.GetUserID(ctx)
Expand All @@ -169,12 +169,11 @@ func (p *Payments) triggerAttemptPaymentIfFrozenOrWarned(ctx context.Context) (e
return err
}

if freeze != nil || warning != nil {
err = p.service.Payments().AttemptPayOverdueInvoices(ctx)
if err != nil {
return err
}
err = p.service.Payments().AttemptPayOverdueInvoices(ctx)
if err != nil {
return err
}

if freeze != nil {
err = p.accountFreezeService.UnfreezeUser(ctx, userID)
if err != nil {
Expand Down Expand Up @@ -214,7 +213,7 @@ func (p *Payments) AddCreditCard(w http.ResponseWriter, r *http.Request) {
return
}

err = p.triggerAttemptPaymentIfFrozenOrWarned(ctx)
err = p.triggerAttemptPayment(ctx)
if err != nil {
p.serveJSONError(ctx, w, http.StatusInternalServerError, err)
return
Expand Down Expand Up @@ -274,7 +273,7 @@ func (p *Payments) MakeCreditCardDefault(w http.ResponseWriter, r *http.Request)
return
}

err = p.triggerAttemptPaymentIfFrozenOrWarned(ctx)
err = p.triggerAttemptPayment(ctx)
if err != nil {
p.serveJSONError(ctx, w, http.StatusInternalServerError, err)
return
Expand Down
24 changes: 24 additions & 0 deletions satellite/payments/accountfreeze/chore.go
Expand Up @@ -170,6 +170,30 @@ func (chore *Chore) Run(ctx context.Context) (err error) {
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
Expand Down
66 changes: 64 additions & 2 deletions satellite/payments/accountfreeze/chore_test.go
Expand Up @@ -113,7 +113,7 @@ func TestAutoFreezeChore(t *testing.T) {
require.Nil(t, warning)
})

t.Run("Freeze event for failed invoice", func(t *testing.T) {
t.Run("Freeze event for failed invoice (failed later payment attempt)", func(t *testing.T) {
// AnalyticsMock tests that events are sent once.
service.TestChangeFreezeTracker(newFreezeTrackerMock(t))
// reset chore clock
Expand Down Expand Up @@ -171,6 +171,68 @@ func TestAutoFreezeChore(t *testing.T) {
freeze, _, err = service.GetAll(ctx, user.ID)
require.NoError(t, err)
require.NotNil(t, freeze)

// 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, user.ID)
require.NoError(t, err)
})

t.Run("No freeze event for failed invoice (successful later payment attempt)", func(t *testing.T) {
// AnalyticsMock tests that events are sent once.
service.TestChangeFreezeTracker(newFreezeTrackerMock(t))
// reset chore clock
chore.TestSetNow(time.Now)

item, err := stripeClient.InvoiceItems().New(&stripe.InvoiceItemParams{
Params: stripe.Params{Context: ctx},
Amount: &amount,
Currency: &curr,
Customer: &cus1,
})
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: &cus1,
InvoiceItems: items,
DefaultPaymentMethod: stripe.String(stripe1.MockInvoicesPaySuccess),
})
require.NoError(t, err)

inv, err = stripeClient.Invoices().FinalizeInvoice(inv.ID, nil)
require.NoError(t, err)
require.Equal(t, stripe.InvoiceStatusOpen, inv.Status)

failed, err := invoicesDB.ListFailed(ctx)
require.NoError(t, err)
require.Equal(t, 1, len(failed))
require.Equal(t, inv.ID, failed[0].ID)

chore.Loop.TriggerWait()

// Payment should have succeeded in the chore.
failed, err = invoicesDB.ListFailed(ctx)
require.NoError(t, err)
require.Equal(t, 0, len(failed))

freeze, warning, err := service.GetAll(ctx, user.ID)
require.NoError(t, err)
require.Nil(t, warning)
require.Nil(t, freeze)
})

t.Run("Storjscan exceptions", func(t *testing.T) {
Expand Down Expand Up @@ -237,7 +299,7 @@ func TestAutoFreezeChore(t *testing.T) {

failed, err := invoicesDB.ListFailed(ctx)
require.NoError(t, err)
require.Equal(t, 2, len(failed))
require.Equal(t, 1, len(failed))
invFound := false
for _, failedInv := range failed {
if failedInv.ID == inv.ID {
Expand Down
26 changes: 24 additions & 2 deletions satellite/payments/stripe/stripemock.go
Expand Up @@ -576,6 +576,10 @@ func (m *mockInvoices) New(params *stripe.InvoiceParams) (*stripe.Invoice, error
},
AmountDue: amountDue,
AmountRemaining: amountDue,
Total: amountDue,
}
if params.DefaultPaymentMethod != nil {
invoice.DefaultPaymentMethod = &stripe.PaymentMethod{ID: *params.DefaultPaymentMethod}
}

m.invoices[*params.Customer] = append(m.invoices[*params.Customer], invoice)
Expand Down Expand Up @@ -608,6 +612,15 @@ func (m *mockInvoices) List(listParams *stripe.InvoiceListParams) *invoice.Iter
}
}
}
} else if listParams.Customer != nil && listParams.Status != nil {
// filter by status and customer
for _, invoices := range m.invoices {
for _, inv := range invoices {
if inv.Status == stripe.InvoiceStatus(*listParams.Status) && inv.Customer.ID == *listParams.Customer {
ret = append(ret, inv)
}
}
}
} else if listParams.Customer == nil {
for _, invoices := range m.invoices {
for _, invoice := range invoices {
Expand Down Expand Up @@ -640,10 +653,9 @@ func (m *mockInvoices) Update(id string, params *stripe.InvoiceParams) (invoice
// FinalizeInvoice forwards the invoice's status from draft to open.
func (m *mockInvoices) FinalizeInvoice(id string, params *stripe.InvoiceFinalizeParams) (*stripe.Invoice, error) {
for _, invoices := range m.invoices {
for i, invoice := range invoices {
for _, invoice := range invoices {
if invoice.ID == id && invoice.Status == stripe.InvoiceStatusDraft {
invoice.Status = stripe.InvoiceStatusOpen
m.invoices[invoice.Customer.ID][i].Status = stripe.InvoiceStatusOpen
return invoice, nil
}
}
Expand All @@ -665,6 +677,16 @@ func (m *mockInvoices) Pay(id string, params *stripe.InvoicePayParams) (*stripe.
invoice.AmountRemaining = 0
return invoice, nil
}
} else if invoice.DefaultPaymentMethod != nil {
if invoice.DefaultPaymentMethod.ID == MockInvoicesPaySuccess {
invoice.Status = stripe.InvoiceStatusPaid
invoice.AmountRemaining = 0
return invoice, nil
}
if invoice.DefaultPaymentMethod.ID == MockInvoicesNewFailure {
invoice.Status = stripe.InvoiceStatusOpen
return invoice, &stripe.Error{}
}
} else if invoice.AmountRemaining == 0 || (params.PaidOutOfBand != nil && *params.PaidOutOfBand) {
invoice.Status = stripe.InvoiceStatusPaid
invoice.AmountRemaining = 0
Expand Down

0 comments on commit 0a8115b

Please sign in to comment.