Skip to content

Commit 42c953a

Browse files
Ben Creechfeliixx
Ben Creech
authored andcommitted
Support confirmation of SetupIntents with a payment method ID
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. I also added a few of the canonical test articles you can use as a payment method ID. These generally map 1:1 with the test card numbers.
1 parent ef48999 commit 42c953a

File tree

2 files changed

+118
-27
lines changed

2 files changed

+118
-27
lines changed

localstripe/resources.py

+65-21
Original file line numberDiff line numberDiff line change
@@ -2203,6 +2203,10 @@ def _api_detach(cls, id, **kwargs):
22032203

22042204
@classmethod
22052205
def _api_retrieve(cls, id):
2206+
obj = cls._try_get_canonical_test_article(id)
2207+
if obj:
2208+
return obj
2209+
22062210
# https://stripe.com/docs/payments/payment-methods#transitioning
22072211
# You can retrieve all saved compatible payment instruments through the
22082212
# Payment Methods API.
@@ -2213,6 +2217,41 @@ def _api_retrieve(cls, id):
22132217

22142218
return super()._api_retrieve(id)
22152219

2220+
@classmethod
2221+
def _try_get_canonical_test_article(cls, id):
2222+
"""Convert special payment method IDs into payment method objects.
2223+
2224+
See https://docs.stripe.com/testing?testing-method=payment-methods.
2225+
2226+
Oddly, as we do here, Stripe will convert these special test IDs into
2227+
actual objects and store them on a GET request, meaning the GET has
2228+
side effects and is not idempotent."""
2229+
2230+
if id == 'pm_card_visa':
2231+
return PaymentMethod(
2232+
type='card',
2233+
card=dict(
2234+
number='4242424242424242',
2235+
exp_month='12',
2236+
exp_year='2030',
2237+
cvc='123'))
2238+
if id == 'pm_card_visa_chargeDeclined':
2239+
return PaymentMethod(
2240+
type='card',
2241+
card=dict(
2242+
number='4000000000000002',
2243+
exp_month='12',
2244+
exp_year='2030',
2245+
cvc='123'))
2246+
if id == 'pm_card_chargeCustomerFail':
2247+
return PaymentMethod(
2248+
type='card',
2249+
card=dict(
2250+
number='4000000000000341',
2251+
exp_month='12',
2252+
exp_year='2030',
2253+
cvc='123'))
2254+
22162255
@classmethod
22172256
def _api_list_all(cls, url, customer=None, type=None, limit=None,
22182257
starting_after=None):
@@ -2704,7 +2743,7 @@ def __init__(self, customer=None, usage=None, payment_method_types=None,
27042743

27052744
@classmethod
27062745
def _api_confirm(cls, id, use_stripe_sdk=None, client_secret=None,
2707-
payment_method_data=None, **kwargs):
2746+
payment_method=None, payment_method_data=None, **kwargs):
27082747
if kwargs:
27092748
raise UserError(400, 'Unexpected ' + ', '.join(kwargs.keys()))
27102749

