@@ -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 )
@@ -1115,6 +1120,47 @@ def _api_update(cls, id, **data):
1115
1120
def _api_delete (cls , id ):
1116
1121
raise UserError (405 , 'Method Not Allowed' )
1117
1122
1123
+ @classmethod
1124
+ def _api_list_all (cls , url , type = None , created = None , limit = None ,
1125
+ starting_after = None , ** kwargs ):
1126
+ if kwargs :
1127
+ raise UserError (400 , 'Unexpected ' + ', ' .join (kwargs .keys ()))
1128
+
1129
+ filters = []
1130
+ try :
1131
+ if type is not None :
1132
+ assert _type (type ) is str
1133
+ filters .append (lambda obj : obj .type == type )
1134
+ if created is not None :
1135
+ assert _type (created ) is dict
1136
+ gt = try_convert_to_int (created .pop ('gt' , None ))
1137
+ if gt is not None :
1138
+ filters .append (lambda obj : obj .created > gt )
1139
+
1140
+ gte = try_convert_to_int (created .pop ('gte' , None ))
1141
+ if gte is not None :
1142
+ filters .append (lambda obj : obj .created >= gte )
1143
+
1144
+ lt = try_convert_to_int (created .pop ('lt' , None ))
1145
+ if lt is not None :
1146
+ filters .append (lambda obj : obj .created < lt )
1147
+
1148
+ lte = try_convert_to_int (created .pop ('lte' , None ))
1149
+ if lte is not None :
1150
+ filters .append (lambda obj : obj .created <= lte )
1151
+
1152
+ assert not created # no other params are supported
1153
+ except AssertionError :
1154
+ raise UserError (400 , 'Bad request' )
1155
+
1156
+ li = super ()._api_list_all (
1157
+ url , limit = limit , starting_after = starting_after
1158
+ )
1159
+
1160
+ li ._list = [obj for obj in li ._list if all (f (obj ) for f in filters )]
1161
+
1162
+ return li
1163
+
1118
1164
1119
1165
class Invoice (StripeObject ):
1120
1166
object = 'invoice'
@@ -1830,7 +1876,8 @@ class PaymentIntent(StripeObject):
1830
1876
1831
1877
def __init__ (self , amount = None , currency = None , customer = None ,
1832
1878
payment_method = None , metadata = None , payment_method_types = None ,
1833
- capture_method = None , payment_method_options = None , ** kwargs ):
1879
+ capture_method = None , payment_method_options = None ,
1880
+ off_session = None , ** kwargs ):
1834
1881
if kwargs :
1835
1882
raise UserError (400 , 'Unexpected ' + ', ' .join (kwargs .keys ()))
1836
1883
@@ -1850,6 +1897,8 @@ def __init__(self, amount=None, currency=None, customer=None,
1850
1897
assert capture_method in ('automatic' ,
1851
1898
'automatic_async' ,
1852
1899
'manual' )
1900
+ if off_session is not None :
1901
+ assert type (off_session ) is bool
1853
1902
except AssertionError :
1854
1903
raise UserError (400 , 'Bad request' )
1855
1904
@@ -1872,23 +1921,27 @@ def __init__(self, amount=None, currency=None, customer=None,
1872
1921
self .invoice = None
1873
1922
self .next_action = None
1874
1923
self .capture_method = capture_method or 'automatic_async'
1924
+ self .off_session = off_session or False
1875
1925
1876
1926
self ._canceled = False
1877
1927
self ._authentication_failed = False
1878
1928
1879
1929
def _on_success (self ):
1930
+ schedule_webhook (Event ('payment_intent.succeeded' , self ))
1880
1931
if self .invoice :
1881
1932
invoice = Invoice ._api_retrieve (self .invoice )
1882
1933
invoice ._on_payment_success ()
1883
1934
1884
1935
def _report_failure (self ):
1936
+ schedule_webhook (Event ('payment_intent.payment_failed' , self ))
1885
1937
if self .invoice :
1886
1938
invoice = Invoice ._api_retrieve (self .invoice )
1887
1939
invoice ._on_payment_failure_now ()
1888
1940
1889
1941
self .latest_charge ._raise_failure ()
1890
1942
1891
- def _on_failure_later (self ):
1943
+ def _report_async_failure (self ):
1944
+ schedule_webhook (Event ('payment_intent.payment_failed' , self ))
1892
1945
if self .invoice :
1893
1946
invoice = Invoice ._api_retrieve (self .invoice )
1894
1947
invoice ._on_payment_failure_later ()
@@ -1905,7 +1958,7 @@ def _create_charge(self, on_failure_now):
1905
1958
charge .payment_intent = self .id
1906
1959
self .latest_charge = charge
1907
1960
charge ._initialize_charge (self ._on_success , on_failure_now ,
1908
- self ._on_failure_later )
1961
+ self ._report_async_failure )
1909
1962
1910
1963
@property
1911
1964
def status (self ):
@@ -1963,7 +2016,7 @@ def _api_create(cls, confirm=None, off_session=None, **data):
1963
2016
except AssertionError :
1964
2017
raise UserError (400 , 'Bad request' )
1965
2018
1966
- obj = super ()._api_create (** data )
2019
+ obj = super ()._api_create (off_session = off_session , ** data )
1967
2020
1968
2021
if confirm :
1969
2022
obj ._confirm (on_failure_now = obj ._report_failure )
@@ -1995,7 +2048,7 @@ def _api_confirm(cls, id, payment_method=None, **kwargs):
1995
2048
def _confirm (self , on_failure_now ):
1996
2049
self ._authentication_failed = False
1997
2050
payment_method = PaymentMethod ._api_retrieve (self .payment_method )
1998
- if payment_method ._requires_authentication ( ):
2051
+ if payment_method ._payment_requires_authentication ( self . off_session ):
1999
2052
self .next_action = {
2000
2053
'type' : 'use_stripe_sdk' ,
2001
2054
'use_stripe_sdk' : {'type' : 'three_d_secure_redirect' ,
@@ -2151,11 +2204,30 @@ def __init__(self, type=None, billing_details=None, card=None,
2151
2204
2152
2205
self .customer = None
2153
2206
self .metadata = metadata or {}
2207
+ self ._authenticated = False
2208
+
2209
+ def _setup_requires_authentication (self , usage = None ):
2210
+ if self .type == 'card' :
2211
+ if self ._card_number == '4000002500003155' :
2212
+ # For this card, if we're setting up a payment method for
2213
+ # off_session future payments, Stripe proactively forces
2214
+ # 3DS authentication at setup time:
2215
+ return usage == 'off_session'
2216
+
2217
+ return self ._card_number in ('4000002760003184' ,
2218
+ '4000008260003178' ,
2219
+ '4000000000003220' ,
2220
+ '4000000000003063' ,
2221
+ '4000008400001629' )
2222
+ return False
2154
2223
2155
- def _requires_authentication (self ):
2224
+ def _payment_requires_authentication (self , off_session = False ):
2156
2225
if self .type == 'card' :
2157
- return self ._card_number in ('4000002500003155' ,
2158
- '4000002760003184' ,
2226
+ if self ._card_number == '4000002500003155' :
2227
+ # See https://docs.stripe.com/testing#authentication-and-setup
2228
+ return not (off_session and self ._authenticated )
2229
+
2230
+ return self ._card_number in ('4000002760003184' ,
2159
2231
'4000008260003178' ,
2160
2232
'4000000000003220' ,
2161
2233
'4000000000003063' ,
@@ -2268,6 +2340,14 @@ def _try_get_canonical_test_article(cls, id):
2268
2340
exp_month = '12' ,
2269
2341
exp_year = '2030' ,
2270
2342
cvc = '123' ))
2343
+ if id == 'pm_card_authenticationRequiredOnSetup' :
2344
+ return PaymentMethod (
2345
+ type = 'card' ,
2346
+ card = dict (
2347
+ number = '4000002500003155' ,
2348
+ exp_month = '12' ,
2349
+ exp_year = '2030' ,
2350
+ cvc = '123' ))
2271
2351
2272
2352
@classmethod
2273
2353
def _api_list_all (cls , url , customer = None , type = None , limit = None ,
@@ -2607,6 +2687,8 @@ def __init__(self, charge=None, payment_intent=None, amount=None,
2607
2687
charge = payment_intent_obj .latest_charge .id
2608
2688
2609
2689
charge_obj = Charge ._api_retrieve (charge )
2690
+ if charge_obj .status == 'failed' :
2691
+ raise UserError (400 , 'Cannot refund a failed payment.' )
2610
2692
2611
2693
# All exceptions must be raised before this point.
2612
2694
super ().__init__ ()
@@ -2707,18 +2789,24 @@ def __init__(self, type=None, currency=None, owner=None, metadata=None,
2707
2789
'mandate_url' : 'https://fake/NXDSYREGC9PSMKWY' ,
2708
2790
}
2709
2791
2710
- def _requires_authentication (self ):
2711
- if self .type == 'sepa_debit' :
2712
- return PaymentMethod ._requires_authentication (self )
2792
+ def _setup_requires_authentication (self , usage = None ):
2793
+ if self .type in ('card' , 'sepa_debit' ):
2794
+ return PaymentMethod ._setup_requires_authentication (self , usage )
2795
+ return False
2796
+
2797
+ def _payment_requires_authentication (self , off_session = False ):
2798
+ if self .type in ('card' , 'sepa_debit' ):
2799
+ return PaymentMethod ._payment_requires_authentication (
2800
+ self , off_session )
2713
2801
return False
2714
2802
2715
2803
def _attaching_is_declined (self ):
2716
- if self .type == ' sepa_debit' :
2804
+ if self .type in ( 'card' , ' sepa_debit') :
2717
2805
return PaymentMethod ._attaching_is_declined (self )
2718
2806
return False
2719
2807
2720
2808
def _charging_is_declined (self ):
2721
- if self .type == ' sepa_debit' :
2809
+ if self .type in ( 'card' , ' sepa_debit') :
2722
2810
return PaymentMethod ._charging_is_declined (self )
2723
2811
return False
2724
2812
@@ -2804,7 +2892,7 @@ def _attach_pm(self, pm):
2804
2892
self .next_action = None
2805
2893
raise UserError (402 , 'Your card was declined.' ,
2806
2894
{'code' : 'card_declined' })
2807
- elif pm ._requires_authentication ( ):
2895
+ elif pm ._setup_requires_authentication ( self . usage ):
2808
2896
self .status = 'requires_action'
2809
2897
self .next_action = {'type' : 'use_stripe_sdk' ,
2810
2898
'use_stripe_sdk' : {
@@ -2836,10 +2924,45 @@ def _api_cancel(cls, id, use_stripe_sdk=None, client_secret=None,
2836
2924
obj .next_action = None
2837
2925
return obj
2838
2926
2927
+ @classmethod
2928
+ def _api_authenticate (cls , id , ** kwargs ):
2929
+ """This is a test-only endpoint to help test payment methods which
2930
+ require authentication during setup.
2931
+
2932
+ E.g., for credit cards which are subject to the 3D Secure protocol,
2933
+ when confirmed, SetupIntent may transition to the 'requires_action'
2934
+ status, with a 'next_action' indicating some flow that usually
2935
+ involves human interaction from the cardholder. This endpoint bypasses
2936
+ that required action for test purposes.
2937
+ """
2938
+
2939
+ if kwargs :
2940
+ raise UserError (400 , 'Unexpected ' + ', ' .join (kwargs .keys ()))
2941
+
2942
+ try :
2943
+ assert type (id ) is str and id .startswith ('seti_' )
2944
+ except AssertionError :
2945
+ raise UserError (400 , 'Bad request' )
2946
+
2947
+ obj = cls ._api_retrieve (id )
2948
+
2949
+ if obj .status != 'requires_action' :
2950
+ raise UserError (400 , 'Bad request' )
2951
+
2952
+ pm = PaymentMethod ._api_retrieve (obj .payment_method )
2953
+ pm ._authenticated = True
2954
+
2955
+ obj .status = 'succeeded'
2956
+ obj .next_action = None
2957
+
2958
+ return obj
2959
+
2839
2960
2840
2961
extra_apis .extend ((
2841
2962
('POST' , '/v1/setup_intents/{id}/confirm' , SetupIntent ._api_confirm ),
2842
- ('POST' , '/v1/setup_intents/{id}/cancel' , SetupIntent ._api_cancel )))
2963
+ ('POST' , '/v1/setup_intents/{id}/cancel' , SetupIntent ._api_cancel ),
2964
+ ('POST' , '/v1/setup_intents/{id}/_authenticate' ,
2965
+ SetupIntent ._api_authenticate )))
2843
2966
2844
2967
2845
2968
class Subscription (StripeObject ):
0 commit comments