@@ -434,13 +434,18 @@ def __init__(self, source=None, **kwargs):
434
434
self .tokenization_method = None
435
435
436
436
self .customer = None
437
+ self ._authenticated = False
437
438
438
439
@property
439
440
def last4 (self ):
440
441
return self ._card_number [- 4 :]
441
442
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 )
444
449
445
450
def _attaching_is_declined (self ):
446
451
return PaymentMethod ._attaching_is_declined (self )
@@ -1830,7 +1835,8 @@ class PaymentIntent(StripeObject):
1830
1835
1831
1836
def __init__ (self , amount = None , currency = None , customer = None ,
1832
1837
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 ):
1834
1840
if kwargs :
1835
1841
raise UserError (400 , 'Unexpected ' + ', ' .join (kwargs .keys ()))
1836
1842
@@ -1850,6 +1856,8 @@ def __init__(self, amount=None, currency=None, customer=None,
1850
1856
assert capture_method in ('automatic' ,
1851
1857
'automatic_async' ,
1852
1858
'manual' )
1859
+ if off_session is not None :
1860
+ assert type (off_session ) is bool
1853
1861
except AssertionError :
1854
1862
raise UserError (400 , 'Bad request' )
1855
1863
@@ -1872,6 +1880,7 @@ def __init__(self, amount=None, currency=None, customer=None,
1872
1880
self .invoice = None
1873
1881
self .next_action = None
1874
1882
self .capture_method = capture_method or 'automatic_async'
1883
+ self .off_session = off_session or False
1875
1884
1876
1885
self ._canceled = False
1877
1886
self ._authentication_failed = False
@@ -1963,7 +1972,7 @@ def _api_create(cls, confirm=None, off_session=None, **data):
1963
1972
except AssertionError :
1964
1973
raise UserError (400 , 'Bad request' )
1965
1974
1966
- obj = super ()._api_create (** data )
1975
+ obj = super ()._api_create (off_session = off_session , ** data )
1967
1976
1968
1977
if confirm :
1969
1978
obj ._confirm (on_failure_now = obj ._report_failure )
@@ -1995,7 +2004,7 @@ def _api_confirm(cls, id, payment_method=None, **kwargs):
1995
2004
def _confirm (self , on_failure_now ):
1996
2005
self ._authentication_failed = False
1997
2006
payment_method = PaymentMethod ._api_retrieve (self .payment_method )
1998
- if payment_method ._requires_authentication ( ):
2007
+ if payment_method ._payment_requires_authentication ( self . off_session ):
1999
2008
self .next_action = {
2000
2009
'type' : 'use_stripe_sdk' ,
2001
2010
'use_stripe_sdk' : {'type' : 'three_d_secure_redirect' ,
@@ -2151,11 +2160,30 @@ def __init__(self, type=None, billing_details=None, card=None,
2151
2160
2152
2161
self .customer = None
2153
2162
self .metadata = metadata or {}
2163
+ self ._authenticated = False
2154
2164
2155
- def _requires_authentication (self ):
2165
+ def _setup_requires_authentication (self , usage = None ):
2156
2166
if self .type == 'card' :
2157
- return self ._card_number in ('4000002500003155' ,
2158
- '4000002760003184' ,
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
2179
+
2180
+ def _payment_requires_authentication (self , off_session = False ):
2181
+ if self .type == 'card' :
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' ,
2159
2187
'4000008260003178' ,
2160
2188
'4000000000003220' ,
2161
2189
'4000000000003063' ,
@@ -2268,6 +2296,14 @@ def _try_get_canonical_test_article(cls, id):
2268
2296
exp_month = '12' ,
2269
2297
exp_year = '2030' ,
2270
2298
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' ))
2271
2307
2272
2308
@classmethod
2273
2309
def _api_list_all (cls , url , customer = None , type = None , limit = None ,
@@ -2607,6 +2643,8 @@ def __init__(self, charge=None, payment_intent=None, amount=None,
2607
2643
charge = payment_intent_obj .latest_charge .id
2608
2644
2609
2645
charge_obj = Charge ._api_retrieve (charge )
2646
+ if charge_obj .status == 'failed' :
2647
+ raise UserError (400 , 'Cannot refund a failed payment.' )
2610
2648
2611
2649
# All exceptions must be raised before this point.
2612
2650
super ().__init__ ()
@@ -2707,18 +2745,24 @@ def __init__(self, type=None, currency=None, owner=None, metadata=None,
2707
2745
'mandate_url' : 'https://fake/NXDSYREGC9PSMKWY' ,
2708
2746
}
2709
2747
2710
- def _requires_authentication (self ):
2711
- if self .type == 'sepa_debit' :
2712
- return PaymentMethod ._requires_authentication (self )
2748
+ def _setup_requires_authentication (self , usage = None ):
2749
+ if self .type in ('card' , 'sepa_debit' ):
2750
+ return PaymentMethod ._setup_requires_authentication (self , usage )
2751
+ return False
2752
+
2753
+ def _payment_requires_authentication (self , off_session = False ):
2754
+ if self .type in ('card' , 'sepa_debit' ):
2755
+ return PaymentMethod ._payment_requires_authentication (
2756
+ self , off_session )
2713
2757
return False
2714
2758
2715
2759
def _attaching_is_declined (self ):
2716
- if self .type == ' sepa_debit' :
2760
+ if self .type in ( 'card' , ' sepa_debit') :
2717
2761
return PaymentMethod ._attaching_is_declined (self )
2718
2762
return False
2719
2763
2720
2764
def _charging_is_declined (self ):
2721
- if self .type == ' sepa_debit' :
2765
+ if self .type in ( 'card' , ' sepa_debit') :
2722
2766
return PaymentMethod ._charging_is_declined (self )
2723
2767
return False
2724
2768
@@ -2804,7 +2848,7 @@ def _attach_pm(self, pm):
2804
2848
self .next_action = None
2805
2849
raise UserError (402 , 'Your card was declined.' ,
2806
2850
{'code' : 'card_declined' })
2807
- elif pm ._requires_authentication ( ):
2851
+ elif pm ._setup_requires_authentication ( self . usage ):
2808
2852
self .status = 'requires_action'
2809
2853
self .next_action = {'type' : 'use_stripe_sdk' ,
2810
2854
'use_stripe_sdk' : {
@@ -2836,10 +2880,45 @@ def _api_cancel(cls, id, use_stripe_sdk=None, client_secret=None,
2836
2880
obj .next_action = None
2837
2881
return obj
2838
2882
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
+
2839
2916
2840
2917
extra_apis .extend ((
2841
2918
('POST' , '/v1/setup_intents/{id}/confirm' , SetupIntent ._api_confirm ),
2842
- ('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 )))
2843
2922
2844
2923
2845
2924
class Subscription (StripeObject ):
0 commit comments