satellite/payments: restrict addition of duplicate credit cards

By this change we don't allow users to add credit cards that are already bind to their account.
We still allow the same CC number but with a different expiration date.

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

Change-Id: Ifeb0cc5ae0c2f0f7596af4dead70ae7d20d30613
This commit is contained in:
Vitalii 2023-09-07 19:21:11 +03:00
parent 3119b614ae
commit c31fb9c1cf
5 changed files with 65 additions and 1 deletions

View File

@ -22,6 +22,7 @@ import (
"storj.io/storj/satellite/payments"
"storj.io/storj/satellite/payments/billing"
"storj.io/storj/satellite/payments/paymentsconfig"
"storj.io/storj/satellite/payments/stripe"
)
var (
@ -209,6 +210,11 @@ func (p *Payments) AddCreditCard(w http.ResponseWriter, r *http.Request) {
return
}
if stripe.ErrDuplicateCard.Has(err) {
p.serveJSONError(ctx, w, http.StatusBadRequest, err)
return
}
p.serveJSONError(ctx, w, http.StatusInternalServerError, err)
return
}

View File

@ -18,6 +18,8 @@ var (
ErrCardNotFound = errs.Class("card not found")
// ErrDefaultCard is returned when a user tries to delete their default card.
ErrDefaultCard = errs.Class("default card")
// ErrDuplicateCard is returned when a user tries to add duplicate card.
ErrDuplicateCard = errs.Class("duplicate card")
)
// creditCards is an implementation of payments.CreditCards.
@ -94,6 +96,27 @@ func (creditCards *creditCards) Add(ctx context.Context, userID uuid.UUID, cardT
return payments.CreditCard{}, Error.Wrap(err)
}
listParams := &stripe.PaymentMethodListParams{
ListParams: stripe.ListParams{Context: ctx},
Customer: &customerID,
Type: stripe.String(string(stripe.PaymentMethodTypeCard)),
}
paymentMethodsIterator := creditCards.service.stripeClient.PaymentMethods().List(listParams)
for paymentMethodsIterator.Next() {
stripeCard := paymentMethodsIterator.PaymentMethod()
if stripeCard.Card.Fingerprint == card.Card.Fingerprint &&
stripeCard.Card.ExpMonth == card.Card.ExpMonth &&
stripeCard.Card.ExpYear == card.Card.ExpYear {
return payments.CreditCard{}, ErrDuplicateCard.New("this card is already on file for your account.")
}
}
if err = paymentMethodsIterator.Err(); err != nil {
return payments.CreditCard{}, Error.Wrap(err)
}
attachParams := &stripe.PaymentMethodAttachParams{
Params: stripe.Params{Context: ctx},
Customer: &customerID,

View File

@ -85,6 +85,35 @@ func TestCreditCards_Add(t *testing.T) {
})
}
func TestCreditCards_AddDuplicateCard(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
satellite := planet.Satellites[0]
u, err := satellite.AddUser(ctx, console.CreateUser{
FullName: "Test User",
Email: "test@storj.test",
}, 1)
require.NoError(t, err)
cardToken := "testToken"
card, err := satellite.API.Payments.Accounts.CreditCards().Add(ctx, u.ID, cardToken)
require.NoError(t, err)
require.NotEmpty(t, card)
card, err = satellite.API.Payments.Accounts.CreditCards().Add(ctx, u.ID, cardToken)
require.Error(t, err)
require.True(t, stripe.ErrDuplicateCard.Has(err))
require.Empty(t, card)
cards, err := satellite.API.Payments.Accounts.CreditCards().List(ctx, u.ID)
require.NoError(t, err)
require.Len(t, cards, 1)
})
}
func TestCreditCards_Remove(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 2,

View File

@ -446,6 +446,7 @@ func (m *mockPaymentMethods) New(params *stripe.PaymentMethodParams) (*stripe.Pa
Brand: "Mastercard",
Last4: "4444",
Description: randID,
Fingerprint: "fingerprint" + *params.Card.Token,
},
Type: stripe.PaymentMethodTypeCard,
}

View File

@ -133,9 +133,14 @@ export class PaymentsHttpApi implements PaymentsApi {
return;
}
const result = await response.json();
const errorMsg = result.error.includes('duplicate card') ?
'This card is already on file for your account' :
'Can not add credit card';
throw new APIError({
status: response.status,
message: 'Can not add credit card',
message: errorMsg,
requestID: response.headers.get('x-request-id'),
});
}