satellite/console: add triggerAttemptPaymentIfFrozen

add triggerAttemptPaymentIfFrozen to check if the account is frozen
and if frozen, will trigger an attempt to pay outstanding invoices

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

Change-Id: I0da6a982e2da4204dee219d98ce2d503cbbb6f8e
This commit is contained in:
Lizzy Thomson 2022-12-21 13:27:29 -07:00 committed by Storj Robot
parent 4241e6bf5f
commit db489125b8
5 changed files with 108 additions and 6 deletions

View File

@ -4,6 +4,7 @@
package consoleapi
import (
"context"
"encoding/json"
"io"
"net/http"
@ -28,15 +29,17 @@ var (
// Payments is an api controller that exposes all payment related functionality.
type Payments struct {
log *zap.Logger
service *console.Service
log *zap.Logger
service *console.Service
accountFreezeService *console.AccountFreezeService
}
// NewPayments is a constructor for api payments controller.
func NewPayments(log *zap.Logger, service *console.Service) *Payments {
func NewPayments(log *zap.Logger, service *console.Service, accountFreezeService *console.AccountFreezeService) *Payments {
return &Payments{
log: log,
service: service,
log: log,
service: service,
accountFreezeService: accountFreezeService,
}
}
@ -128,6 +131,34 @@ func (p *Payments) ProjectsCharges(w http.ResponseWriter, r *http.Request) {
}
}
// triggerAttemptPaymentIfFrozen checks if the account is frozen and if frozen, will trigger attempt to pay outstanding invoices.
func (p *Payments) triggerAttemptPaymentIfFrozen(ctx context.Context) (err error) {
defer mon.Task()(&ctx)(&err)
userID, err := p.service.GetUserID(ctx)
if err != nil {
return err
}
isFrozen, err := p.accountFreezeService.IsUserFrozen(ctx, userID)
if err != nil {
return err
}
if isFrozen {
err = p.service.Payments().AttemptPayOverdueInvoices(ctx)
if err != nil {
return err
}
err = p.accountFreezeService.UnfreezeUser(ctx, userID)
if err != nil {
return err
}
}
return nil
}
// AddCreditCard is used to save new credit card and attach it to payment account.
func (p *Payments) AddCreditCard(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@ -152,6 +183,12 @@ func (p *Payments) AddCreditCard(w http.ResponseWriter, r *http.Request) {
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
err = p.triggerAttemptPaymentIfFrozen(ctx)
if err != nil {
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
}
// ListCreditCards returns a list of credit cards for a given payment account.
@ -206,6 +243,12 @@ func (p *Payments) MakeCreditCardDefault(w http.ResponseWriter, r *http.Request)
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
err = p.triggerAttemptPaymentIfFrozen(ctx)
if err != nil {
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
}
// RemoveCreditCard is used to detach a credit card from payment account.

View File

@ -305,7 +305,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, oidc
abRouter.Handle("/hit/{action}", server.withAuth(http.HandlerFunc(abController.SendHit))).Methods(http.MethodPost)
}
paymentController := consoleapi.NewPayments(logger, service)
paymentController := consoleapi.NewPayments(logger, service, accountFreezeService)
paymentsRouter := router.PathPrefix("/api/v0/payments").Subrouter()
paymentsRouter.Use(server.withAuth)
paymentsRouter.HandleFunc("/cards", paymentController.AddCreditCard).Methods(http.MethodPost)

View File

@ -625,6 +625,23 @@ func (payment Payments) GetCoupon(ctx context.Context) (coupon *payments.Coupon,
return coupon, nil
}
// AttemptPayOverdueInvoices attempts to pay a user's open, overdue invoices.
func (payment Payments) AttemptPayOverdueInvoices(ctx context.Context) (err error) {
defer mon.Task()(&ctx)(&err)
user, err := payment.service.getUserAndAuditLog(ctx, "attempt to pay overdue invoices")
if err != nil {
return Error.Wrap(err)
}
err = payment.service.accounts.Invoices().AttemptPayOverdueInvoices(ctx, user.ID)
if err != nil {
return Error.Wrap(err)
}
return nil
}
// checkRegistrationSecret returns a RegistrationToken if applicable (nil if not), and an error
// if and only if the registration shouldn't proceed.
func (s *Service) checkRegistrationSecret(ctx context.Context, tokenSecret RegistrationSecret) (*RegistrationToken, error) {

View File

@ -20,6 +20,8 @@ type Invoices interface {
ListWithDiscounts(ctx context.Context, userID uuid.UUID) ([]Invoice, []CouponUsage, error)
// CheckPendingItems returns if pending invoice items for a given payment account exist.
CheckPendingItems(ctx context.Context, userID uuid.UUID) (existingItems bool, err error)
// AttemptPayOverdueInvoices attempts to pay a user's open, overdue invoices.
AttemptPayOverdueInvoices(ctx context.Context, userID uuid.UUID) (err error)
}
// Invoice holds all public information about invoice.

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/stripe/stripe-go/v72"
"github.com/zeebo/errs"
"storj.io/common/uuid"
"storj.io/storj/satellite/payments"
@ -20,6 +21,45 @@ type invoices struct {
service *Service
}
// AttemptPayOverdueInvoices attempts to pay a user's open, overdue invoices.
func (invoices *invoices) AttemptPayOverdueInvoices(ctx context.Context, userID uuid.UUID) (err error) {
customerID, err := invoices.service.db.Customers().GetCustomerID(ctx, userID)
if err != nil {
return Error.Wrap(err)
}
params := &stripe.InvoiceListParams{
Customer: &customerID,
Status: stripe.String(string(stripe.InvoiceStatusOpen)),
DueDateRange: &stripe.RangeQueryParams{LesserThan: time.Now().Unix()},
}
var errGrp errs.Group
invoicesIterator := invoices.service.stripeClient.Invoices().List(params)
for invoicesIterator.Next() {
stripeInvoice := invoicesIterator.Invoice()
params := &stripe.InvoicePayParams{}
invResponse, err := invoices.service.stripeClient.Invoices().Pay(stripeInvoice.ID, params)
if err != nil {
errGrp.Add(Error.New("unable to pay invoice %s: %w", stripeInvoice.ID, err))
continue
}
if invResponse != nil && invResponse.Status != stripe.InvoiceStatusPaid {
errGrp.Add(Error.New("invoice not paid after payment triggered %s", stripeInvoice.ID))
}
}
if err = invoicesIterator.Err(); err != nil {
return Error.Wrap(err)
}
return errGrp.Err()
}
// List returns a list of invoices for a given payment account.
func (invoices *invoices) List(ctx context.Context, userID uuid.UUID) (invoicesList []payments.Invoice, err error) {
defer mon.Task()(&ctx, userID)(&err)