Stripe checkout #104411

Merged
Anna Sirota merged 61 commits from stripe into main 2024-06-17 18:08:41 +02:00
5 changed files with 143 additions and 41 deletions
Showing only changes of commit 8271a8c0aa - Show all commits

View File

@ -0,0 +1,51 @@
responses:
- response:
auto_calculate_content_length: false
body: "{\n \"id\": \"cus_QGeKULCHd4p9o2\",\n \"object\": \"customer\",\n \"\
address\": null,\n \"balance\": 0,\n \"created\": 1718021356,\n \"currency\"\
: null,\n \"default_source\": null,\n \"delinquent\": false,\n \"description\"\
: null,\n \"discount\": null,\n \"email\": \"billing@example.com\",\n \"\
invoice_prefix\": \"0ABA8C57\",\n \"invoice_settings\": {\n \"custom_fields\"\
: null,\n \"default_payment_method\": null,\n \"footer\": null,\n \"\
rendering_options\": null\n },\n \"livemode\": false,\n \"metadata\": {},\n\
\ \"name\": \"\u0410\u043B\u0435\u043A\u0441\u0435\u0439 \u041D.\",\n \"phone\"\
: null,\n \"preferred_locales\": [],\n \"shipping\": null,\n \"tax_exempt\"\
: \"none\",\n \"test_clock\": null\n}"
content_type: text/plain; charset=utf-8
method: POST
status: 200
url: https://api.stripe.com/v1/customers
- response:
auto_calculate_content_length: false
body: "{\n \"id\": \"cs_test_c1QeSt36UcbmnmrXJnEYZpWakr377WPMfbWLeR9d3ZBYJPWXRUJ3TQ0UG9\"\
,\n \"object\": \"checkout.session\",\n \"after_expiration\": null,\n \"\
allow_promotion_codes\": null,\n \"amount_subtotal\": null,\n \"amount_total\"\
: null,\n \"automatic_tax\": {\n \"enabled\": false,\n \"liability\"\
: null,\n \"status\": null\n },\n \"billing_address_collection\": null,\n\
\ \"cancel_url\": \"http://testserver/settings/billing/\",\n \"client_reference_id\"\
: null,\n \"client_secret\": null,\n \"consent\": null,\n \"consent_collection\"\
: null,\n \"created\": 1718021357,\n \"currency\": null,\n \"currency_conversion\"\
: null,\n \"custom_fields\": [],\n \"custom_text\": {\n \"after_submit\"\
: null,\n \"shipping_address\": null,\n \"submit\": null,\n \"terms_of_service_acceptance\"\
: null\n },\n \"customer\": \"cus_QGeKULCHd4p9o2\",\n \"customer_creation\"\
: null,\n \"customer_details\": {\n \"address\": null,\n \"email\": \"\
billing@example.com\",\n \"name\": null,\n \"phone\": null,\n \"tax_exempt\"\
: null,\n \"tax_ids\": null\n },\n \"customer_email\": null,\n \"expires_at\"\
: 1718107757,\n \"invoice\": null,\n \"invoice_creation\": null,\n \"livemode\"\
: false,\n \"locale\": null,\n \"metadata\": {},\n \"mode\": \"setup\",\n\
\ \"payment_intent\": null,\n \"payment_link\": null,\n \"payment_method_collection\"\
: \"always\",\n \"payment_method_configuration_details\": null,\n \"payment_method_options\"\
: {\n \"card\": {\n \"request_three_d_secure\": \"automatic\"\n }\n\
\ },\n \"payment_method_types\": [\n \"card\",\n \"link\",\n \"paypal\"\
\n ],\n \"payment_status\": \"no_payment_required\",\n \"phone_number_collection\"\
: {\n \"enabled\": false\n },\n \"recovered_from\": null,\n \"saved_payment_method_options\"\
: null,\n \"setup_intent\": \"seti_1PQ72HE4KAUB5djsPthAKRdZ\",\n \"shipping_address_collection\"\
: null,\n \"shipping_cost\": null,\n \"shipping_details\": null,\n \"shipping_options\"\
: [],\n \"status\": \"open\",\n \"submit_type\": null,\n \"subscription\"\
: null,\n \"success_url\": \"http://testserver/subscription/1/payment-method/change/{CHECKOUT_SESSION_ID}/\"\
,\n \"total_details\": null,\n \"ui_mode\": \"hosted\",\n \"url\": \"https://checkout.stripe.com/c/pay/cs_test_c1QeSt36UcbmnmrXJnEYZpWakr377WPMfbWLeR9d3ZBYJPWXRUJ3TQ0UG9#fidkdWxOYHwnPyd1blpxYHZxWjA0VUpnNzNAMU5EUEcwYW92dWIwclxRMzQ8ZkxsUDRET2dPbTVidnBCNEJTdlBJQTRJYFF2c09BMEFBdlxVT19USGpWSXRSXFJwdm5UQXRpdVw2Rmp%2FZ11NNTU3fHdHUl1JTCcpJ2N3amhWYHdzYHcnP3F3cGApJ2lkfGpwcVF8dWAnPyd2bGtiaWBaZmppcGhrJyknYGtkZ2lgVWlkZmBtamlhYHd2Jz9xd3BgeCUl\"\
\n}"
content_type: text/plain
method: POST
status: 200
url: https://api.stripe.com/v1/checkout/sessions

