@@ -434,13 +434,18 @@ def __init__(self, source=None, **kwargs):
434434 self .tokenization_method = None
435435
436436 self .customer = None
437+ self ._authenticated = False
437438
438439 @property
439440 def last4 (self ):
440441 return self ._card_number [- 4 :]
441442
442- def _requires_authentication (self ):
443- return PaymentMethod ._requires_authentication (self )
443+ def _setup_requires_authentication (self , usage = None ):
444+ return PaymentMethod ._setup_requires_authentication (self , usage )
445+
446+ def _payment_requires_authentication (self , off_session = False ):
447+ return PaymentMethod ._payment_requires_authentication (
448+ self , off_session )
444449
445450 def _attaching_is_declined (self ):
446451 return PaymentMethod ._attaching_is_declined (self )
@@ -1830,7 +1835,8 @@ class PaymentIntent(StripeObject):
18301835
18311836 def __init__ (self , amount = None , currency = None , customer = None ,
18321837 payment_method = None , metadata = None , payment_method_types = None ,
1833- capture_method = None , payment_method_options = None , ** kwargs ):
1838+ capture_method = None , payment_method_options = None ,
1839+ off_session = None , ** kwargs ):
18341840 if kwargs :
18351841 raise UserError (400 , 'Unexpected ' + ', ' .join (kwargs .keys ()))
18361842
@@ -1850,6 +1856,8 @@ def __init__(self, amount=None, currency=None, customer=None,
18501856 assert capture_method in ('automatic' ,
18511857 'automatic_async' ,
18521858 'manual' )
1859+ if off_session is not None :
1860+ assert type (off_session ) is bool
18531861 except AssertionError :
18541862 raise UserError (400 , 'Bad request' )
18551863
@@ -1872,6 +1880,7 @@ def __init__(self, amount=None, currency=None, customer=None,
18721880 self .invoice = None
18731881 self .next_action = None
18741882 self .capture_method = capture_method or 'automatic_async'
1883+ self .off_session = off_session or False
18751884
18761885 self ._canceled = False
18771886 self ._authentication_failed = False
@@ -1963,7 +1972,7 @@ def _api_create(cls, confirm=None, off_session=None, **data):
19631972 except AssertionError :
19641973 raise UserError (400 , 'Bad request' )
19651974
1966- obj = super ()._api_create (** data )
1975+ obj = super ()._api_create (off_session = off_session , ** data )
19671976
19681977 if confirm :
19691978 obj ._confirm (on_failure_now = obj ._report_failure )
@@ -1995,7 +2004,7 @@ def _api_confirm(cls, id, payment_method=None, **kwargs):
19952004 def _confirm (self , on_failure_now ):
19962005 self ._authentication_failed = False
19972006 payment_method = PaymentMethod ._api_retrieve (self .payment_method )
1998- if payment_method ._requires_authentication ( ):
2007+ if payment_method ._payment_requires_authentication ( self . off_session ):
19992008 self .next_action = {
20002009 'type' : 'use_stripe_sdk' ,
20012010 'use_stripe_sdk' : {'type' : 'three_d_secure_redirect' ,
@@ -2151,11 +2160,30 @@ def __init__(self, type=None, billing_details=None, card=None,
21512160
21522161 self .customer = None
21532162 self .metadata = metadata or {}
2163+ self ._authenticated = False
2164+
2165+ def _setup_requires_authentication (self , usage = None ):
2166+ if self .type == 'card' :
2167+ if self ._card_number == '4000002500003155' :
2168+ # For this card, if we're setting up a payment method for
2169+ # off_session future payments, Stripe proactively forces
2170+ # 3DS authentication at setup time:
2171+ return usage == 'off_session'
2172+
2173+ return self ._card_number in ('4000002760003184' ,
2174+ '4000008260003178' ,
2175+ '4000000000003220' ,
2176+ '4000000000003063' ,
2177+ '4000008400001629' )
2178+ return False
21542179
2155- def _requires_authentication (self ):
2180+ def _payment_requires_authentication (self , off_session = False ):
21562181 if self .type == 'card' :
2157- return self ._card_number in ('4000002500003155' ,
2158- '4000002760003184' ,
2182+ if self ._card_number == '4000002500003155' :
2183+ # See https://docs.stripe.com/testing#authentication-and-setup
2184+ return not (off_session and self ._authenticated )
2185+
2186+ return self ._card_number in ('4000002760003184' ,
21592187 '4000008260003178' ,
21602188 '4000000000003220' ,
21612189 '4000000000003063' ,
@@ -2268,6 +2296,14 @@ def _try_get_canonical_test_article(cls, id):
22682296 exp_month = '12' ,
22692297 exp_year = '2030' ,
22702298 cvc = '123' ))
2299+ if id == 'pm_card_authenticationRequiredOnSetup' :
2300+ return PaymentMethod (
2301+ type = 'card' ,
2302+ card = dict (
2303+ number = '4000002500003155' ,
2304+ exp_month = '12' ,
2305+ exp_year = '2030' ,
2306+ cvc = '123' ))
22712307
22722308 @classmethod
22732309 def _api_list_all (cls , url , customer = None , type = None , limit = None ,
@@ -2709,9 +2745,15 @@ def __init__(self, type=None, currency=None, owner=None, metadata=None,
27092745 'mandate_url' : 'https://fake/NXDSYREGC9PSMKWY' ,
27102746 }
27112747
2712- def _requires_authentication (self ):
2748+ def _setup_requires_authentication (self , usage = None ):
2749+ if self .type == 'sepa_debit' :
2750+ return PaymentMethod ._setup_requires_authentication (self , usage )
2751+ return False
2752+
2753+ def _payment_requires_authentication (self , off_session = False ):
27132754 if self .type == 'sepa_debit' :
2714- return PaymentMethod ._requires_authentication (self )
2755+ return PaymentMethod ._payment_requires_authentication (
2756+ self , off_session )
27152757 return False
27162758
27172759 def _attaching_is_declined (self ):
@@ -2806,7 +2848,7 @@ def _attach_pm(self, pm):
28062848 self .next_action = None
28072849 raise UserError (402 , 'Your card was declined.' ,
28082850 {'code' : 'card_declined' })
2809- elif pm ._requires_authentication ( ):
2851+ elif pm ._setup_requires_authentication ( self . usage ):
28102852 self .status = 'requires_action'
28112853 self .next_action = {'type' : 'use_stripe_sdk' ,
28122854 'use_stripe_sdk' : {
@@ -2838,10 +2880,45 @@ def _api_cancel(cls, id, use_stripe_sdk=None, client_secret=None,
28382880 obj .next_action = None
28392881 return obj
28402882
2883+ @classmethod
2884+ def _api_authenticate (cls , id , ** kwargs ):
2885+ """This is a test-only endpoint to help test payment methods which
2886+ require authentication during setup.
2887+
2888+ E.g., for credit cards which are subject to the 3D Secure protocol,
2889+ when confirmed, SetupIntent may transition to the 'requires_action'
2890+ status, with a 'next_action' indicating some flow that usually
2891+ involves human interaction from the cardholder. This endpoint bypasses
2892+ that required action for test purposes.
2893+ """
2894+
2895+ if kwargs :
2896+ raise UserError (400 , 'Unexpected ' + ', ' .join (kwargs .keys ()))
2897+
2898+ try :
2899+ assert type (id ) is str and id .startswith ('seti_' )
2900+ except AssertionError :
2901+ raise UserError (400 , 'Bad request' )
2902+
2903+ obj = cls ._api_retrieve (id )
2904+
2905+ if obj .status != 'requires_action' :
2906+ raise UserError (400 , 'Bad request' )
2907+
2908+ pm = PaymentMethod ._api_retrieve (obj .payment_method )
2909+ pm ._authenticated = True
2910+
2911+ obj .status = 'succeeded'
2912+ obj .next_action = None
2913+
2914+ return obj
2915+
28412916
28422917extra_apis .extend ((
28432918 ('POST' , '/v1/setup_intents/{id}/confirm' , SetupIntent ._api_confirm ),
2844- ('POST' , '/v1/setup_intents/{id}/cancel' , SetupIntent ._api_cancel )))
2919+ ('POST' , '/v1/setup_intents/{id}/cancel' , SetupIntent ._api_cancel ),
2920+ ('POST' , '/v1/setup_intents/{id}/_authenticate' ,
2921+ SetupIntent ._api_authenticate )))
28452922
28462923
28472924class Subscription (StripeObject ):
0 commit comments