@@ -2722,27 +2761,13 @@ def _api_confirm(cls, id, use_stripe_sdk=None, client_secret=None,
27222761
if client_secret and client_secret != obj.client_secret:
27232762
raise UserError(401, 'Unauthorized')
27242763

2725-
if payment_method_data:
2726-
if obj.payment_method is not None:
2727-
raise UserError(400, 'Bad request')
2728-
2764+
if payment_method is not None:
2765+
assert isinstance(payment_method, str)
2766+
pm = PaymentMethod._api_retrieve(payment_method)
2767+
obj._attach_pm(pm)
2768+
elif payment_method_data is not None:
27292769
pm = PaymentMethod(**payment_method_data)
2730-
obj.payment_method = pm.id
2731-
2732-
if pm._attaching_is_declined():
2733-
obj.status = 'canceled'
2734-
obj.next_action = None
2735-
raise UserError(402, 'Your card was declined.',
2736-
{'code': 'card_declined'})
2737-
elif pm._requires_authentication():
2738-
obj.status = 'requires_action'
2739-
obj.next_action = {'type': 'use_stripe_sdk',
2740-
'use_stripe_sdk': {
2741-
'type': 'three_d_secure_redirect',
2742-
'stripe_js': ''}}
2743-
else:
2744-
obj.status = 'succeeded'
2745-
obj.next_action = None
2770+
obj._attach_pm(pm)
27462771
elif obj.payment_method is None:
27472772
obj.status = 'requires_payment_method'
27482773
obj.next_action = None
@@ -2751,6 +2776,25 @@ def _api_confirm(cls, id, use_stripe_sdk=None, client_secret=None,
27512776
obj.next_action = None
27522777
return obj
27532778

2779+
def _attach_pm(self, pm):
2780+
self.payment_method = pm.id
2781+
self.payment_method_types = [pm.type]
2782+
2783+
if pm._attaching_is_declined():
2784+
self.status = 'canceled'
2785+
self.next_action = None
2786+
raise UserError(402, 'Your card was declined.',
2787+
{'code': 'card_declined'})
2788+
elif pm._requires_authentication():
2789+
self.status = 'requires_action'
2790+
self.next_action = {'type': 'use_stripe_sdk',
2791+
'use_stripe_sdk': {
2792+
'type': 'three_d_secure_redirect',
2793+
'stripe_js': ''}}
2794+
else:
2795+
self.status = 'succeeded'
2796+
self.next_action = None
2797+
27542798
@classmethod
27552799
def _api_cancel(cls, id, use_stripe_sdk=None, client_secret=None,
27562800
**kwargs):

test.sh

+53-6
Original file line numberDiff line numberDiff line change
@@ -560,35 +560,35 @@ cus=$(curl -sSfg -u $SK: $HOST/v1/customers \
560560
-d email=john.malkovich@example.com \
561561
| grep -oE 'cus_\w+' | head -n 1)
562562

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

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

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

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

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

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

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

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

621+
# We can also pass a payment method ID to setup_intents/*/confirm:
622+
res=$(curl -sSfg -u $SK: $HOST/v1/setup_intents -X POST)
623+
seti=$(echo "$res" | grep '"id"' | grep -oE 'seti_\w+' | head -n 1)
624+
seti_secret=$(echo $res | grep -oE 'seti_\w+_secret_\w+' | head -n 1)
625+
status=$(
626+
curl -sSfg $HOST/v1/setup_intents/$seti/confirm \
627+
-d key=pk_test_sldkjflaksdfj \
628+
-d client_secret=$seti_secret \
629+
-d payment_method=$pm_card_okay \
630+
| grep -oE '"status": "succeeded"')
631+
[ -n "$status" ]
632+
633+
# ... and payment method IDs on bad cards fail on setup_intents/*/confirm:
634+
res=$(curl -sSfg -u $SK: $HOST/v1/setup_intents -X POST)
635+
seti=$(echo "$res" | grep '"id"' | grep -oE 'seti_\w+' | head -n 1)
636+
seti_secret=$(echo $res | grep -oE 'seti_\w+_secret_\w+' | head -n 1)
637+
code=$(
638+
curl -sg -w "%{http_code}" -o /dev/null $HOST/v1/setup_intents/$seti/confirm \
639+
-d key=pk_test_sldkjflaksdfj \
640+
-d client_secret=$seti_secret \
641+
-d payment_method=$pm_card_decline_on_attach)
642+
[ "$code" = 402 ]
643+
644+
# We can also pass a special well-known payment method ID to
645+
# setup_intents/*/confirm:
646+
res=$(curl -sSfg -u $SK: $HOST/v1/setup_intents -X POST)
647+
seti=$(echo "$res" | grep '"id"' | grep -oE 'seti_\w+' | head -n 1)
648+
seti_secret=$(echo $res | grep -oE 'seti_\w+_secret_\w+' | head -n 1)
649+
status=$(
650+
curl -sSfg $HOST/v1/setup_intents/$seti/confirm \
651+
-d key=pk_test_sldkjflaksdfj \
652+
-d client_secret=$seti_secret \
653+
-d payment_method=pm_card_visa \
654+
| grep -oE '"status": "succeeded"')
655+
[ -n "$status" ]
656+
657+
# ... including well-known bad payment method IDs:
658+
res=$(curl -sSfg -u $SK: $HOST/v1/setup_intents -X POST)
659+
seti=$(echo "$res" | grep '"id"' | grep -oE 'seti_\w+' | head -n 1)
660+
seti_secret=$(echo $res | grep -oE 'seti_\w+_secret_\w+' | head -n 1)
661+
code=$(
662+
curl -sg -w "%{http_code}" -o /dev/null $HOST/v1/setup_intents/$seti/confirm \
663+
-d key=pk_test_sldkjflaksdfj \
664+
-d client_secret=$seti_secret \
665+
-d payment_method=pm_card_visa_chargeDeclined)
666+
[ "$code" = 402 ]
667+
621668
# off_session cannot be used when confirm is false
622669
code=$(
623670
curl -sg -o /dev/null -w "%{http_code}" \

0 commit comments

Comments
 (0)