View File

@ -0,0 +1,67 @@
responses:
- response:
auto_calculate_content_length: false
body: "{\n \"id\": \"cs_test_c1QeSt36UcbmnmrXJnEYZpWakr377WPMfbWLeR9d3ZBYJPWXRUJ3TQ0UG9\"\
,\n \"object\": \"checkout.session\",\n \"after_expiration\": null,\n \"\
allow_promotion_codes\": null,\n \"amount_subtotal\": null,\n \"amount_total\"\
: null,\n \"automatic_tax\": {\n \"enabled\": false,\n \"liability\"\
: null,\n \"status\": null\n },\n \"billing_address_collection\": null,\n\
\ \"cancel_url\": \"http://testserver/settings/billing/\",\n \"client_reference_id\"\
: null,\n \"client_secret\": null,\n \"consent\": null,\n \"consent_collection\"\
: null,\n \"created\": 1718021357,\n \"currency\": null,\n \"currency_conversion\"\
: null,\n \"custom_fields\": [],\n \"custom_text\": {\n \"after_submit\"\
: null,\n \"shipping_address\": null,\n \"submit\": null,\n \"terms_of_service_acceptance\"\
: null\n },\n \"customer\": \"cus_QGeKULCHd4p9o2\",\n \"customer_creation\"\
: null,\n \"customer_details\": {\n \"address\": null,\n \"email\": \"\
billing@example.com\",\n \"name\": \"\u0410\u043B\u0435\u043A\u0441\u0435\
\u0439 \u041D.\",\n \"phone\": null,\n \"tax_exempt\": null,\n \"tax_ids\"\
: []\n },\n \"customer_email\": null,\n \"expires_at\": 1718107757,\n \"\
invoice\": null,\n \"invoice_creation\": null,\n \"livemode\": false,\n \"\
locale\": null,\n \"metadata\": {},\n \"mode\": \"setup\",\n \"payment_intent\"\
: null,\n \"payment_link\": null,\n \"payment_method_collection\": \"always\"\
,\n \"payment_method_configuration_details\": null,\n \"payment_method_options\"\
: {\n \"card\": {\n \"request_three_d_secure\": \"automatic\"\n }\n\
\ },\n \"payment_method_types\": [\n \"card\",\n \"link\",\n \"paypal\"\
\n ],\n \"payment_status\": \"no_payment_required\",\n \"phone_number_collection\"\
: {\n \"enabled\": false\n },\n \"recovered_from\": null,\n \"saved_payment_method_options\"\
: null,\n \"setup_intent\": {\n \"id\": \"seti_1PQ72HE4KAUB5djsPthAKRdZ\"\
,\n \"object\": \"setup_intent\",\n \"application\": null,\n \"automatic_payment_methods\"\
: null,\n \"cancellation_reason\": null,\n \"client_secret\": \"seti_1PQ72HE4KAUB5djsPthAKRdZ_secret_QGeKD74KJNAouKtjbevE1O3dwVKxxt7\"\
,\n \"created\": 1718021357,\n \"customer\": \"cus_QGeKULCHd4p9o2\",\n\
\ \"description\": null,\n \"flow_directions\": null,\n \"last_setup_error\"\
: null,\n \"latest_attempt\": \"setatt_1PQ7DuE4KAUB5djsZtPVk4XB\",\n \"\
livemode\": false,\n \"mandate\": null,\n \"metadata\": {\n \"subscription_id\"\
: \"1\"\n },\n \"next_action\": null,\n \"on_behalf_of\": null,\n \
\ \"payment_method\": {\n \"id\": \"pm_1PQ7DsE4KAUB5djsw4z0K5PS\",\n\
\ \"object\": \"payment_method\",\n \"allow_redisplay\": \"always\"\
,\n \"billing_details\": {\n \"address\": {\n \"city\"\
: null,\n \"country\": \"NL\",\n \"line1\": null,\n \
\ \"line2\": null,\n \"postal_code\": null,\n \"state\"\
: null\n },\n \"email\": \"billing@example.com\",\n \"\
name\": \"John Smith\",\n \"phone\": null\n },\n \"card\":\
\ {\n \"brand\": \"visa\",\n \"checks\": {\n \"address_line1_check\"\
: null,\n \"address_postal_code_check\": null,\n \"cvc_check\"\
: \"pass\"\n },\n \"country\": \"US\",\n \"display_brand\"\
: \"visa\",\n \"exp_month\": 12,\n \"exp_year\": 2033,\n \
\ \"fingerprint\": \"YcmpGi38fZZuBsh4\",\n \"funding\": \"credit\"\
,\n \"generated_from\": null,\n \"last4\": \"4242\",\n \
\ \"networks\": {\n \"available\": [\n \"visa\"\n \
\ ],\n \"preferred\": null\n },\n \"three_d_secure_usage\"\
: {\n \"supported\": true\n },\n \"wallet\": null\n \
\ },\n \"created\": 1718022076,\n \"customer\": \"cus_QGeKULCHd4p9o2\"\
,\n \"livemode\": false,\n \"metadata\": {},\n \"type\": \"card\"\
\n },\n \"payment_method_configuration_details\": null,\n \"payment_method_options\"\
: {\n \"card\": {\n \"mandate_options\": null,\n \"network\"\
: null,\n \"request_three_d_secure\": \"automatic\"\n }\n },\n\
\ \"payment_method_types\": [\n \"card\",\n \"link\",\n \"\
paypal\"\n ],\n \"single_use_mandate\": null,\n \"status\": \"succeeded\"\
,\n \"usage\": \"off_session\"\n },\n \"shipping_address_collection\":\
\ null,\n \"shipping_cost\": null,\n \"shipping_details\": null,\n \"shipping_options\"\
: [],\n \"status\": \"complete\",\n \"submit_type\": null,\n \"subscription\"\
: null,\n \"success_url\": \"http://testserver/subscription/1/payment-method/change/{CHECKOUT_SESSION_ID}/\"\
,\n \"total_details\": null,\n \"ui_mode\": \"hosted\",\n \"url\": null\n\
}"
content_type: text/plain; charset=utf-8
method: GET
status: 200
url: https://api.stripe.com/v1/checkout/sessions/cs_test_c1QeSt36UcbmnmrXJnEYZpWakr377WPMfbWLeR9d3ZBYJPWXRUJ3TQ0UG9?expand%5B0%5D=setup_intent&expand%5B1%5D=setup_intent.payment_method

