Skip to content

Commit

Permalink
Support confirmation of SetupIntents with a payment method ID
Browse files Browse the repository at this point in the history
Previously localstripe only supported confirmation of SetupIntents with
payment_method_data, which is a dictionary containing the information needed
to create a PaymentMethod. The Stripe SetupIntents API also allows the caller
to pass in an already-created PaymentMethod, by ID. This change adds that
capability to localstripe.
  • Loading branch information
Ben Creech committed Sep 21, 2024
1 parent 825150b commit 08a5437
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 27 deletions.
86 changes: 65 additions & 21 deletions localstripe/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -2195,6 +2195,10 @@ def _api_detach(cls, id, **kwargs):

@classmethod
def _api_retrieve(cls, id):
obj = cls._try_get_canonical_test_article(id)
if obj:
return obj

# https://stripe.com/docs/payments/payment-methods#transitioning
# You can retrieve all saved compatible payment instruments through the
# Payment Methods API.
Expand All @@ -2205,6 +2209,41 @@ def _api_retrieve(cls, id):

return super()._api_retrieve(id)

@classmethod
def _try_get_canonical_test_article(cls, id):
"""Convert special payment method IDs into payment method objects.
See https://docs.stripe.com/testing?testing-method=payment-methods.
Oddly, as we do here, Stripe will convert these special test IDs into
actual objects and store them on a GET request, meaning the GET has
side effects and is not idempotent."""

if id == 'pm_card_visa':
return PaymentMethod(
type='card',
card=dict(
number='4242424242424242',
exp_month='12',
exp_year='2030',
cvc='123'))
if id == 'pm_card_visa_chargeDeclined':
return PaymentMethod(
type='card',
card=dict(
number='4000000000000002',
exp_month='12',
exp_year='2030',
cvc='123'))
if id == 'pm_card_chargeCustomerFail':
return PaymentMethod(
type='card',
card=dict(
number='4000000000000341',
exp_month='12',
exp_year='2030',
cvc='123'))

@classmethod
def _api_list_all(cls, url, customer=None, type=None, limit=None,
starting_after=None):
Expand Down Expand Up @@ -2696,7 +2735,7 @@ def __init__(self, customer=None, usage=None, payment_method_types=None,

@classmethod
def _api_confirm(cls, id, use_stripe_sdk=None, client_secret=None,
payment_method_data=None, **kwargs):
payment_method=None, payment_method_data=None, **kwargs):
if kwargs:
raise UserError(400, 'Unexpected ' + ', '.join(kwargs.keys()))

