storj/satellite/payments/stripe/client.go
Wilfred Asomani 4a49bc4b65 satellite/payments: fix account freeze chore race condition
This change fixes an issue where a formerly warned/frozen user will be
warned again even though they have made payment for the invoice that got
them frozen in the first place. A payment status check is now made
right before a warn/freeze event to make sure the invoice hasn't been
paid already.

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

Change-Id: I3f6ac1e224f40107d58dc8f7bdbce58bbbea0196
2023-06-14 10:10:56 +00:00

282 lines
8.9 KiB
Go

// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
package stripe
import (
"bytes"
"context"
"errors"
"math"
"math/rand"
"net/http"
"time"
"github.com/stripe/stripe-go/v72"
"github.com/stripe/stripe-go/v72/charge"
"github.com/stripe/stripe-go/v72/client"
"github.com/stripe/stripe-go/v72/customer"
"github.com/stripe/stripe-go/v72/customerbalancetransaction"
"github.com/stripe/stripe-go/v72/form"
"github.com/stripe/stripe-go/v72/invoice"
"github.com/stripe/stripe-go/v72/invoiceitem"
"github.com/stripe/stripe-go/v72/paymentmethod"
"github.com/stripe/stripe-go/v72/promotioncode"
"go.uber.org/zap"
"storj.io/common/time2"
)
// Client Stripe client interface.
type Client interface {
Customers() Customers
PaymentMethods() PaymentMethods
Invoices() Invoices
InvoiceItems() InvoiceItems
CustomerBalanceTransactions() CustomerBalanceTransactions
Charges() Charges
PromoCodes() PromoCodes
CreditNotes() CreditNotes
}
// Customers Stripe Customers interface.
type Customers interface {
New(params *stripe.CustomerParams) (*stripe.Customer, error)
Get(id string, params *stripe.CustomerParams) (*stripe.Customer, error)
Update(id string, params *stripe.CustomerParams) (*stripe.Customer, error)
List(listParams *stripe.CustomerListParams) *customer.Iter
}
// PaymentMethods Stripe PaymentMethods interface.
type PaymentMethods interface {
List(listParams *stripe.PaymentMethodListParams) *paymentmethod.Iter
New(params *stripe.PaymentMethodParams) (*stripe.PaymentMethod, error)
Attach(id string, params *stripe.PaymentMethodAttachParams) (*stripe.PaymentMethod, error)
Detach(id string, params *stripe.PaymentMethodDetachParams) (*stripe.PaymentMethod, error)
}
// Invoices Stripe Invoices interface.
type Invoices interface {
New(params *stripe.InvoiceParams) (*stripe.Invoice, error)
List(listParams *stripe.InvoiceListParams) *invoice.Iter
Update(id string, params *stripe.InvoiceParams) (*stripe.Invoice, error)
FinalizeInvoice(id string, params *stripe.InvoiceFinalizeParams) (*stripe.Invoice, error)
Pay(id string, params *stripe.InvoicePayParams) (*stripe.Invoice, error)
Del(id string, params *stripe.InvoiceParams) (*stripe.Invoice, error)
Get(id string, params *stripe.InvoiceParams) (*stripe.Invoice, error)
}
// InvoiceItems Stripe InvoiceItems interface.
type InvoiceItems interface {
New(params *stripe.InvoiceItemParams) (*stripe.InvoiceItem, error)
Update(id string, params *stripe.InvoiceItemParams) (*stripe.InvoiceItem, error)
List(listParams *stripe.InvoiceItemListParams) *invoiceitem.Iter
Del(id string, params *stripe.InvoiceItemParams) (*stripe.InvoiceItem, error)
}
// Charges Stripe Charges interface.
type Charges interface {
List(listParams *stripe.ChargeListParams) *charge.Iter
}
// PromoCodes is the Stripe PromoCodes interface.
type PromoCodes interface {
List(params *stripe.PromotionCodeListParams) *promotioncode.Iter
}
// CustomerBalanceTransactions Stripe CustomerBalanceTransactions interface.
type CustomerBalanceTransactions interface {
New(params *stripe.CustomerBalanceTransactionParams) (*stripe.CustomerBalanceTransaction, error)
List(listParams *stripe.CustomerBalanceTransactionListParams) *customerbalancetransaction.Iter
}
// CreditNotes Stripe CreditNotes interface.
type CreditNotes interface {
New(params *stripe.CreditNoteParams) (*stripe.CreditNote, error)
}
type stripeClient struct {
client *client.API
}
func (s *stripeClient) Customers() Customers {
return s.client.Customers
}
func (s *stripeClient) PaymentMethods() PaymentMethods {
return s.client.PaymentMethods
}
func (s *stripeClient) Invoices() Invoices {
return s.client.Invoices
}
func (s *stripeClient) InvoiceItems() InvoiceItems {
return s.client.InvoiceItems
}
func (s *stripeClient) CustomerBalanceTransactions() CustomerBalanceTransactions {
return s.client.CustomerBalanceTransactions
}
func (s *stripeClient) Charges() Charges {
return s.client.Charges
}
func (s *stripeClient) PromoCodes() PromoCodes {
return s.client.PromotionCodes
}
func (s *stripeClient) CreditNotes() CreditNotes {
return s.client.CreditNotes
}
// NewStripeClient creates Stripe client from configuration.
func NewStripeClient(log *zap.Logger, config Config) Client {
sClient := client.New(config.StripeSecretKey,
&stripe.Backends{
API: NewBackendWrapper(log, stripe.APIBackend, config.Retries),
Connect: NewBackendWrapper(log, stripe.ConnectBackend, config.Retries),
Uploads: NewBackendWrapper(log, stripe.UploadsBackend, config.Retries),
},
)
return &stripeClient{client: sClient}
}
// RetryConfig contains the configuration for an exponential backoff strategy when retrying Stripe API calls.
type RetryConfig struct {
InitialBackoff time.Duration `help:"the duration of the first retry interval" default:"20ms"`
MaxBackoff time.Duration `help:"the maximum duration of any retry interval" default:"5s"`
Multiplier float64 `help:"the factor by which the retry interval will be multiplied on each iteration" default:"2"`
MaxRetries int64 `help:"the maximum number of times to retry a request" default:"10"`
}
// BackendWrapper is a wrapper for the Stripe backend that uses an exponential backoff strategy for retrying Stripe API calls.
type BackendWrapper struct {
backend stripe.Backend
retryCfg RetryConfig
clock time2.Clock
}
// NewBackendWrapper creates a new wrapper for a Stripe backend.
func NewBackendWrapper(log *zap.Logger, backendType stripe.SupportedBackend, retryCfg RetryConfig) *BackendWrapper {
backendConfig := &stripe.BackendConfig{
LeveledLogger: log.Sugar(),
// Disable internal retries since we have our own retry+backoff strategy.
MaxNetworkRetries: stripe.Int64(0),
}
return &BackendWrapper{
retryCfg: retryCfg,
backend: stripe.GetBackendWithConfig(backendType, backendConfig),
}
}
// TestSwapBackend replaces the wrapped backend with the one specified for use in testing.
func (w *BackendWrapper) TestSwapBackend(backend stripe.Backend) {
w.backend = backend
}
// TestSwapClock replaces the internal clock with the one specified for use in testing.
func (w *BackendWrapper) TestSwapClock(clock time2.Clock) {
w.clock = clock
}
// Call implements the stripe.Backend interface.
func (w *BackendWrapper) Call(method, path, key string, params stripe.ParamsContainer, v stripe.LastResponseSetter) error {
return w.withRetries(params, func() error {
return w.backend.Call(method, path, key, params, v)
})
}
// CallStreaming implements the stripe.Backend interface.
func (w *BackendWrapper) CallStreaming(method, path, key string, params stripe.ParamsContainer, v stripe.StreamingLastResponseSetter) error {
return w.withRetries(params, func() error {
return w.backend.CallStreaming(method, path, key, params, v)
})
}
// CallRaw implements the stripe.Backend interface.
func (w *BackendWrapper) CallRaw(method, path, key string, body *form.Values, params *stripe.Params, v stripe.LastResponseSetter) error {
return w.withRetries(params, func() error {
return w.backend.CallRaw(method, path, key, body, params, v)
})
}
// CallMultipart implements the stripe.Backend interface.
func (w *BackendWrapper) CallMultipart(method, path, key, boundary string, body *bytes.Buffer, params *stripe.Params, v stripe.LastResponseSetter) error {
return w.withRetries(params, func() error {
return w.backend.CallMultipart(method, path, key, boundary, body, params, v)
})
}
// SetMaxNetworkRetries sets the maximum number of times to retry failed requests.
func (w *BackendWrapper) SetMaxNetworkRetries(max int64) {
w.retryCfg.MaxRetries = max
}
// withRetries executes the provided Stripe API call using an exponential backoff strategy
// for retrying in the case of failure.
func (w *BackendWrapper) withRetries(params stripe.ParamsContainer, call func() error) error {
ctx := context.Background()
if params != nil {
innerParams := params.GetParams()
if innerParams != nil && innerParams.Context != nil {
ctx = innerParams.Context
}
}
if err := ctx.Err(); err != nil {
return err
}
for retry := int64(0); ; retry++ {
err := call()
if err == nil {
return nil
}
if !w.shouldRetry(retry, err) {
return err
}
minBackoff := float64(w.retryCfg.InitialBackoff)
maxBackoff := math.Min(
float64(w.retryCfg.MaxBackoff),
minBackoff*math.Pow(w.retryCfg.Multiplier, float64(retry)),
)
backoff := minBackoff + rand.Float64()*(maxBackoff-minBackoff)
if !w.clock.Sleep(ctx, time.Duration(backoff)) {
return ctx.Err()
}
}
}
// shouldRetry returns whether a Stripe API call should be retried.
func (w *BackendWrapper) shouldRetry(retry int64, err error) bool {
if retry >= w.retryCfg.MaxRetries {
return false
}
var stripeErr *stripe.Error
if !errors.As(err, &stripeErr) {
return false
}
resp := stripeErr.LastResponse
if resp == nil {
return false
}
switch resp.Header.Get("Stripe-Should-Retry") {
case "true":
return true
case "false":
return false
}
return resp.StatusCode == http.StatusTooManyRequests
}