View File

@ -21,6 +21,7 @@ import subscriptions.tasks
import users.tasks import users.tasks
import users.tests.util as util import users.tests.util as util
responses_dir = 'subscriptions/tests/_responses/'
required_address_data = { required_address_data = {
'country': 'NL', 'country': 'NL',
'email': 'my.billing.email@example.com', 'email': 'my.billing.email@example.com',
@ -176,7 +177,7 @@ class TestGETJoinView(BaseSubscriptionTestCase):
class TestPOSTJoinView(BaseSubscriptionTestCase): class TestPOSTJoinView(BaseSubscriptionTestCase):
url_usd = reverse('subscriptions:join-billing-details', kwargs={'plan_variation_id': 1}) url_usd = reverse('subscriptions:join-billing-details', kwargs={'plan_variation_id': 1})
url = reverse('subscriptions:join-billing-details', kwargs={'plan_variation_id': 2}) url = reverse('subscriptions:join-billing-details', kwargs={'plan_variation_id': 2})
responses._add_from_file('stripe_create_checkout_session.yaml') responses._add_from_file(f'{responses_dir}stripe_create_checkout_session.yaml')
def test_post_updates_billing_address_and_customer_renders_next_form_de(self): def test_post_updates_billing_address_and_customer_renders_next_form_de(self):
customer = create_customer_with_billing_address(vat_number='', country='DE') customer = create_customer_with_billing_address(vat_number='', country='DE')

View File

@ -5,11 +5,15 @@ from django.urls import reverse
from looper.models import PaymentMethod, PaymentMethodAuthentication, Gateway from looper.models import PaymentMethod, PaymentMethodAuthentication, Gateway
from looper.money import Money from looper.money import Money
from looper.tests.factories import SubscriptionFactory from looper.tests.factories import SubscriptionFactory
import responses
# from responses import _recorder
from common.tests.factories.users import UserFactory from common.tests.factories.users import UserFactory
from subscriptions.tests.base import BaseSubscriptionTestCase from subscriptions.tests.base import BaseSubscriptionTestCase
import subscriptions.tasks import subscriptions.tasks
responses_dir = 'subscriptions/tests/_responses/'
required_address_data = { required_address_data = {
'country': 'NL', 'country': 'NL',
'email': 'my.billing.email@example.com', 'email': 'my.billing.email@example.com',
@ -99,6 +103,8 @@ class TestSubscriptionSettingsChangePaymentMethod(BaseSubscriptionTestCase):
url_name = 'subscriptions:payment-method-change' url_name = 'subscriptions:payment-method-change'
success_url_name = 'user-settings-billing' success_url_name = 'user-settings-billing'
# @_recorder.record(file_path=f'{responses_dir}stripe_create_checkout_session_setup.yaml')
# @_recorder.record(file_path=f'{responses_dir}stripe_retrieve_checkout_session_setup.yaml')
def test_can_change_payment_method_from_bank_to_credit_card_with_sca(self): def test_can_change_payment_method_from_bank_to_credit_card_with_sca(self):
bank = Gateway.objects.get(name='bank') bank = Gateway.objects.get(name='bank')
subscription = SubscriptionFactory( subscription = SubscriptionFactory(
@ -112,15 +118,23 @@ class TestSubscriptionSettingsChangePaymentMethod(BaseSubscriptionTestCase):
self.client.force_login(self.user) self.client.force_login(self.user)
url = reverse(self.url_name, kwargs={'subscription_id': subscription.pk}) url = reverse(self.url_name, kwargs={'subscription_id': subscription.pk})
data = { with responses.RequestsMock() as rsps:
**self.shared_payment_form_data, rsps._add_from_file(f'{responses_dir}stripe_create_checkout_session_setup.yaml')
'gateway': 'braintree', response = self.client.post(url)
'payment_method_nonce': 'fake-three-d-secure-visa-full-authentication-nonce',
}
response = self.client.post(url, data=data)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response['Location'], reverse(self.success_url_name)) expected_redirect_url = 'https://checkout.stripe.com/c/pay/cs_test_c1QeSt36UcbmnmrXJnEYZpWakr377WPMfbWLeR9d3ZBYJPWXRUJ3TQ0UG9#fidkdWxOYHwnPyd1blpxYHZxWjA0VUpnNzNAMU5EUEcwYW92dWIwclxRMzQ8ZkxsUDRET2dPbTVidnBCNEJTdlBJQTRJYFF2c09BMEFBdlxVT19USGpWSXRSXFJwdm5UQXRpdVw2Rmp%2FZ11NNTU3fHdHUl1JTCcpJ2N3amhWYHdzYHcnP3F3cGApJ2lkfGpwcVF8dWAnPyd2bGtiaWBaZmppcGhrJyknYGtkZ2lgVWlkZmBtamlhYHd2Jz9xd3BgeCUl'
self.assertEqual(response['Location'], expected_redirect_url, response['Location'])
# Pretend that checkout session was completed and we've returned to the success page with its ID:
checkout_session_id = 'cs_test_c1QeSt36UcbmnmrXJnEYZpWakr377WPMfbWLeR9d3ZBYJPWXRUJ3TQ0UG9'
success_url = url + f'{checkout_session_id}/'
with responses.RequestsMock() as rsps:
rsps._add_from_file(f'{responses_dir}stripe_retrieve_checkout_session_setup.yaml')
response = self.client.get(success_url)
self.assertEqual(response.status_code, 302)
self.assertEqual(response['Location'], '/subscription/1/manage/')
# New payment method was created # New payment method was created
self.assertEqual(PaymentMethod.objects.count(), 2) self.assertEqual(PaymentMethod.objects.count(), 2)
@ -130,40 +144,9 @@ class TestSubscriptionSettingsChangePaymentMethod(BaseSubscriptionTestCase):
self.assertNotEqual(subscription.payment_method_id, payment_method.pk) self.assertNotEqual(subscription.payment_method_id, payment_method.pk)
self.assertEqual( self.assertEqual(
str(subscription.payment_method), str(subscription.payment_method),
'Visa credit card ending in 0002', 'visa credit card ending in 4242',
) )
# SCA was stored # SCA isn't stored for Stripe payment methods
self.assertIsNotNone(PaymentMethodAuthentication.objects.first())
def test_can_change_payment_method_from_credit_card_to_bank(self):
braintree = Gateway.objects.get(name='braintree')
subscription = SubscriptionFactory(
customer=self.user.customer,
payment_method__customer_id=self.user.customer.pk,
payment_method__gateway=braintree,
)
self.assertEqual(PaymentMethod.objects.count(), 1)
payment_method = subscription.payment_method
self.client.force_login(self.user)
url = reverse(self.url_name, kwargs={'subscription_id': subscription.pk})
data = {
**self.shared_payment_form_data,
'gateway': 'bank',
'payment_method_nonce': 'unused',
}
response = self.client.post(url, data=data)
self.assertEqual(response.status_code, 302)
self.assertEqual(response['Location'], reverse(self.success_url_name))
# New payment method was created
self.assertEqual(PaymentMethod.objects.count(), 2)
subscription.refresh_from_db()
subscription.payment_method.refresh_from_db()
# Subscription's payment method was changed to bank transfer
self.assertNotEqual(subscription.payment_method_id, payment_method.pk)
self.assertEqual(str(subscription.payment_method), 'Bank Transfer')
self.assertIsNone(PaymentMethodAuthentication.objects.first()) self.assertIsNone(PaymentMethodAuthentication.objects.first())