Expand All @@ -2714,27 +2753,13 @@ def _api_confirm(cls, id, use_stripe_sdk=None, client_secret=None,
if client_secret and client_secret != obj.client_secret:
raise UserError(401, 'Unauthorized')

if payment_method_data:
if obj.payment_method is not None:
raise UserError(400, 'Bad request')

if payment_method is not None:
assert isinstance(payment_method, str)
pm = PaymentMethod._api_retrieve(payment_method)
obj._attach_pm(pm)
elif payment_method_data is not None:
pm = PaymentMethod(**payment_method_data)
obj.payment_method = pm.id

if pm._attaching_is_declined():
obj.status = 'canceled'
obj.next_action = None
raise UserError(402, 'Your card was declined.',
{'code': 'card_declined'})
elif pm._requires_authentication():
obj.status = 'requires_action'
obj.next_action = {'type': 'use_stripe_sdk',
'use_stripe_sdk': {
'type': 'three_d_secure_redirect',
'stripe_js': ''}}
else:
obj.status = 'succeeded'
obj.next_action = None
obj._attach_pm(pm)
elif obj.payment_method is None:
obj.status = 'requires_payment_method'
obj.next_action = None
Expand All @@ -2743,6 +2768,25 @@ def _api_confirm(cls, id, use_stripe_sdk=None, client_secret=None,
obj.next_action = None
return obj

def _attach_pm(self, pm):
self.payment_method = pm.id
self.payment_method_types = [pm.type]

if pm._attaching_is_declined():
self.status = 'canceled'
self.next_action = None
raise UserError(402, 'Your card was declined.',
{'code': 'card_declined'})
elif pm._requires_authentication():
self.status = 'requires_action'
self.next_action = {'type': 'use_stripe_sdk',
'use_stripe_sdk': {
'type': 'three_d_secure_redirect',
'stripe_js': ''}}
else:
self.status = 'succeeded'
self.next_action = None

@classmethod
def _api_cancel(cls, id, use_stripe_sdk=None, client_secret=None,
**kwargs):
Expand Down
59 changes: 53 additions & 6 deletions test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -560,35 +560,35 @@ cus=$(curl -sSfg -u $SK: $HOST/v1/customers \
-d [email protected] \
| grep -oE 'cus_\w+' | head -n 1)

pm=$(curl -sSfg -u $SK: $HOST/v1/payment_methods \
pm_card_okay=$(curl -sSfg -u $SK: $HOST/v1/payment_methods \
-d type=card \
-d card[number]=4242424242424242 \
-d card[exp_month]=12 \
-d card[exp_year]=2020 \
-d card[cvc]=123 \
| grep -oE 'pm_\w+' | head -n 1)

curl -sSfg -u $SK: $HOST/v1/payment_methods/$pm/attach \
curl -sSfg -u $SK: $HOST/v1/payment_methods/$pm_card_okay/attach \
-d customer=$cus

curl -sSfg -u $SK: $HOST/v1/customers/$cus \
-d invoice_settings[default_payment_method]=$pm
-d invoice_settings[default_payment_method]=$pm_card_okay

curl -sSfg -u $SK: $HOST/v1/customers/$cus?expand[]=invoice_settings.default_payment_method

curl -sSfg -u $SK: $HOST/v1/payment_methods?customer=$cus\&type=card

curl -sSfg -u $SK: $HOST/v1/payment_methods/$pm/detach -X POST
curl -sSfg -u $SK: $HOST/v1/payment_methods/$pm_card_okay/detach -X POST

pm=$(curl -sSfg -u $SK: $HOST/v1/payment_methods \
pm_card_decline_on_attach=$(curl -sSfg -u $SK: $HOST/v1/payment_methods \
-d type=card \
-d card[number]=4000000000000002 \
-d card[exp_month]=4 \
-d card[exp_year]=2042 \
-d card[cvc]=123 \
| grep -oE 'pm_\w+' | head -n 1)
code=$(curl -sg -o /dev/null -w "%{http_code}" -u $SK: \
$HOST/v1/payment_methods/$pm/attach \
$HOST/v1/payment_methods/$pm_card_decline_on_attach/attach \
-d customer=$cus)
[ "$code" = 402 ]

Expand Down Expand Up @@ -618,6 +618,53 @@ curl -sSfg $HOST/v1/setup_intents/$seti/confirm \
-d payment_method_data[card][exp_year]=24 \
-d payment_method_data[billing_details][address][postal_code]=42424

# We can also pass a payment method ID to setup_intents/*/confirm:
res=$(curl -sSfg -u $SK: $HOST/v1/setup_intents -X POST)
seti=$(echo "$res" | grep '"id"' | grep -oE 'seti_\w+' | head -n 1)
seti_secret=$(echo $res | grep -oE 'seti_\w+_secret_\w+' | head -n 1)
status=$(
curl -sSfg $HOST/v1/setup_intents/$seti/confirm \
-d key=pk_test_sldkjflaksdfj \
-d client_secret=$seti_secret \
-d payment_method=$pm_card_okay \
| grep -oE '"status": "succeeded"')
[ -n "$status" ]

# ... and payment method IDs on bad cards fail on setup_intents/*/confirm:
res=$(curl -sSfg -u $SK: $HOST/v1/setup_intents -X POST)
seti=$(echo "$res" | grep '"id"' | grep -oE 'seti_\w+' | head -n 1)
seti_secret=$(echo $res | grep -oE 'seti_\w+_secret_\w+' | head -n 1)
code=$(
curl -sg -w "%{http_code}" -o /dev/null $HOST/v1/setup_intents/$seti/confirm \
-d key=pk_test_sldkjflaksdfj \
-d client_secret=$seti_secret \
-d payment_method=$pm_card_decline_on_attach)
[ "$code" = 402 ]

# We can also pass a special well-known payment method ID to
# setup_intents/*/confirm:
res=$(curl -sSfg -u $SK: $HOST/v1/setup_intents -X POST)
seti=$(echo "$res" | grep '"id"' | grep -oE 'seti_\w+' | head -n 1)
seti_secret=$(echo $res | grep -oE 'seti_\w+_secret_\w+' | head -n 1)
status=$(
curl -sSfg $HOST/v1/setup_intents/$seti/confirm \
-d key=pk_test_sldkjflaksdfj \
-d client_secret=$seti_secret \
-d payment_method=pm_card_visa \
| grep -oE '"status": "succeeded"')
[ -n "$status" ]

# ... including well-known bad payment method IDs:
res=$(curl -sSfg -u $SK: $HOST/v1/setup_intents -X POST)
seti=$(echo "$res" | grep '"id"' | grep -oE 'seti_\w+' | head -n 1)
seti_secret=$(echo $res | grep -oE 'seti_\w+_secret_\w+' | head -n 1)
code=$(
curl -sg -w "%{http_code}" -o /dev/null $HOST/v1/setup_intents/$seti/confirm \
-d key=pk_test_sldkjflaksdfj \
-d client_secret=$seti_secret \
-d payment_method=pm_card_visa_chargeDeclined)
[ "$code" = 402 ]

# off_session cannot be used when confirm is false
code=$(
curl -sg -o /dev/null -w "%{http_code}" \
Expand Down

0 comments on commit 08a5437

Please sign in to comment.