diff --git a/localstripe/resources.py b/localstripe/resources.py index 6c7b8b2..62067ef 100644 --- a/localstripe/resources.py +++ b/localstripe/resources.py @@ -2203,6 +2203,10 @@ def _api_detach(cls, id, **kwargs): @classmethod def _api_retrieve(cls, id): + obj = cls._try_get_canonical_test_article(id) + if obj: + return obj + # https://stripe.com/docs/payments/payment-methods#transitioning # You can retrieve all saved compatible payment instruments through the # Payment Methods API. @@ -2213,6 +2217,41 @@ def _api_retrieve(cls, id): return super()._api_retrieve(id) + @classmethod + def _try_get_canonical_test_article(cls, id): + """Convert special payment method IDs into payment method objects. + + See https://docs.stripe.com/testing?testing-method=payment-methods. + + Oddly, as we do here, Stripe will convert these special test IDs into + actual objects and store them on a GET request, meaning the GET has + side effects and is not idempotent.""" + + if id == 'pm_card_visa': + return PaymentMethod( + type='card', + card=dict( + number='4242424242424242', + exp_month='12', + exp_year='2030', + cvc='123')) + if id == 'pm_card_visa_chargeDeclined': + return PaymentMethod( + type='card', + card=dict( + number='4000000000000002', + exp_month='12', + exp_year='2030', + cvc='123')) + if id == 'pm_card_chargeCustomerFail': + return PaymentMethod( + type='card', + card=dict( + number='4000000000000341', + exp_month='12', + exp_year='2030', + cvc='123')) + @classmethod def _api_list_all(cls, url, customer=None, type=None, limit=None, starting_after=None): @@ -2704,7 +2743,7 @@ def __init__(self, customer=None, usage=None, payment_method_types=None, @classmethod def _api_confirm(cls, id, use_stripe_sdk=None, client_secret=None, - payment_method_data=None, **kwargs): + payment_method=None, payment_method_data=None, **kwargs): if kwargs: raise UserError(400, 'Unexpected ' + ', '.join(kwargs.keys())) @@ -2722,27 +2761,13 @@ def _api_confirm(cls, id, use_stripe_sdk=None, client_secret=None, if client_secret and client_secret != obj.client_secret: raise UserError(401, 'Unauthorized') - if payment_method_data: - if obj.payment_method is not None: - raise UserError(400, 'Bad request') - + if payment_method is not None: + assert isinstance(payment_method, str) + pm = PaymentMethod._api_retrieve(payment_method) + obj._attach_pm(pm) + elif payment_method_data is not None: pm = PaymentMethod(**payment_method_data) - obj.payment_method = pm.id - - if pm._attaching_is_declined(): - obj.status = 'canceled' - obj.next_action = None - raise UserError(402, 'Your card was declined.', - {'code': 'card_declined'}) - elif pm._requires_authentication(): - obj.status = 'requires_action' - obj.next_action = {'type': 'use_stripe_sdk', - 'use_stripe_sdk': { - 'type': 'three_d_secure_redirect', - 'stripe_js': ''}} - else: - obj.status = 'succeeded' - obj.next_action = None + obj._attach_pm(pm) elif obj.payment_method is None: obj.status = 'requires_payment_method' obj.next_action = None @@ -2751,6 +2776,25 @@ def _api_confirm(cls, id, use_stripe_sdk=None, client_secret=None, obj.next_action = None return obj + def _attach_pm(self, pm): + self.payment_method = pm.id + self.payment_method_types = [pm.type] + + if pm._attaching_is_declined(): + self.status = 'canceled' + self.next_action = None + raise UserError(402, 'Your card was declined.', + {'code': 'card_declined'}) + elif pm._requires_authentication(): + self.status = 'requires_action' + self.next_action = {'type': 'use_stripe_sdk', + 'use_stripe_sdk': { + 'type': 'three_d_secure_redirect', + 'stripe_js': ''}} + else: + self.status = 'succeeded' + self.next_action = None + @classmethod def _api_cancel(cls, id, use_stripe_sdk=None, client_secret=None, **kwargs): diff --git a/test.sh b/test.sh index 9ecd648..7bffac7 100755 --- a/test.sh +++ b/test.sh @@ -560,7 +560,7 @@ cus=$(curl -sSfg -u $SK: $HOST/v1/customers \ -d email=john.malkovich@example.com \ | grep -oE 'cus_\w+' | head -n 1) -pm=$(curl -sSfg -u $SK: $HOST/v1/payment_methods \ +pm_card_okay=$(curl -sSfg -u $SK: $HOST/v1/payment_methods \ -d type=card \ -d card[number]=4242424242424242 \ -d card[exp_month]=12 \ @@ -568,19 +568,19 @@ pm=$(curl -sSfg -u $SK: $HOST/v1/payment_methods \ -d card[cvc]=123 \ | grep -oE 'pm_\w+' | head -n 1) -curl -sSfg -u $SK: $HOST/v1/payment_methods/$pm/attach \ +curl -sSfg -u $SK: $HOST/v1/payment_methods/$pm_card_okay/attach \ -d customer=$cus curl -sSfg -u $SK: $HOST/v1/customers/$cus \ - -d invoice_settings[default_payment_method]=$pm + -d invoice_settings[default_payment_method]=$pm_card_okay curl -sSfg -u $SK: $HOST/v1/customers/$cus?expand[]=invoice_settings.default_payment_method curl -sSfg -u $SK: $HOST/v1/payment_methods?customer=$cus\&type=card -curl -sSfg -u $SK: $HOST/v1/payment_methods/$pm/detach -X POST +curl -sSfg -u $SK: $HOST/v1/payment_methods/$pm_card_okay/detach -X POST -pm=$(curl -sSfg -u $SK: $HOST/v1/payment_methods \ +pm_card_decline_on_attach=$(curl -sSfg -u $SK: $HOST/v1/payment_methods \ -d type=card \ -d card[number]=4000000000000002 \ -d card[exp_month]=4 \ @@ -588,7 +588,7 @@ pm=$(curl -sSfg -u $SK: $HOST/v1/payment_methods \ -d card[cvc]=123 \ | grep -oE 'pm_\w+' | head -n 1) code=$(curl -sg -o /dev/null -w "%{http_code}" -u $SK: \ - $HOST/v1/payment_methods/$pm/attach \ + $HOST/v1/payment_methods/$pm_card_decline_on_attach/attach \ -d customer=$cus) [ "$code" = 402 ] @@ -618,6 +618,53 @@ curl -sSfg $HOST/v1/setup_intents/$seti/confirm \ -d payment_method_data[card][exp_year]=24 \ -d payment_method_data[billing_details][address][postal_code]=42424 +# We can also pass a payment method ID to setup_intents/*/confirm: +res=$(curl -sSfg -u $SK: $HOST/v1/setup_intents -X POST) +seti=$(echo "$res" | grep '"id"' | grep -oE 'seti_\w+' | head -n 1) +seti_secret=$(echo $res | grep -oE 'seti_\w+_secret_\w+' | head -n 1) +status=$( + curl -sSfg $HOST/v1/setup_intents/$seti/confirm \ + -d key=pk_test_sldkjflaksdfj \ + -d client_secret=$seti_secret \ + -d payment_method=$pm_card_okay \ + | grep -oE '"status": "succeeded"') +[ -n "$status" ] + +# ... and payment method IDs on bad cards fail on setup_intents/*/confirm: +res=$(curl -sSfg -u $SK: $HOST/v1/setup_intents -X POST) +seti=$(echo "$res" | grep '"id"' | grep -oE 'seti_\w+' | head -n 1) +seti_secret=$(echo $res | grep -oE 'seti_\w+_secret_\w+' | head -n 1) +code=$( + curl -sg -w "%{http_code}" -o /dev/null $HOST/v1/setup_intents/$seti/confirm \ + -d key=pk_test_sldkjflaksdfj \ + -d client_secret=$seti_secret \ + -d payment_method=$pm_card_decline_on_attach) +[ "$code" = 402 ] + +# We can also pass a special well-known payment method ID to +# setup_intents/*/confirm: +res=$(curl -sSfg -u $SK: $HOST/v1/setup_intents -X POST) +seti=$(echo "$res" | grep '"id"' | grep -oE 'seti_\w+' | head -n 1) +seti_secret=$(echo $res | grep -oE 'seti_\w+_secret_\w+' | head -n 1) +status=$( + curl -sSfg $HOST/v1/setup_intents/$seti/confirm \ + -d key=pk_test_sldkjflaksdfj \ + -d client_secret=$seti_secret \ + -d payment_method=pm_card_visa \ + | grep -oE '"status": "succeeded"') +[ -n "$status" ] + +# ... including well-known bad payment method IDs: +res=$(curl -sSfg -u $SK: $HOST/v1/setup_intents -X POST) +seti=$(echo "$res" | grep '"id"' | grep -oE 'seti_\w+' | head -n 1) +seti_secret=$(echo $res | grep -oE 'seti_\w+_secret_\w+' | head -n 1) +code=$( + curl -sg -w "%{http_code}" -o /dev/null $HOST/v1/setup_intents/$seti/confirm \ + -d key=pk_test_sldkjflaksdfj \ + -d client_secret=$seti_secret \ + -d payment_method=pm_card_visa_chargeDeclined) +[ "$code" = 402 ] + # off_session cannot be used when confirm is false code=$( curl -sg -o /dev/null -w "%{http_code}" \