@@ -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 ):
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 )
@@ -2001,7 +2010,7 @@ def _api_confirm(cls, id, payment_method=None, client_secret=None,
20012010 def _confirm (self , on_failure_now ):
20022011 self ._authentication_failed = False
20032012 payment_method = PaymentMethod ._api_retrieve (self .payment_method )
2004- if payment_method ._requires_authentication ( ):
2013+ if payment_method ._payment_requires_authentication ( self . off_session ):
20052014 self .next_action = {
20062015 'type' : 'use_stripe_sdk' ,
20072016 'use_stripe_sdk' : {'type' : 'three_d_secure_redirect' ,
@@ -2157,11 +2166,30 @@ def __init__(self, type=None, billing_details=None, card=None,
21572166
21582167 self .customer = None
21592168 self .metadata = metadata or {}
2169+ self ._authenticated = False
2170+
2171+ def _setup_requires_authentication (self , usage = None ):
2172+ if self .type == 'card' :
2173+ if self ._card_number == '4000002500003155' :
2174+ # For this card, if we're setting up a payment method for
2175+ # off_session future payments, Stripe proactively forces
2176+ # 3DS authentication at setup time:
2177+ return usage == 'off_session'
2178+
2179+ return self ._card_number in ('4000002760003184' ,
2180+ '4000008260003178' ,
2181+ '4000000000003220' ,
2182+ '4000000000003063' ,
2183+ '4000008400001629' )
2184+ return False
21602185
2161- def _requires_authentication (self ):
2186+ def _payment_requires_authentication (self , off_session ):
21622187 if self .type == 'card' :
2163- return self ._card_number in ('4000002500003155' ,
2164- '4000002760003184' ,
2188+ if self ._card_number == '4000002500003155' :
2189+ # See https://docs.stripe.com/testing#authentication-and-setup
2190+ return not (off_session and self ._authenticated )
2191+
2192+ return self ._card_number in ('4000002760003184' ,
21652193 '4000008260003178' ,
21662194 '4000000000003220' ,
21672195 '4000000000003063' ,
@@ -2274,6 +2302,14 @@ def _try_get_canonical_test_article(cls, id):
22742302 exp_month = '12' ,
22752303 exp_year = '2030' ,
22762304 cvc = '123' ))
2305+ if id == 'pm_card_authenticationRequiredOnSetup' :
2306+ return PaymentMethod (
2307+ type = 'card' ,
2308+ card = dict (
2309+ number = '4000002500003155' ,
2310+ exp_month = '12' ,
2311+ exp_year = '2030' ,
2312+ cvc = '123' ))
22772313
22782314 @classmethod
22792315 def _api_list_all (cls , url , customer = None , type = None , limit = None ,
@@ -2715,9 +2751,15 @@ def __init__(self, type=None, currency=None, owner=None, metadata=None,
27152751 'mandate_url' : 'https://fake/NXDSYREGC9PSMKWY' ,
27162752 }
27172753
2718- def _requires_authentication (self ):
2754+ def _setup_requires_authentication (self , usage = None ):
2755+ if self .type == 'sepa_debit' :
2756+ return PaymentMethod ._setup_requires_authentication (self , usage )
2757+ return False
2758+
2759+ def _payment_requires_authentication (self , off_session ):
27192760 if self .type == 'sepa_debit' :
2720- return PaymentMethod ._requires_authentication (self )
2761+ return PaymentMethod ._payment_requires_authentication (
2762+ self , off_session )
27212763 return False
27222764
27232765 def _attaching_is_declined (self ):
@@ -2812,7 +2854,7 @@ def _attach_pm(self, pm):
28122854 self .next_action = None
28132855 raise UserError (402 , 'Your card was declined.' ,
28142856 {'code' : 'card_declined' })
2815- elif pm ._requires_authentication ( ):
2857+ elif pm ._setup_requires_authentication ( self . usage ):
28162858 self .status = 'requires_action'
28172859 self .next_action = {'type' : 'use_stripe_sdk' ,
28182860 'use_stripe_sdk' : {
@@ -2844,10 +2886,49 @@ def _api_cancel(cls, id, use_stripe_sdk=None, client_secret=None,
28442886 obj .next_action = None
28452887 return obj
28462888
2889+ @classmethod
2890+ def _api_authenticate (cls , id , success = False , ** kwargs ):
2891+ """This is a test-only endpoint to help test payment methods which
2892+ require authentication during setup.
2893+
2894+ E.g., for credit cards which are subject to the 3D Secure protocol,
2895+ when confirmed, SetupIntent may transition to the 'requires_action'
2896+ status, with a 'next_action' indicating some flow that usually
2897+ involves human interaction from the cardholder. This endpoint bypasses
2898+ that required action for test purposes.
2899+ """
2900+
2901+ if kwargs :
2902+ raise UserError (400 , 'Unexpected ' + ', ' .join (kwargs .keys ()))
2903+
2904+ success = try_convert_to_bool (success )
2905+ try :
2906+ assert type (id ) is str and id .startswith ('seti_' )
2907+ assert type (success ) is bool
2908+ except AssertionError :
2909+ raise UserError (400 , 'Bad request' )
2910+
2911+ obj = cls ._api_retrieve (id )
2912+
2913+ if obj .status != 'requires_action' :
2914+ raise UserError (400 , 'Bad request' )
2915+
2916+ pm = PaymentMethod ._api_retrieve (obj .payment_method )
2917+
2918+ if success :
2919+ pm ._authenticated = True
2920+
2921+ obj .status = 'succeeded'
2922+ obj .next_action = None
2923+
2924+ return obj
2925+
28472926
28482927extra_apis .extend ((
28492928 ('POST' , '/v1/setup_intents/{id}/confirm' , SetupIntent ._api_confirm ),
2850- ('POST' , '/v1/setup_intents/{id}/cancel' , SetupIntent ._api_cancel )))
2929+ ('POST' , '/v1/setup_intents/{id}/cancel' , SetupIntent ._api_cancel ),
2930+ ('POST' , '/v1/setup_intents/{id}/_authenticate' ,
2931+ SetupIntent ._api_authenticate )))
28512932
28522933
28532934class Subscription (StripeObject ):
0 commit comments