Stripe checkout #104411
@ -508,7 +508,7 @@ GATEWAYS = {
|
||||
'api_publishable_key': _get('STRIPE_API_PUBLISHABLE_KEY'),
|
||||
'api_secret_key': _get('STRIPE_API_SECRET_KEY'),
|
||||
'endpoint_secret': _get('STRIPE_ENDPOINT_SECRET'),
|
||||
'supported_collection_methods': {'automatic'},
|
||||
'supported_collection_methods': {'automatic', 'manual'},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -161,14 +161,10 @@ class PaymentForm(BillingAddressForm):
|
||||
)
|
||||
)
|
||||
|
||||
# These are used when a payment fails, so that the next attempt to pay can reuse
|
||||
# the already-created subscription and order.
|
||||
subscription_pk = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
order_pk = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Pre-fill additional initial data from request."""
|
||||
self.request = kwargs.pop('request', None)
|
||||
self.plan_variation = kwargs.pop('plan_variation', None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@ -190,6 +186,16 @@ class PaymentForm(BillingAddressForm):
|
||||
if self.request.user.full_name:
|
||||
self.initial['full_name'] = self.request.user.full_name
|
||||
|
||||
def clean_gateway(self):
|
||||
"""Validate gateway against selected plan variation."""
|
||||
gw = self.cleaned_data['gateway']
|
||||
if not self.plan_variation:
|
||||
return gw
|
||||
if self.plan_variation.collection_method not in gw.provider.supported_collection_methods:
|
||||
msg = self.fields['gateway'].default_error_messages['invalid_choice']
|
||||
self.add_error('gateway', msg)
|
||||
return gw
|
||||
|
||||
|
||||
class SelectPlanVariationForm(forms.Form):
|
||||
"""Form used in the plan selector."""
|
||||
|
158
subscriptions/tests/_responses/stripe_get_cs_eur.yaml
Normal file
158
subscriptions/tests/_responses/stripe_get_cs_eur.yaml
Normal file
@ -0,0 +1,158 @@
|
||||
responses:
|
||||
- response:
|
||||
auto_calculate_content_length: false
|
||||
body: "{\n \"id\": \"cs_test_a19GMh7hJXYbh9OhEln16y1M8hfrdu0ySIpjRX4HQJqSVvpe9a9UP30bWW\"\
|
||||
,\n \"object\": \"checkout.session\",\n \"after_expiration\": null,\n \"\
|
||||
allow_promotion_codes\": null,\n \"amount_subtotal\": 1252,\n \"amount_total\"\
|
||||
: 1252,\n \"automatic_tax\": {\n \"enabled\": false,\n \"liability\"\
|
||||
: null,\n \"status\": null\n },\n \"billing_address_collection\": null,\n\
|
||||
\ \"cancel_url\": \"http://testserver/join/plan-variation/10/billing/\",\n\
|
||||
\ \"client_reference_id\": null,\n \"client_secret\": null,\n \"consent\"\
|
||||
: null,\n \"consent_collection\": null,\n \"created\": 1718354548,\n \"currency\"\
|
||||
: \"eur\",\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_QI5uhaeNfeXwYP\",\n \"customer_creation\": null,\n \"customer_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\": \"my.billing.email@example.com\"\
|
||||
,\n \"name\": \"New Full Name\",\n \"phone\": null,\n \"tax_exempt\"\
|
||||
: \"none\",\n \"tax_ids\": []\n },\n \"customer_email\": null,\n \"expires_at\"\
|
||||
: 1718440948,\n \"invoice\": null,\n \"invoice_creation\": {\n \"enabled\"\
|
||||
: false,\n \"invoice_data\": {\n \"account_tax_ids\": null,\n \"\
|
||||
custom_fields\": null,\n \"description\": null,\n \"footer\": null,\n\
|
||||
\ \"issuer\": null,\n \"metadata\": {},\n \"rendering_options\"\
|
||||
: null\n }\n },\n \"livemode\": false,\n \"locale\": null,\n \"metadata\"\
|
||||
: {},\n \"mode\": \"payment\",\n \"payment_intent\": {\n \"id\": \"pi_3PRVj3E4KAUB5djs1xbsGdDP\"\
|
||||
,\n \"object\": \"payment_intent\",\n \"amount\": 1252,\n \"amount_capturable\"\
|
||||
: 0,\n \"amount_details\": {\n \"tip\": {}\n },\n \"amount_received\"\
|
||||
: 1252,\n \"application\": null,\n \"application_fee_amount\": null,\n\
|
||||
\ \"automatic_payment_methods\": null,\n \"canceled_at\": null,\n \"\
|
||||
cancellation_reason\": null,\n \"capture_method\": \"automatic\",\n \"\
|
||||
client_secret\": \"pi_3PRVj3E4KAUB5djs1xbsGdDP_secret_BClI8ssVIUSjybh2UvIeFa6v0\"\
|
||||
,\n \"confirmation_method\": \"automatic\",\n \"created\": 1718354593,\n\
|
||||
\ \"currency\": \"eur\",\n \"customer\": {\n \"id\": \"cus_QI5uhaeNfeXwYP\"\
|
||||
,\n \"object\": \"customer\",\n \"address\": null,\n \"balance\"\
|
||||
: 0,\n \"created\": 1718354548,\n \"currency\": null,\n \"default_source\"\
|
||||
: null,\n \"delinquent\": false,\n \"description\": null,\n \"\
|
||||
discount\": null,\n \"email\": \"my.billing.email@example.com\",\n \
|
||||
\ \"invoice_prefix\": \"8D38744D\",\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\": \"New Full\
|
||||
\ Name\",\n \"phone\": null,\n \"preferred_locales\": [],\n \"\
|
||||
shipping\": null,\n \"tax_exempt\": \"none\",\n \"test_clock\": null\n\
|
||||
\ },\n \"description\": null,\n \"invoice\": null,\n \"last_payment_error\"\
|
||||
: null,\n \"latest_charge\": {\n \"id\": \"ch_3PRVj3E4KAUB5djs16uXWpi8\"\
|
||||
,\n \"object\": \"charge\",\n \"amount\": 1252,\n \"amount_captured\"\
|
||||
: 1252,\n \"amount_refunded\": 0,\n \"application\": null,\n \
|
||||
\ \"application_fee\": null,\n \"application_fee_amount\": null,\n \
|
||||
\ \"balance_transaction\": \"txn_3PRVj3E4KAUB5djs1sxkERoM\",\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\": \"my.billing.email@example.com\",\n \"name\": \"Jane Doe\",\n\
|
||||
\ \"phone\": null\n },\n \"calculated_statement_descriptor\"\
|
||||
: \"BLENDER STUDIO\",\n \"captured\": true,\n \"created\": 1718354593,\n\
|
||||
\ \"currency\": \"eur\",\n \"customer\": \"cus_QI5uhaeNfeXwYP\",\n\
|
||||
\ \"description\": null,\n \"destination\": null,\n \"dispute\"\
|
||||
: null,\n \"disputed\": false,\n \"failure_balance_transaction\":\
|
||||
\ null,\n \"failure_code\": null,\n \"failure_message\": null,\n \
|
||||
\ \"fraud_details\": {},\n \"invoice\": null,\n \"livemode\":\
|
||||
\ false,\n \"metadata\": {\n \"order_id\": \"1\"\n },\n \
|
||||
\ \"on_behalf_of\": null,\n \"order\": null,\n \"outcome\": {\n\
|
||||
\ \"network_status\": \"approved_by_network\",\n \"reason\": null,\n\
|
||||
\ \"risk_level\": \"normal\",\n \"risk_score\": 16,\n \"\
|
||||
seller_message\": \"Payment complete.\",\n \"type\": \"authorized\"\n\
|
||||
\ },\n \"paid\": true,\n \"payment_intent\": \"pi_3PRVj3E4KAUB5djs1xbsGdDP\"\
|
||||
,\n \"payment_method\": \"pm_1PRVj2E4KAUB5djsNQr0k105\",\n \"payment_method_details\"\
|
||||
: {\n \"card\": {\n \"amount_authorized\": 1252,\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 \"exp_month\"\
|
||||
: 12,\n \"exp_year\": 2033,\n \"extended_authorization\":\
|
||||
\ {\n \"status\": \"disabled\"\n },\n \"fingerprint\"\
|
||||
: \"YcmpGi38fZZuBsh4\",\n \"funding\": \"credit\",\n \"incremental_authorization\"\
|
||||
: {\n \"status\": \"unavailable\"\n },\n \"installments\"\
|
||||
: null,\n \"last4\": \"4242\",\n \"mandate\": null,\n \
|
||||
\ \"multicapture\": {\n \"status\": \"unavailable\"\n \
|
||||
\ },\n \"network\": \"visa\",\n \"network_token\": {\n\
|
||||
\ \"used\": false\n },\n \"overcapture\": {\n \
|
||||
\ \"maximum_amount_capturable\": 1252,\n \"status\": \"\
|
||||
unavailable\"\n },\n \"three_d_secure\": null,\n \
|
||||
\ \"wallet\": null\n },\n \"type\": \"card\"\n },\n \
|
||||
\ \"radar_options\": {},\n \"receipt_email\": null,\n \"receipt_number\"\
|
||||
: null,\n \"receipt_url\": \"https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xUE9iMjZFNEtBVUI1ZGpzKPOJsLMGMgbH3ZicbJc6LBYdfpRMBgTew5GCKCPV-K4DC44oir_sd03RTaqsJpsM5qstpEWJU0oqii03\"\
|
||||
,\n \"refunded\": false,\n \"review\": null,\n \"shipping\":\
|
||||
\ null,\n \"source\": null,\n \"source_transfer\": null,\n \"\
|
||||
statement_descriptor\": null,\n \"statement_descriptor_suffix\": null,\n\
|
||||
\ \"status\": \"succeeded\",\n \"transfer_data\": null,\n \"\
|
||||
transfer_group\": null\n },\n \"livemode\": false,\n \"metadata\":\
|
||||
\ {\n \"order_id\": \"1\"\n },\n \"next_action\": null,\n \"on_behalf_of\"\
|
||||
: null,\n \"payment_method\": {\n \"id\": \"pm_1PRVj2E4KAUB5djsNQr0k105\"\
|
||||
,\n \"object\": \"payment_method\",\n \"allow_redisplay\": \"limited\"\
|
||||
,\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\": \"my.billing.email@example.com\",\n \
|
||||
\ \"name\": \"Jane Doe\",\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\": 1718354592,\n\
|
||||
\ \"customer\": \"cus_QI5uhaeNfeXwYP\",\n \"livemode\": false,\n \
|
||||
\ \"metadata\": {},\n \"type\": \"card\"\n },\n \"payment_method_configuration_details\"\
|
||||
: null,\n \"payment_method_options\": {\n \"card\": {\n \"installments\"\
|
||||
: null,\n \"mandate_options\": null,\n \"network\": null,\n \
|
||||
\ \"request_three_d_secure\": \"automatic\"\n }\n },\n \"payment_method_types\"\
|
||||
: [\n \"card\"\n ],\n \"processing\": null,\n \"receipt_email\"\
|
||||
: null,\n \"review\": null,\n \"setup_future_usage\": \"off_session\"\
|
||||
,\n \"shipping\": null,\n \"source\": null,\n \"statement_descriptor\"\
|
||||
: null,\n \"statement_descriptor_suffix\": null,\n \"status\": \"succeeded\"\
|
||||
,\n \"transfer_data\": null,\n \"transfer_group\": null\n },\n \"payment_link\"\
|
||||
: null,\n \"payment_method_collection\": \"if_required\",\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\": \"paid\",\n \"\
|
||||
phone_number_collection\": {\n \"enabled\": false\n },\n \"recovered_from\"\
|
||||
: null,\n \"saved_payment_method_options\": {\n \"allow_redisplay_filters\"\
|
||||
: [\n \"always\"\n ],\n \"payment_method_remove\": null,\n \"\
|
||||
payment_method_save\": null\n },\n \"setup_intent\": null,\n \"shipping_address_collection\"\
|
||||
: null,\n \"shipping_cost\": null,\n \"shipping_details\": null,\n \"shipping_options\"\
|
||||
: [],\n \"status\": \"complete\",\n \"submit_type\": \"pay\",\n \"subscription\"\
|
||||
: null,\n \"success_url\": \"http://testserver/looper/stripe_success/1/{CHECKOUT_SESSION_ID}\"\
|
||||
,\n \"total_details\": {\n \"amount_discount\": 0,\n \"amount_shipping\"\
|
||||
: 0,\n \"amount_tax\": 0\n },\n \"ui_mode\": \"hosted\",\n \"url\": null\n\
|
||||
}"
|
||||
content_type: text/plain
|
||||
method: GET
|
||||
status: 200
|
||||
url: https://api.stripe.com/v1/checkout/sessions/cs_test_a19GMh7hJXYbh9OhEln16y1M8hfrdu0ySIpjRX4HQJqSVvpe9a9UP30bWW?expand%5B0%5D=payment_intent&expand%5B1%5D=payment_intent.customer&expand%5B2%5D=payment_intent.latest_charge&expand%5B3%5D=payment_intent.latest_charge.payment_method_details&expand%5B4%5D=payment_intent.payment_method
|
||||
- response:
|
||||
auto_calculate_content_length: false
|
||||
body: "{\n \"id\": \"pm_1PRVj2E4KAUB5djsNQr0k105\",\n \"object\": \"payment_method\"\
|
||||
,\n \"allow_redisplay\": \"limited\",\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\": \"my.billing.email@example.com\",\n \"name\": \"\
|
||||
Jane Doe\",\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\": 1718354592,\n \"customer\"\
|
||||
: \"cus_QI5uhaeNfeXwYP\",\n \"livemode\": false,\n \"metadata\": {},\n \"\
|
||||
type\": \"card\"\n}"
|
||||
content_type: text/plain
|
||||
method: GET
|
||||
status: 200
|
||||
url: https://api.stripe.com/v1/payment_methods/pm_1PRVj2E4KAUB5djsNQr0k105
|
@ -1,11 +1,11 @@
|
||||
responses:
|
||||
- response:
|
||||
auto_calculate_content_length: false
|
||||
body: "{\n \"id\": \"cus_QFaxRrT6ZbD9NN\",\n \"object\": \"customer\",\n \"\
|
||||
address\": null,\n \"balance\": 0,\n \"created\": 1717778124,\n \"currency\"\
|
||||
body: "{\n \"id\": \"cus_QI5uhaeNfeXwYP\",\n \"object\": \"customer\",\n \"\
|
||||
address\": null,\n \"balance\": 0,\n \"created\": 1718354548,\n \"currency\"\
|
||||
: null,\n \"default_source\": null,\n \"delinquent\": false,\n \"description\"\
|
||||
: null,\n \"discount\": null,\n \"email\": \"my.billing.email@example.com\"\
|
||||
,\n \"invoice_prefix\": \"046F772E\",\n \"invoice_settings\": {\n \"custom_fields\"\
|
||||
,\n \"invoice_prefix\": \"8D38744D\",\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\": \"New Full Name\",\n \"phone\": null,\n \"preferred_locales\"\
|
||||
@ -17,22 +17,22 @@ responses:
|
||||
url: https://api.stripe.com/v1/customers
|
||||
- response:
|
||||
auto_calculate_content_length: false
|
||||
body: "{\n \"id\": \"cs_test_a1hoP4Yj4ZmfghAwGoUtWJngVt1XreEVLGAj2n7U5o9BlvqhnDimuA07zh\"\
|
||||
body: "{\n \"id\": \"cs_test_a19GMh7hJXYbh9OhEln16y1M8hfrdu0ySIpjRX4HQJqSVvpe9a9UP30bWW\"\
|
||||
,\n \"object\": \"checkout.session\",\n \"after_expiration\": null,\n \"\
|
||||
allow_promotion_codes\": null,\n \"amount_subtotal\": 990,\n \"amount_total\"\
|
||||
: 990,\n \"automatic_tax\": {\n \"enabled\": false,\n \"liability\":\
|
||||
\ null,\n \"status\": null\n },\n \"billing_address_collection\": null,\n\
|
||||
\ \"cancel_url\": \"http://testserver/join/plan-variation/2/billing/\",\n \
|
||||
\ \"client_reference_id\": null,\n \"client_secret\": null,\n \"consent\"\
|
||||
: null,\n \"consent_collection\": null,\n \"created\": 1717778125,\n \"currency\"\
|
||||
allow_promotion_codes\": null,\n \"amount_subtotal\": 1252,\n \"amount_total\"\
|
||||
: 1252,\n \"automatic_tax\": {\n \"enabled\": false,\n \"liability\"\
|
||||
: null,\n \"status\": null\n },\n \"billing_address_collection\": null,\n\
|
||||
\ \"cancel_url\": \"http://testserver/join/plan-variation/10/billing/\",\n\
|
||||
\ \"client_reference_id\": null,\n \"client_secret\": null,\n \"consent\"\
|
||||
: null,\n \"consent_collection\": null,\n \"created\": 1718354548,\n \"currency\"\
|
||||
: \"eur\",\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_QFaxRrT6ZbD9NN\",\n \"customer_creation\": null,\n \"customer_details\"\
|
||||
customer\": \"cus_QI5uhaeNfeXwYP\",\n \"customer_creation\": null,\n \"customer_details\"\
|
||||
: {\n \"address\": null,\n \"email\": \"my.billing.email@example.com\"\
|
||||
,\n \"name\": null,\n \"phone\": null,\n \"tax_exempt\": \"none\",\n\
|
||||
\ \"tax_ids\": null\n },\n \"customer_email\": null,\n \"expires_at\"\
|
||||
: 1717864524,\n \"invoice\": null,\n \"invoice_creation\": {\n \"enabled\"\
|
||||
: 1718440948,\n \"invoice\": null,\n \"invoice_creation\": {\n \"enabled\"\
|
||||
: false,\n \"invoice_data\": {\n \"account_tax_ids\": null,\n \"\
|
||||
custom_fields\": null,\n \"description\": null,\n \"footer\": null,\n\
|
||||
\ \"issuer\": null,\n \"metadata\": {},\n \"rendering_options\"\
|
||||
@ -51,7 +51,7 @@ responses:
|
||||
: null,\n \"success_url\": \"http://testserver/looper/stripe_success/1/{CHECKOUT_SESSION_ID}\"\
|
||||
,\n \"total_details\": {\n \"amount_discount\": 0,\n \"amount_shipping\"\
|
||||
: 0,\n \"amount_tax\": 0\n },\n \"ui_mode\": \"hosted\",\n \"url\": \"\
|
||||
https://checkout.stripe.com/c/pay/cs_test_a1hoP4Yj4ZmfghAwGoUtWJngVt1XreEVLGAj2n7U5o9BlvqhnDimuA07zh#fidkdWxOYHwnPyd1blpxYHZxWjA0VUpnNzNAMU5EUEcwYW92dWIwclxRMzQ8ZkxsUDRET2dPbTVidnBCNEJTdlBJQTRJYFF2c09BMEFBdlxVT19USGpWSXRSXFJwdm5UQXRpdVw2Rmp%2FZ11NNTU3fHdHUl1JTCcpJ2N3amhWYHdzYHcnP3F3cGApJ2lkfGpwcVF8dWAnPyd2bGtiaWBabHFgaCcpJ2BrZGdpYFVpZGZgbWppYWB3dic%2FcXdwYHgl\"\
|
||||
https://checkout.stripe.com/c/pay/cs_test_a19GMh7hJXYbh9OhEln16y1M8hfrdu0ySIpjRX4HQJqSVvpe9a9UP30bWW#fidkdWxOYHwnPyd1blpxYHZxWjA0VUpnNzNAMU5EUEcwYW92dWIwclxRMzQ8ZkxsUDRET2dPbTVidnBCNEJTdlBJQTRJYFF2c09BMEFBdlxVT19USGpWSXRSXFJwdm5UQXRpdVw2Rmp%2FZ11NNTU3fHdHUl1JTCcpJ2N3amhWYHdzYHcnP3F3cGApJ2lkfGpwcVF8dWAnPyd2bGtiaWBabHFgaCcpJ2BrZGdpYFVpZGZgbWppYWB3dic%2FcXdwYHgl\"\
|
||||
\n}"
|
||||
content_type: text/plain
|
||||
method: POST
|
9
subscriptions/tests/_responses/vies_valid.yaml
Normal file
9
subscriptions/tests/_responses/vies_valid.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
responses:
|
||||
- response:
|
||||
auto_calculate_content_length: false
|
||||
body: <env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"><env:Header/><env:Body><ns2:checkVatResponse
|
||||
xmlns:ns2="urn:ec.europa.eu:taxud:vies:services:checkVat:types"><ns2:countryCode>DE</ns2:countryCode><ns2:vatNumber>260543043</ns2:vatNumber><ns2:requestDate>2024-06-14+02:00</ns2:requestDate><ns2:valid>true</ns2:valid><ns2:name>---</ns2:name><ns2:address>---</ns2:address></ns2:checkVatResponse></env:Body></env:Envelope>
|
||||
content_type: text/plain
|
||||
method: POST
|
||||
status: 200
|
||||
url: https://ec.europa.eu/taxation_customs/vies/services/checkVatService
|
163
subscriptions/tests/_responses/vies_wsdl.yaml
Normal file
163
subscriptions/tests/_responses/vies_wsdl.yaml
Normal file
@ -0,0 +1,163 @@
|
||||
responses:
|
||||
- response:
|
||||
auto_calculate_content_length: false
|
||||
body: "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<wsdl:definitions targetNamespace=\"\
|
||||
urn:ec.europa.eu:taxud:vies:services:checkVat\" xmlns:tns1=\"urn:ec.europa.eu:taxud:vies:services:checkVat:types\"\
|
||||
\ xmlns:soapenc=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:impl=\"\
|
||||
urn:ec.europa.eu:taxud:vies:services:checkVat\" xmlns:apachesoap=\"http://xml.apache.org/xml-soap\"\
|
||||
\ xmlns:wsdl=\"http://schemas.xmlsoap.org/wsdl/\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\
|
||||
\ xmlns:wsdlsoap=\"http://schemas.xmlsoap.org/wsdl/soap/\">\n <xsd:documentation>\n\
|
||||
\t The objective of this Internet site is to allow persons involved in the intra-Community\
|
||||
\ supply of goods or of services to obtain confirmation of the validity of the\
|
||||
\ VAT identification number of any specified person, in accordance to article\
|
||||
\ 31 of Council Regulation (EC) No. 904/2010 of 7 October 2010.\n Any other\
|
||||
\ use and any extraction and use of the data which is not in conformity with\
|
||||
\ the objective of this site is strictly forbidden. \n Any retransmission\
|
||||
\ of the contents of this site, whether for a commercial purpose or otherwise,\
|
||||
\ as well as any more general use other than as far as is necessary to support\
|
||||
\ the activity of a legitimate user (for example: to draw up their own invoices)\
|
||||
\ is expressly forbidden. In addition, any copying or reproduction of the contents\
|
||||
\ of this site is strictly forbidden. \n The European Commission maintains\
|
||||
\ this website to enhance the access by taxable persons making intra-Community\
|
||||
\ supplies to verification of their customers' VAT identification numbers. Our\
|
||||
\ goal is to supply instantaneous and accurate information. \n However the\
|
||||
\ Commission accepts no responsibility or liability whatsoever with regard to\
|
||||
\ the information obtained using this site. This information: \n - is obtained\
|
||||
\ from Member States' databases over which the Commission services have no control\
|
||||
\ and for which the Commission assumes no responsibility; it is the responsibility\
|
||||
\ of the Member States to keep their databases complete, accurate and up to\
|
||||
\ date; \n - is not professional or legal advice (if you need specific advice,\
|
||||
\ you should always consult a suitably qualified professional); \n - does\
|
||||
\ not in itself give a right to exempt intra-Community supplies from Value Added\
|
||||
\ Tax; \n - does not change any obligations imposed on taxable persons in\
|
||||
\ relation to intra-Community supplies. \n It is our goal to minimise disruption\
|
||||
\ caused by technical errors. However some data or information on our site may\
|
||||
\ have been created or structured in files or formats which are not error-free\
|
||||
\ and we cannot guarantee that our service will not be interrupted or otherwise\
|
||||
\ affected by such problems. The Commission accepts no responsibility with regard\
|
||||
\ to such problems incurred as a result of using this site or any linked external\
|
||||
\ sites. \n This disclaimer is not intended to limit the liability of the\
|
||||
\ Commission in contravention of any requirements laid down in applicable national\
|
||||
\ law nor to exclude its liability for matters which may not be excluded under\
|
||||
\ that law. \n Collecting or handling personal data falls under the Data\
|
||||
\ Protection Notice. This data protection declaration explains the Processing\
|
||||
\ in the VIES-on-the-web Internet Website of VAT Identification Numbers for\
|
||||
\ intra-Community Transaction on Goods or Services. Details of your legal rights\
|
||||
\ associated with the collection, processing and use of this data are also provided:\
|
||||
\ http://ec.europa.eu/dpo-register/details.htm?id=40647 . \n \n Usage:\
|
||||
\ \n The countryCode input parameter must follow the pattern [A-Z]{2} \n\
|
||||
\ The vatNumber input parameter must follow the pattern [0-9A-Za-z\\+\\*\\\
|
||||
.]{2,12} \n In case of problems, the returned FaultString can take the following\
|
||||
\ specific values: \n - INVALID_INPUT: The provided CountryCode is invalid\
|
||||
\ or the VAT number is empty; \n - GLOBAL_MAX_CONCURRENT_REQ: Your Request\
|
||||
\ for VAT validation has not been processed; the maximum number of concurrent\
|
||||
\ requests has been reached. Please re-submit your request later or contact\
|
||||
\ TAXUD-VIESWEB@ec.europa.eu for further information\": Your request cannot\
|
||||
\ be processed due to high traffic on the web application. Please try again\
|
||||
\ later; \n - MS_MAX_CONCURRENT_REQ: Your Request for VAT validation has\
|
||||
\ not been processed; the maximum number of concurrent requests for this Member\
|
||||
\ State has been reached. Please re-submit your request later or contact TAXUD-VIESWEB@ec.europa.eu\
|
||||
\ for further information\": Your request cannot be processed due to high traffic\
|
||||
\ towards the Member State you are trying to reach. Please try again later.\
|
||||
\ \n - SERVICE_UNAVAILABLE: an error was encountered either at the network\
|
||||
\ level or the Web application level, try again later; \n - MS_UNAVAILABLE:\
|
||||
\ The application at the Member State is not replying or not available. Please\
|
||||
\ refer to the Technical Information page to check the status of the requested\
|
||||
\ Member State, try again later; \n - TIMEOUT: The application did not receive\
|
||||
\ a reply within the allocated time period, try again later. \n\t</xsd:documentation>\n\
|
||||
\ \n <wsdl:types>\n <xsd:schema attributeFormDefault=\"qualified\" elementFormDefault=\"\
|
||||
qualified\" targetNamespace=\"urn:ec.europa.eu:taxud:vies:services:checkVat:types\"\
|
||||
\ xmlns=\"urn:ec.europa.eu:taxud:vies:services:checkVat:types\">\n\t\t\t<xsd:element\
|
||||
\ name=\"checkVat\">\n\t\t\t\t<xsd:complexType>\n\t\t\t\t\t<xsd:sequence>\n\t\
|
||||
\t\t\t\t\t<xsd:element name=\"countryCode\" type=\"xsd:string\"/>\n\t\t\t\t\t\
|
||||
\t<xsd:element name=\"vatNumber\" type=\"xsd:string\"/>\n\t\t\t\t\t</xsd:sequence>\n\
|
||||
\t\t\t\t</xsd:complexType>\n\t\t\t</xsd:element>\n\t\t\t<xsd:element name=\"\
|
||||
checkVatResponse\">\n\t\t\t\t<xsd:complexType>\n\t\t\t\t\t<xsd:sequence>\n\t\
|
||||
\t\t\t\t\t<xsd:element name=\"countryCode\" type=\"xsd:string\"/>\n\t\t\t\t\t\
|
||||
\t<xsd:element name=\"vatNumber\" type=\"xsd:string\"/>\n\t\t\t\t\t\t<xsd:element\
|
||||
\ name=\"requestDate\" type=\"xsd:date\"/>\n\t\t\t\t\t\t<xsd:element name=\"\
|
||||
valid\" type=\"xsd:boolean\"/>\n\t\t\t\t\t\t<xsd:element maxOccurs=\"1\" minOccurs=\"\
|
||||
0\" name=\"name\" nillable=\"true\" type=\"xsd:string\"/>\n\t\t\t\t\t\t<xsd:element\
|
||||
\ maxOccurs=\"1\" minOccurs=\"0\" name=\"address\" nillable=\"true\" type=\"\
|
||||
xsd:string\"/>\n\t\t\t\t\t</xsd:sequence>\n\t\t\t\t</xsd:complexType>\n\t\t\t\
|
||||
</xsd:element>\n\t\t\t<xsd:element name=\"checkVatApprox\">\n\t\t\t\t<xsd:complexType>\n\
|
||||
\t\t\t\t\t<xsd:sequence>\n\t\t\t\t\t\t<xsd:element name=\"countryCode\" type=\"\
|
||||
xsd:string\"/>\n\t\t\t\t\t\t<xsd:element name=\"vatNumber\" type=\"xsd:string\"\
|
||||
/>\n\t\t\t\t\t\t<xsd:element maxOccurs=\"1\" minOccurs=\"0\" name=\"traderName\"\
|
||||
\ type=\"xsd:string\"/>\n\t\t\t\t\t\t<xsd:element maxOccurs=\"1\" minOccurs=\"\
|
||||
0\" name=\"traderCompanyType\" type=\"tns1:companyTypeCode\"/>\n\t\t\t\t\t\t\
|
||||
<xsd:element maxOccurs=\"1\" minOccurs=\"0\" name=\"traderStreet\" type=\"xsd:string\"\
|
||||
/>\n\t\t\t\t\t\t<xsd:element maxOccurs=\"1\" minOccurs=\"0\" name=\"traderPostcode\"\
|
||||
\ type=\"xsd:string\"/>\n\t\t\t\t\t\t<xsd:element maxOccurs=\"1\" minOccurs=\"\
|
||||
0\" name=\"traderCity\" type=\"xsd:string\"/>\n\t\t\t\t\t\t<xsd:element maxOccurs=\"\
|
||||
1\" minOccurs=\"0\" name=\"requesterCountryCode\" type=\"xsd:string\"/>\n\t\t\
|
||||
\t\t\t\t<xsd:element maxOccurs=\"1\" minOccurs=\"0\" name=\"requesterVatNumber\"\
|
||||
\ type=\"xsd:string\"/>\n\t\t\t\t\t</xsd:sequence>\n\t\t\t\t</xsd:complexType>\n\
|
||||
\t\t\t</xsd:element>\n\t\t\t<xsd:element name=\"checkVatApproxResponse\">\n\t\
|
||||
\t\t\t<xsd:complexType>\n\t\t\t\t\t<xsd:sequence>\n\t\t\t\t\t\t<xsd:element\
|
||||
\ name=\"countryCode\" type=\"xsd:string\"/>\n\t\t\t\t\t\t<xsd:element name=\"\
|
||||
vatNumber\" type=\"xsd:string\"/>\n\t\t\t\t\t\t<xsd:element name=\"requestDate\"\
|
||||
\ type=\"xsd:date\"/>\n\t\t\t\t\t\t<xsd:element name=\"valid\" type=\"xsd:boolean\"\
|
||||
/>\n\t\t\t\t\t\t<xsd:element maxOccurs=\"1\" minOccurs=\"0\" name=\"traderName\"\
|
||||
\ nillable=\"true\" type=\"xsd:string\"/>\n\t\t\t\t\t\t<xsd:element maxOccurs=\"\
|
||||
1\" minOccurs=\"0\" name=\"traderCompanyType\" nillable=\"true\" type=\"tns1:companyTypeCode\"\
|
||||
/>\n\t\t\t\t\t\t<xsd:element maxOccurs=\"1\" minOccurs=\"0\" name=\"traderAddress\"\
|
||||
\ type=\"xsd:string\"/>\n\t\t\t\t\t\t<xsd:element maxOccurs=\"1\" minOccurs=\"\
|
||||
0\" name=\"traderStreet\" type=\"xsd:string\"/>\n\t\t\t\t\t\t<xsd:element maxOccurs=\"\
|
||||
1\" minOccurs=\"0\" name=\"traderPostcode\" type=\"xsd:string\"/>\n\t\t\t\t\t\
|
||||
\t<xsd:element maxOccurs=\"1\" minOccurs=\"0\" name=\"traderCity\" type=\"xsd:string\"\
|
||||
/>\n\t\t\t\t\t\t<xsd:element maxOccurs=\"1\" minOccurs=\"0\" name=\"traderNameMatch\"\
|
||||
\ type=\"tns1:matchCode\"/>\n\t\t\t\t\t\t<xsd:element maxOccurs=\"1\" minOccurs=\"\
|
||||
0\" name=\"traderCompanyTypeMatch\" type=\"tns1:matchCode\"/>\n\t\t\t\t\t\t\
|
||||
<xsd:element maxOccurs=\"1\" minOccurs=\"0\" name=\"traderStreetMatch\" type=\"\
|
||||
tns1:matchCode\"/>\n\t\t\t\t\t\t<xsd:element maxOccurs=\"1\" minOccurs=\"0\"\
|
||||
\ name=\"traderPostcodeMatch\" type=\"tns1:matchCode\"/>\n\t\t\t\t\t\t<xsd:element\
|
||||
\ maxOccurs=\"1\" minOccurs=\"0\" name=\"traderCityMatch\" type=\"tns1:matchCode\"\
|
||||
/>\n\t\t\t\t\t\t<xsd:element name=\"requestIdentifier\" type=\"xsd:string\"\
|
||||
/>\n\t\t\t\t\t</xsd:sequence>\n\t\t\t\t</xsd:complexType>\n\t\t\t</xsd:element>\n\
|
||||
\t\t\t<xsd:simpleType name=\"companyTypeCode\">\n\t\t\t\t<xsd:restriction base=\"\
|
||||
xsd:string\">\n\t\t\t\t\t<xsd:pattern value=\"[A-Z]{2}\\-[1-9][0-9]?\"/>\n\t\
|
||||
\t\t\t</xsd:restriction>\n\t\t\t</xsd:simpleType>\n\t\t\t<xsd:simpleType name=\"\
|
||||
matchCode\">\n\t\t\t\t<xsd:restriction base=\"xsd:string\">\n\t\t\t\t\t<xsd:enumeration\
|
||||
\ value=\"1\">\n\t\t\t\t\t\t<xsd:annotation>\n\t\t\t\t\t\t\t<xsd:documentation>VALID</xsd:documentation>\n\
|
||||
\t\t\t\t\t\t</xsd:annotation>\n\t\t\t\t\t</xsd:enumeration>\n\t\t\t\t\t<xsd:enumeration\
|
||||
\ value=\"2\">\n <xsd:annotation>\n \
|
||||
\ <xsd:documentation>INVALID</xsd:documentation>\n \
|
||||
\ </xsd:annotation>\n </xsd:enumeration>\n \
|
||||
\ <xsd:enumeration value=\"3\">\n <xsd:annotation>\n\
|
||||
\ <xsd:documentation>NOT_PROCESSED</xsd:documentation>\n\
|
||||
\ </xsd:annotation>\n </xsd:enumeration>\n\
|
||||
\t\t\t\t</xsd:restriction>\n\t\t\t</xsd:simpleType>\n\t\t</xsd:schema>\n </wsdl:types>\n\
|
||||
\ <wsdl:message name=\"checkVatRequest\">\n <wsdl:part name=\"parameters\"\
|
||||
\ element=\"tns1:checkVat\">\n </wsdl:part>\n </wsdl:message>\n <wsdl:message\
|
||||
\ name=\"checkVatApproxResponse\">\n <wsdl:part name=\"parameters\" element=\"\
|
||||
tns1:checkVatApproxResponse\">\n </wsdl:part>\n </wsdl:message>\n <wsdl:message\
|
||||
\ name=\"checkVatApproxRequest\">\n <wsdl:part name=\"parameters\" element=\"\
|
||||
tns1:checkVatApprox\">\n </wsdl:part>\n </wsdl:message>\n <wsdl:message\
|
||||
\ name=\"checkVatResponse\">\n <wsdl:part name=\"parameters\" element=\"\
|
||||
tns1:checkVatResponse\">\n </wsdl:part>\n </wsdl:message>\n <wsdl:portType\
|
||||
\ name=\"checkVatPortType\">\n <wsdl:operation name=\"checkVat\">\n \
|
||||
\ <wsdl:input name=\"checkVatRequest\" message=\"impl:checkVatRequest\">\n \
|
||||
\ </wsdl:input>\n <wsdl:output name=\"checkVatResponse\" message=\"impl:checkVatResponse\"\
|
||||
>\n </wsdl:output>\n </wsdl:operation>\n <wsdl:operation name=\"checkVatApprox\"\
|
||||
>\n <wsdl:input name=\"checkVatApproxRequest\" message=\"impl:checkVatApproxRequest\"\
|
||||
>\n </wsdl:input>\n <wsdl:output name=\"checkVatApproxResponse\" message=\"\
|
||||
impl:checkVatApproxResponse\">\n </wsdl:output>\n </wsdl:operation>\n\
|
||||
\ </wsdl:portType>\n <wsdl:binding name=\"checkVatBinding\" type=\"impl:checkVatPortType\"\
|
||||
>\n <wsdlsoap:binding style=\"document\" transport=\"http://schemas.xmlsoap.org/soap/http\"\
|
||||
/>\n <wsdl:operation name=\"checkVat\">\n <wsdlsoap:operation soapAction=\"\
|
||||
\"/>\n <wsdl:input name=\"checkVatRequest\">\n <wsdlsoap:body use=\"\
|
||||
literal\"/>\n </wsdl:input>\n <wsdl:output name=\"checkVatResponse\"\
|
||||
>\n <wsdlsoap:body use=\"literal\"/>\n </wsdl:output>\n </wsdl:operation>\n\
|
||||
\ <wsdl:operation name=\"checkVatApprox\">\n <wsdlsoap:operation soapAction=\"\
|
||||
\"/>\n <wsdl:input name=\"checkVatApproxRequest\">\n <wsdlsoap:body\
|
||||
\ use=\"literal\"/>\n </wsdl:input>\n <wsdl:output name=\"checkVatApproxResponse\"\
|
||||
>\n <wsdlsoap:body use=\"literal\"/>\n </wsdl:output>\n </wsdl:operation>\n\
|
||||
\ </wsdl:binding>\n <wsdl:service name=\"checkVatService\">\n <wsdl:port\
|
||||
\ name=\"checkVatPort\" binding=\"impl:checkVatBinding\">\n <wsdlsoap:address\
|
||||
\ location=\"http://ec.europa.eu/taxation_customs/vies/services/checkVatService\"\
|
||||
/>\n </wsdl:port>\n </wsdl:service>\n</wsdl:definitions>\n"
|
||||
content_type: text/plain
|
||||
method: GET
|
||||
status: 200
|
||||
url: https://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl
|
@ -13,6 +13,8 @@ import looper.models
|
||||
|
||||
import users.tests.util as util
|
||||
|
||||
responses_dir = 'subscriptions/tests/_responses/'
|
||||
|
||||
|
||||
def _write_mail(mail, index=0):
|
||||
email = mail.outbox[index]
|
||||
@ -24,7 +26,20 @@ def _write_mail(mail, index=0):
|
||||
f.write(str(content))
|
||||
|
||||
|
||||
def responses_from_file(file_name: str, rsps=responses, order_id=None):
|
||||
"""Add a response mock from file, override `order_id` metadata with a given one."""
|
||||
rsps._add_from_file(f'{responses_dir}{file_name}')
|
||||
# Replace metadata's "order_id" hardcoded in the response YAML with current order ID,
|
||||
# because it differs depending on whether this test is run alone or with all the tests.
|
||||
for _ in rsps.registered():
|
||||
if '%5D=payment_intent' in _.url:
|
||||
assert '\"order_id\": \"1' in _.body
|
||||
_.body = _.body.replace('\"order_id\": \"1', f'\"order_id\": \"{order_id}')
|
||||
|
||||
|
||||
class BaseSubscriptionTestCase(TestCase):
|
||||
fixtures = ['gateways']
|
||||
|
||||
def _get_url_for(self, **filter_params) -> Tuple[str, looper.models.PlanVariation]:
|
||||
plan_variation = looper.models.PlanVariation.objects.active().get(**filter_params)
|
||||
return (
|
||||
@ -37,6 +52,8 @@ class BaseSubscriptionTestCase(TestCase):
|
||||
|
||||
@factory.django.mute_signals(signals.pre_save, signals.post_save)
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Allow requests to Braintree Sandbox
|
||||
responses.add_passthru('https://api.sandbox.braintreegateway.com:443/')
|
||||
|
||||
@ -58,6 +75,13 @@ class BaseSubscriptionTestCase(TestCase):
|
||||
self.user = self.customer.user
|
||||
self.billing_address = self.customer.billing_address
|
||||
|
||||
responses._add_from_file(f'{responses_dir}stripe_new_cs_eur.yaml')
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
responses.stop()
|
||||
responses.reset()
|
||||
|
||||
def _mock_vies_response(self, is_valid=True, is_broken=False):
|
||||
path = os.path.abspath(__file__)
|
||||
dir_path = os.path.join(os.path.dirname(path), 'vies')
|
||||
@ -97,13 +121,11 @@ class BaseSubscriptionTestCase(TestCase):
|
||||
self.assertContains(response, 'id_street_address')
|
||||
self.assertContains(response, 'id_full_name')
|
||||
self.assertContains(response, 'name="gateway" value="stripe"')
|
||||
|
||||
def _assert_pay_via_bank_not_displayed(self, response):
|
||||
self.assertNotContains(response, 'name="gateway" value="bank"')
|
||||
|
||||
def _assert_billing_details_form_with_pay_via_bank_displayed(self, response):
|
||||
self._assert_continue_to_payment_displayed(response)
|
||||
self.assertContains(response, 'id_street_address')
|
||||
self.assertContains(response, 'id_full_name')
|
||||
self.assertContains(response, 'name="gateway" value="stripe"')
|
||||
def _assert_pay_via_bank_displayed(self, response):
|
||||
self.assertContains(response, 'name="gateway" value="bank"')
|
||||
|
||||
def _assert_pricing_has_been_updated(self, response):
|
||||
|
@ -51,6 +51,9 @@ class TestClockBraintree(BaseSubscriptionTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# Allow requests to Braintree Sandbox
|
||||
responses.add_passthru('https://api.sandbox.braintreegateway.com:443/')
|
||||
|
||||
self.subscription = self._create_subscription_due_now()
|
||||
|
||||
@patch(
|
||||
|
@ -90,6 +90,7 @@ class JoinView(LoginRequiredMixin, FormView):
|
||||
form_kwargs.update(
|
||||
{
|
||||
'request': self.request,
|
||||
'plan_variation': self.plan_variation,
|
||||
'instance': self.customer.billing_address,
|
||||
}
|
||||
)
|
||||
@ -103,11 +104,6 @@ class JoinView(LoginRequiredMixin, FormView):
|
||||
'subscription': self.subscription,
|
||||
}
|
||||
|
||||
def gateway_from_form(self, form) -> looper.models.Gateway:
|
||||
"""Use Stripe by default, but allow bank transfer payments for manual plan variations."""
|
||||
self.gateway = looper.models.Gateway.objects.get(name=form.cleaned_data['gateway'])
|
||||
return self.gateway
|
||||
|
||||
def _get_or_create_subscription(
|
||||
self, gateway: looper.models.Gateway
|
||||
) -> looper.models.Subscription:
|
||||
@ -116,8 +112,8 @@ class JoinView(LoginRequiredMixin, FormView):
|
||||
if not subscription:
|
||||
subscription = looper.models.Subscription(customer=self.customer)
|
||||
is_new = True
|
||||
args = [self.customer.pk, gateway]
|
||||
logger.debug('Creating a new subscription for customer pk=%s, %s', *args)
|
||||
logger_args = [self.customer.pk, gateway]
|
||||
logger.debug('Creating a new subscription for customer pk=%s, %s', *logger_args)
|
||||
collection_method = self.plan_variation.collection_method
|
||||
supported = set(gateway.provider.supported_collection_methods)
|
||||
if collection_method not in supported:
|
||||
@ -136,6 +132,15 @@ class JoinView(LoginRequiredMixin, FormView):
|
||||
subscription.collection_method = collection_method
|
||||
subscription.save()
|
||||
|
||||
if gateway.name == 'bank':
|
||||
payment_method = self.customer.payment_method_add(None, gateway)
|
||||
if subscription.payment_method_id != payment_method.pk:
|
||||
logger.info(
|
||||
'Switching subscription pk=%d from payment method pk=%d to pk=%d',
|
||||
*[subscription.pk, subscription.payment_method_id, payment_method.pk],
|
||||
)
|
||||
subscription.switch_payment_method(payment_method)
|
||||
|
||||
# Configure the team if this is a team plan
|
||||
if hasattr(subscription.plan, 'team_properties'):
|
||||
team_properties = subscription.plan.team_properties
|
||||
@ -185,7 +190,7 @@ class JoinView(LoginRequiredMixin, FormView):
|
||||
messages.add_message(self.request, messages.INFO, msg)
|
||||
return self.form_invalid(form)
|
||||
|
||||
gateway = self.gateway_from_form(form)
|
||||
gateway = form.cleaned_data['gateway']
|
||||
price_cents = new_taxable.price.cents
|
||||
subscription = self._get_or_create_subscription(gateway)
|
||||
# Update the tax info stored on the subscription
|
||||
@ -193,6 +198,8 @@ class JoinView(LoginRequiredMixin, FormView):
|
||||
|
||||
order = self._fetch_or_create_order(form, subscription)
|
||||
# Update the order to take into account latest changes
|
||||
if order.payment_method_id != subscription.payment_method_id:
|
||||
order.switch_payment_method(subscription.payment_method)
|
||||
order.update()
|
||||
# Make sure we are charging what we've displayed
|
||||
price = looper.money.Money(order.price.currency, price_cents)
|
||||
|
@ -15,8 +15,8 @@ from looper.money import Money
|
||||
import looper.models
|
||||
|
||||
from looper.tests.factories import create_customer_with_billing_address
|
||||
from common.tests.factories.users import UserFactory
|
||||
from subscriptions.tests.base import BaseSubscriptionTestCase
|
||||
from common.tests.factories.users import UserFactory, OAuthUserInfoFactory
|
||||
from subscriptions.tests.base import BaseSubscriptionTestCase, responses_from_file
|
||||
import subscriptions.tasks
|
||||
import users.tasks
|
||||
import users.tests.util as util
|
||||
@ -58,7 +58,7 @@ class TestGETJoinView(BaseSubscriptionTestCase):
|
||||
response = self.client.get(url, REMOTE_ADDR=EURO_IPV4)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self._assert_billing_details_form_with_pay_via_bank_displayed(response)
|
||||
self._assert_pay_via_bank_displayed(response)
|
||||
|
||||
def test_get_prefills_full_name_and_billing_email_from_user(self):
|
||||
user = UserFactory(full_name="Jane До", email='jane.doe@example.com')
|
||||
@ -68,6 +68,7 @@ class TestGETJoinView(BaseSubscriptionTestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self._assert_total_default_variation_selected_tax_21_eur(response)
|
||||
self._assert_pay_via_bank_not_displayed(response)
|
||||
self.assertContains(
|
||||
response,
|
||||
'<input type="text" name="full_name" value="Jane До" maxlength="255" placeholder="Your Full Name" class="form-control" required id="id_full_name">',
|
||||
@ -183,13 +184,38 @@ class TestGETJoinView(BaseSubscriptionTestCase):
|
||||
)
|
||||
self._assert_total_default_variation_selected_tax_21_eur(response)
|
||||
|
||||
def test_plan_variation_matches_detected_currency_eur_non_eea_ip(self):
|
||||
customer = create_customer_with_billing_address()
|
||||
self.client.force_login(customer.user)
|
||||
|
||||
response = self.client.get(self.url, REMOTE_ADDR=SINGAPORE_IPV4)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Check that prices are in EUR and there is no tax
|
||||
self._assert_total_default_variation_selected_no_tax_eur(response)
|
||||
|
||||
def test_billing_address_country_takes_precedence_over_geo_ip(self):
|
||||
customer = create_customer_with_billing_address(country='NL')
|
||||
self.client.force_login(customer.user)
|
||||
|
||||
response = self.client.get(self.url, REMOTE_ADDR=SINGAPORE_IPV4)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self._assert_total_default_variation_selected_tax_21_eur(response)
|
||||
|
||||
|
||||
@freeze_time('2023-05-19 11:41:11')
|
||||
@responses.activate
|
||||
class TestPOSTJoinView(BaseSubscriptionTestCase):
|
||||
url_usd = reverse('subscriptions:join-billing-details', kwargs={'plan_variation_id': 1})
|
||||
url = reverse('subscriptions:join-billing-details', kwargs={'plan_variation_id': 2})
|
||||
responses._add_from_file(f'{responses_dir}stripe_create_checkout_session.yaml')
|
||||
|
||||
cs_url = 'https://checkout.stripe.com/c/pay/cs_test_a19GMh7hJXYbh9OhEln16y1M8hfrdu0ySIpjRX4HQJqSVvpe9a9UP30bWW#fidkdWxOYHwnPyd1blpxYHZxWjA0VUpnNzNAMU5EUEcwYW92dWIwclxRMzQ8ZkxsUDRET2dPbTVidnBCNEJTdlBJQTRJYFF2c09BMEFBdlxVT19USGpWSXRSXFJwdm5UQXRpdVw2Rmp%2FZ11NNTU3fHdHUl1JTCcpJ2N3amhWYHdzYHcnP3F3cGApJ2lkfGpwcVF8dWAnPyd2bGtiaWBabHFgaCcpJ2BrZGdpYFVpZGZgbWppYWB3dic%2FcXdwYHgl'
|
||||
cs_id = 'cs_test_a19GMh7hJXYbh9OhEln16y1M8hfrdu0ySIpjRX4HQJqSVvpe9a9UP30bWW'
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
responses._add_from_file(f'{responses_dir}vies_wsdl.yaml')
|
||||
responses._add_from_file(f'{responses_dir}stripe_new_cs_eur.yaml')
|
||||
|
||||
def test_post_updates_billing_address_and_customer_renders_next_form_de(self):
|
||||
customer = create_customer_with_billing_address(vat_number='', country='DE')
|
||||
@ -204,7 +230,7 @@ class TestPOSTJoinView(BaseSubscriptionTestCase):
|
||||
)
|
||||
.first()
|
||||
)
|
||||
data = full_billing_address_data
|
||||
data = {**full_billing_address_data, 'gateway': 'stripe'}
|
||||
url = reverse(
|
||||
'subscriptions:join-billing-details',
|
||||
kwargs={'plan_variation_id': selected_variation.pk},
|
||||
@ -227,31 +253,27 @@ class TestPOSTJoinView(BaseSubscriptionTestCase):
|
||||
self.assertContains(response, 'Manual ')
|
||||
self.assertContains(response, '/ <span class="x-price-period">1 year</span>', html=True)
|
||||
|
||||
# @_recorder.record(file_path='stripe_create_checkout_session.yaml')
|
||||
def test_post_has_correct_price_field_value(self):
|
||||
@responses.activate
|
||||
def test_post_redirects_to_stripe_hosted_checkout(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
data = required_address_data
|
||||
data = {**required_address_data, 'gateway': 'stripe'}
|
||||
response = self.client.post(self.url, data, REMOTE_ADDR=EURO_IPV4)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(
|
||||
response['Location'],
|
||||
'https://checkout.stripe.com/c/pay/cs_test_a1hoP4Yj4ZmfghAwGoUtWJngVt1XreEVLGAj2'
|
||||
'n7U5o9BlvqhnDimuA07zh#fidkdWxOYHwnPyd1blpxYHZxWjA0VUpnNzNAMU5EUEcwYW92dWIwclxRMzQ8Zkx'
|
||||
'sUDRET2dPbTVidnBCNEJTdlBJQTRJYFF2c09BMEFBdlxVT19USGpWSXRSXFJwdm5UQXRpdVw2Rmp'
|
||||
'%2FZ11NNTU3fHdHUl1JTCcpJ2N3amhWYHdzYHcnP3F3cGApJ2lkfGpwcVF8dWAnPyd2bGtiaWBabH'
|
||||
'FgaCcpJ2BrZGdpYFVpZGZgbWppYWB3dic%2FcXdwYHgl',
|
||||
)
|
||||
self.assertEqual(response['Location'], self.cs_url)
|
||||
|
||||
@responses.activate
|
||||
def test_post_updates_billing_address_and_customer_applies_reverse_charged_tax(self):
|
||||
responses._add_from_file(f'{responses_dir}vies_valid.yaml')
|
||||
self.client.force_login(self.user)
|
||||
|
||||
data = {
|
||||
**required_address_data,
|
||||
'vat_number': 'DE 260543043',
|
||||
'gateway': 'stripe',
|
||||
'country': 'DE',
|
||||
'postal_code': '11111',
|
||||
'vat_number': 'DE 260543043',
|
||||
}
|
||||
response = self.client.post(self.url, data, REMOTE_ADDR=EURO_IPV4)
|
||||
|
||||
@ -259,6 +281,7 @@ class TestPOSTJoinView(BaseSubscriptionTestCase):
|
||||
|
||||
self.user.refresh_from_db()
|
||||
address = self.user.customer.billing_address
|
||||
address.refresh_from_db()
|
||||
self.assertEqual(address.vat_number, 'DE260543043')
|
||||
self.assertEqual(address.full_name, 'New Full Name')
|
||||
self.assertEqual(address.postal_code, '11111')
|
||||
@ -273,18 +296,8 @@ class TestPOSTJoinView(BaseSubscriptionTestCase):
|
||||
# Post the same form again
|
||||
response = self.client.post(self.url, data)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
# Follow the redirect to avoid unexpected assertion errors
|
||||
response = self.client.get(response['Location'])
|
||||
|
||||
# Check that we are no longer on the billing details page
|
||||
self._assert_payment_form_displayed(response)
|
||||
|
||||
# The hidden price field must also be set to a matching amount
|
||||
self.assertContains(
|
||||
response,
|
||||
'<input type="hidden" name="price" value="8.32" class="form-control" id="id_price">',
|
||||
html=True,
|
||||
)
|
||||
self.assertEqual(response['Location'], self.cs_url, response['Location'])
|
||||
|
||||
def test_post_changing_address_from_with_region_to_without_region_clears_region(self):
|
||||
customer = create_customer_with_billing_address(
|
||||
@ -304,6 +317,7 @@ class TestPOSTJoinView(BaseSubscriptionTestCase):
|
||||
# Post an new address that doesn't require a region
|
||||
data = {
|
||||
**required_address_data,
|
||||
'gateway': 'stripe',
|
||||
'country': 'DE',
|
||||
'postal_code': '11111',
|
||||
}
|
||||
@ -329,77 +343,41 @@ class TestPOSTJoinView(BaseSubscriptionTestCase):
|
||||
user = customer.user
|
||||
self.client.force_login(user)
|
||||
|
||||
data = required_address_data
|
||||
data = {**required_address_data, 'gateway': 'stripe'}
|
||||
response = self.client.post(url, data, REMOTE_ADDR=EURO_IPV4)
|
||||
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_plan_variation_matches_detected_currency_eur_non_eea_ip(self):
|
||||
url, _ = self._get_url_for(currency='EUR', price=990)
|
||||
customer = create_customer_with_billing_address()
|
||||
user = customer.user
|
||||
self.client.force_login(user)
|
||||
|
||||
data = required_address_data
|
||||
response = self.client.post(url, data, REMOTE_ADDR=SINGAPORE_IPV4)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Check that prices are in EUR and there is no tax
|
||||
self._assert_total_default_variation_selected_no_tax_eur(response)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
expected_url, _ = self._get_url_for(currency='EUR', price=10900)
|
||||
self.assertEqual(response['Location'], expected_url)
|
||||
|
||||
def test_billing_address_country_takes_precedence_over_geo_ip(self):
|
||||
url, _ = self._get_url_for(currency='EUR', price=990)
|
||||
customer = create_customer_with_billing_address(country='NL')
|
||||
user = customer.user
|
||||
self.client.force_login(user)
|
||||
customer = create_customer_with_billing_address(country='GE')
|
||||
self.client.force_login(customer.user)
|
||||
|
||||
data = required_address_data
|
||||
response = self.client.post(url, data, REMOTE_ADDR=SINGAPORE_IPV4)
|
||||
data = {**required_address_data, 'gateway': 'stripe'}
|
||||
response = self.client.post(self.url, data, REMOTE_ADDR=SINGAPORE_IPV4)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self._assert_total_default_variation_selected_tax_21_eur(response)
|
||||
|
||||
def test_invalid_missing_required_fields(self):
|
||||
url, _ = self._get_url_for(currency='EUR', price=990)
|
||||
customer = create_customer_with_billing_address(country='NL')
|
||||
user = customer.user
|
||||
self.client.force_login(user)
|
||||
self.client.force_login(customer.user)
|
||||
|
||||
data = required_address_data
|
||||
response = self.client.post(url, data, REMOTE_ADDR=EURO_IPV4)
|
||||
response = self.client.post(self.url, {}, REMOTE_ADDR=EURO_IPV4)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self._assert_total_default_variation_selected_tax_21_eur(response)
|
||||
self.assertEqual(
|
||||
response.context['form'].errors,
|
||||
{
|
||||
'country': ['This field is required.'],
|
||||
'email': ['This field is required.'],
|
||||
'full_name': ['This field is required.'],
|
||||
'gateway': ['This field is required.'],
|
||||
'payment_method_nonce': ['This field is required.'],
|
||||
'price': ['This field is required.'],
|
||||
},
|
||||
)
|
||||
|
||||
def test_invalid_price_does_not_match_selected_plan_variation(self):
|
||||
url, selected_variation = self._get_url_for(currency='EUR', price=990)
|
||||
customer = create_customer_with_billing_address(country='NL')
|
||||
user = customer.user
|
||||
self.client.force_login(user)
|
||||
|
||||
data = {
|
||||
**required_address_data,
|
||||
'gateway': 'braintree',
|
||||
'payment_method_nonce': 'fake-valid-nonce',
|
||||
'price': '999.09',
|
||||
}
|
||||
response = self.client.post(url, data, REMOTE_ADDR=EURO_IPV4)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self._assert_total_default_variation_selected_tax_21_eur(response)
|
||||
self.assertEqual(
|
||||
response.context['form'].errors,
|
||||
{'__all__': ['Payment failed: please reload the page and try again']},
|
||||
)
|
||||
|
||||
def test_invalid_bank_transfer_cannot_be_selected_for_automatic_payments(self):
|
||||
url, selected_variation = self._get_url_for(currency='EUR', price=990)
|
||||
customer = create_customer_with_billing_address(country='NL')
|
||||
@ -435,8 +413,9 @@ class TestPOSTJoinView(BaseSubscriptionTestCase):
|
||||
)
|
||||
@responses.activate
|
||||
def test_pay_with_bank_transfer_creates_order_subscription_on_hold(self):
|
||||
customer = create_customer_with_billing_address(country='NL', full_name='Jane Doe')
|
||||
customer = create_customer_with_billing_address(country='NL')
|
||||
user = customer.user
|
||||
OAuthUserInfoFactory(user=user, oauth_user_id=554433)
|
||||
self.client.force_login(user)
|
||||
util.mock_blender_id_badger_badger_response(
|
||||
'grant', 'cloud_has_subscription', user.oauth_info.oauth_user_id
|
||||
@ -448,12 +427,7 @@ class TestPOSTJoinView(BaseSubscriptionTestCase):
|
||||
interval_unit='month',
|
||||
plan__name='Manual renewal',
|
||||
)
|
||||
data = {
|
||||
**required_address_data,
|
||||
'gateway': 'bank',
|
||||
'payment_method_nonce': 'unused',
|
||||
'price': '14.90',
|
||||
}
|
||||
data = {**required_address_data, 'full_name': 'Jane Doe', 'gateway': 'bank'}
|
||||
response = self.client.post(url, data, REMOTE_ADDR=EURO_IPV4)
|
||||
|
||||
self._assert_transactionless_done_page_displayed(response)
|
||||
@ -513,10 +487,17 @@ class TestPOSTJoinView(BaseSubscriptionTestCase):
|
||||
def test_pay_with_bank_transfer_creates_order_subscription_on_hold_shows_reverse_charged_price(
|
||||
self,
|
||||
):
|
||||
customer = create_customer_with_billing_address(
|
||||
country='ES', full_name='Jane Doe', vat_number='DE260543043'
|
||||
)
|
||||
responses._add_from_file(f'{responses_dir}vies_valid.yaml')
|
||||
address = {
|
||||
**required_address_data,
|
||||
'country': 'ES',
|
||||
'full_name': 'Jane Doe',
|
||||
'postal_code': '11111',
|
||||
'vat_number': 'ES A78374725',
|
||||
}
|
||||
customer = create_customer_with_billing_address(**address)
|
||||
user = customer.user
|
||||
OAuthUserInfoFactory(user=user, oauth_user_id=554433)
|
||||
self.client.force_login(user)
|
||||
util.mock_blender_id_badger_badger_response(
|
||||
'grant', 'cloud_has_subscription', user.oauth_info.oauth_user_id
|
||||
@ -528,12 +509,7 @@ class TestPOSTJoinView(BaseSubscriptionTestCase):
|
||||
interval_length=3,
|
||||
interval_unit='month',
|
||||
)
|
||||
data = {
|
||||
**required_address_data,
|
||||
'gateway': 'bank',
|
||||
'payment_method_nonce': 'unused',
|
||||
'price': '26.45',
|
||||
}
|
||||
data = {'gateway': 'bank', **address}
|
||||
response = self.client.post(url, data, REMOTE_ADDR=EURO_IPV4)
|
||||
|
||||
self._assert_transactionless_done_page_displayed(response)
|
||||
@ -547,6 +523,8 @@ class TestPOSTJoinView(BaseSubscriptionTestCase):
|
||||
self.assertEqual(subscription.collection_method, selected_variation.collection_method)
|
||||
self.assertEqual(subscription.collection_method, 'manual')
|
||||
self.assertEqual(subscription.plan, selected_variation.plan)
|
||||
self.assertEqual(str(subscription.payment_method), 'Bank Transfer')
|
||||
self.assertIsNone(subscription.payment_method.token)
|
||||
|
||||
order = subscription.latest_order()
|
||||
self.assertEqual(order.status, 'created')
|
||||
@ -559,6 +537,8 @@ class TestPOSTJoinView(BaseSubscriptionTestCase):
|
||||
self.assertIsNotNone(order.display_number)
|
||||
self.assertIsNotNone(order.vat_number)
|
||||
self.assertNotEqual(order.display_number, str(order.pk))
|
||||
self.assertEqual(str(order.payment_method), 'Bank Transfer')
|
||||
self.assertIsNone(order.payment_method.token)
|
||||
|
||||
self._assert_bank_transfer_email_is_sent(subscription)
|
||||
self._assert_bank_transfer_email_is_sent_tax_21_eur_reverse_charged(subscription)
|
||||
@ -567,6 +547,7 @@ class TestPOSTJoinView(BaseSubscriptionTestCase):
|
||||
response = self.client.get(
|
||||
reverse('subscriptions:manage', kwargs={'subscription_id': subscription.pk})
|
||||
)
|
||||
|
||||
self.assertNotIn('32.00', response)
|
||||
self.assertNotIn('21%', response)
|
||||
self.assertNotIn('Inc.', response)
|
||||
@ -595,35 +576,44 @@ class TestPOSTJoinView(BaseSubscriptionTestCase):
|
||||
'users.signals.tasks.grant_blender_id_role',
|
||||
new=users.tasks.grant_blender_id_role.task_function,
|
||||
)
|
||||
@responses.activate
|
||||
def test_pay_with_credit_card_creates_order_subscription_active(self):
|
||||
url, selected_variation = self._get_url_for(currency='EUR', price=990)
|
||||
customer = create_customer_with_billing_address(country='NL', full_name='Jane Doe')
|
||||
user = customer.user
|
||||
OAuthUserInfoFactory(user=user, oauth_user_id=554433)
|
||||
self.client.force_login(user)
|
||||
util.mock_blender_id_badger_badger_response(
|
||||
'grant', 'cloud_has_subscription', user.oauth_info.oauth_user_id
|
||||
)
|
||||
util.mock_blender_id_badger_badger_response(
|
||||
'grant', 'cloud_subscriber', user.oauth_info.oauth_user_id
|
||||
)
|
||||
|
||||
data = {
|
||||
**required_address_data,
|
||||
'gateway': 'braintree',
|
||||
# fake-three-d-secure-visa-full-authentication-nonce
|
||||
# causes the following error:
|
||||
# Merchant account must match the 3D Secure authorization merchant account: code 91584
|
||||
# TODO(anna): figure out if this is due to our settings or a quirk of the sandbox
|
||||
'payment_method_nonce': 'fake-valid-nonce',
|
||||
'price': '9.90',
|
||||
}
|
||||
response = self.client.post(url, data, REMOTE_ADDR=EURO_IPV4)
|
||||
data = {**required_address_data, 'gateway': 'stripe'}
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps._add_from_file(f'{responses_dir}stripe_new_cs_eur.yaml')
|
||||
response = self.client.post(url, data, REMOTE_ADDR=EURO_IPV4)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response['Location'], self.cs_url, response['Location'])
|
||||
|
||||
# **N.B**: this flow happens in 2 different views separated by a Stripe payment page.
|
||||
# Pretend that checkout session was completed and we've returned to the success page with its ID:
|
||||
subscription = user.customer.subscription_set.first()
|
||||
order = subscription.latest_order()
|
||||
url = reverse(
|
||||
'looper:stripe_success',
|
||||
kwargs={'pk': order.pk, 'stripe_session_id': 'CHECKOUT_SESSION_ID'},
|
||||
)
|
||||
url = url.replace('CHECKOUT_SESSION_ID', self.cs_id)
|
||||
with responses.RequestsMock() as rsps:
|
||||
responses_from_file('stripe_get_cs_eur.yaml', order_id=order.pk, rsps=rsps)
|
||||
util.mock_blender_id_badger_badger_response(
|
||||
'grant', 'cloud_has_subscription', user.oauth_info.oauth_user_id, rsps
|
||||
)
|
||||
util.mock_blender_id_badger_badger_response(
|
||||
'grant', 'cloud_subscriber', user.oauth_info.oauth_user_id, rsps
|
||||
)
|
||||
response = self.client.get(url)
|
||||
|
||||
self._assert_done_page_displayed(response)
|
||||
|
||||
subscription = user.customer.subscription_set.first()
|
||||
order = subscription.latest_order()
|
||||
subscription.refresh_from_db()
|
||||
order.refresh_from_db()
|
||||
self.assertEqual(subscription.status, 'active')
|
||||
self.assertEqual(subscription.price, Money('EUR', 990))
|
||||
self.assertEqual(subscription.collection_method, selected_variation.collection_method)
|
||||
@ -644,7 +634,6 @@ class TestPOSTJoinView(BaseSubscriptionTestCase):
|
||||
'users.signals.tasks.grant_blender_id_role',
|
||||
new=users.tasks.grant_blender_id_role.task_function,
|
||||
)
|
||||
@responses.activate
|
||||
def test_pay_with_credit_card_creates_order_subscription_active_team(self):
|
||||
url, selected_variation = self._get_url_for(
|
||||
currency='EUR',
|
||||
@ -653,21 +642,35 @@ class TestPOSTJoinView(BaseSubscriptionTestCase):
|
||||
)
|
||||
customer = create_customer_with_billing_address(country='NL', full_name='Jane Doe')
|
||||
user = customer.user
|
||||
OAuthUserInfoFactory(user=user, oauth_user_id=554433)
|
||||
self.client.force_login(user)
|
||||
util.mock_blender_id_badger_badger_response(
|
||||
'grant', 'cloud_has_subscription', user.oauth_info.oauth_user_id
|
||||
)
|
||||
util.mock_blender_id_badger_badger_response(
|
||||
'grant', 'cloud_subscriber', user.oauth_info.oauth_user_id
|
||||
)
|
||||
|
||||
data = {
|
||||
**required_address_data,
|
||||
'gateway': 'braintree',
|
||||
'payment_method_nonce': 'fake-valid-nonce',
|
||||
'price': '90.00',
|
||||
}
|
||||
response = self.client.post(url, data, REMOTE_ADDR=EURO_IPV4)
|
||||
data = {**required_address_data, 'gateway': 'stripe'}
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps._add_from_file(f'{responses_dir}stripe_new_cs_eur.yaml')
|
||||
response = self.client.post(url, data, REMOTE_ADDR=EURO_IPV4)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response['Location'], self.cs_url, response['Location'])
|
||||
|
||||
# **N.B**: this flow happens in 2 different views separated by a Stripe payment page.
|
||||
# Pretend that checkout session was completed and we've returned to the success page with its ID:
|
||||
subscription = user.customer.subscription_set.first()
|
||||
order = subscription.latest_order()
|
||||
url = reverse(
|
||||
'looper:stripe_success',
|
||||
kwargs={'pk': order.pk, 'stripe_session_id': 'CHECKOUT_SESSION_ID'},
|
||||
)
|
||||
url = url.replace('CHECKOUT_SESSION_ID', self.cs_id)
|
||||
with responses.RequestsMock() as rsps:
|
||||
responses_from_file('stripe_get_cs_eur.yaml', order_id=order.pk, rsps=rsps)
|
||||
util.mock_blender_id_badger_badger_response(
|
||||
'grant', 'cloud_has_subscription', user.oauth_info.oauth_user_id, rsps
|
||||
)
|
||||
util.mock_blender_id_badger_badger_response(
|
||||
'grant', 'cloud_subscriber', user.oauth_info.oauth_user_id, rsps
|
||||
)
|
||||
response = self.client.get(url)
|
||||
|
||||
self._assert_done_page_displayed(response)
|
||||
|
||||
@ -698,19 +701,46 @@ class TestPOSTJoinView(BaseSubscriptionTestCase):
|
||||
)
|
||||
data = {
|
||||
**required_address_data,
|
||||
'vat_number': 'DE 260543043',
|
||||
'gateway': 'stripe',
|
||||
'country': 'DE',
|
||||
'postal_code': '11111',
|
||||
'gateway': 'braintree',
|
||||
'payment_method_nonce': 'fake-valid-nonce',
|
||||
# VAT is subtracted from the plan variation price:
|
||||
'price': '12.52',
|
||||
'vat_number': 'DE 260543043',
|
||||
}
|
||||
response = self.client.post(url, data, REMOTE_ADDR=EURO_IPV4)
|
||||
# @_recorder.record(file_path=f'{responses_dir}stripe_new_cs_eur.yaml')
|
||||
def _continue_to_payment(): # noqa: E306
|
||||
return self.client.post(url, data, REMOTE_ADDR=EURO_IPV4)
|
||||
|
||||
with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
|
||||
# request to wsdl doesn't happen on subsequent calls to the validator,
|
||||
# hence assert_all_requests_are_fired = False.
|
||||
rsps._add_from_file(f'{responses_dir}vies_wsdl.yaml')
|
||||
rsps._add_from_file(f'{responses_dir}vies_valid.yaml')
|
||||
rsps._add_from_file(f'{responses_dir}stripe_new_cs_eur.yaml')
|
||||
response = _continue_to_payment()
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response['Location'], self.cs_url, response['Location'])
|
||||
|
||||
# **N.B**: this flow happens in 2 different views separated by a Stripe payment page.
|
||||
# Pretend that checkout session was completed and we've returned to the success page with its ID:
|
||||
subscription = user.customer.subscription_set.first()
|
||||
order = subscription.latest_order()
|
||||
url = reverse(
|
||||
'looper:stripe_success',
|
||||
kwargs={'pk': order.pk, 'stripe_session_id': 'CHECKOUT_SESSION_ID'},
|
||||
)
|
||||
url = url.replace('CHECKOUT_SESSION_ID', self.cs_id)
|
||||
# @_recorder.record(file_path=f'{responses_dir}stripe_get_cs_eur.yaml')
|
||||
def _back_to_success_url(): # noqa: E306
|
||||
return self.client.get(url)
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
responses_from_file('stripe_get_cs_eur.yaml', order_id=order.pk, rsps=rsps)
|
||||
response = _back_to_success_url()
|
||||
|
||||
self._assert_done_page_displayed(response)
|
||||
|
||||
subscription = user.customer.subscription_set.first()
|
||||
subscription.refresh_from_db()
|
||||
self.assertEqual(subscription.status, 'active')
|
||||
self.assertEqual(subscription.price, Money('EUR', 1490))
|
||||
self.assertEqual(subscription.tax, Money('EUR', 0))
|
||||
|
@ -10,7 +10,7 @@ import responses
|
||||
# from responses import _recorder
|
||||
|
||||
from common.tests.factories.users import UserFactory
|
||||
from subscriptions.tests.base import BaseSubscriptionTestCase
|
||||
from subscriptions.tests.base import BaseSubscriptionTestCase, responses_from_file
|
||||
import subscriptions.tasks
|
||||
|
||||
responses_dir = 'subscriptions/tests/_responses/'
|
||||
@ -104,8 +104,8 @@ class TestSubscriptionSettingsChangePaymentMethod(BaseSubscriptionTestCase):
|
||||
url_name = 'subscriptions:payment-method-change'
|
||||
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')
|
||||
# @_recorder.record(file_path=f'{responses_dir}stripe_new_cs_setup.yaml')
|
||||
# @_recorder.record(file_path=f'{responses_dir}stripe_get_cs_setup.yaml')
|
||||
def test_can_change_payment_method_from_bank_to_credit_card_with_sca(self):
|
||||
bank = Gateway.objects.get(name='bank')
|
||||
subscription = SubscriptionFactory(
|
||||
@ -120,7 +120,7 @@ class TestSubscriptionSettingsChangePaymentMethod(BaseSubscriptionTestCase):
|
||||
|
||||
url = reverse(self.url_name, kwargs={'subscription_id': subscription.pk})
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps._add_from_file(f'{responses_dir}stripe_create_checkout_session_setup.yaml')
|
||||
rsps._add_from_file(f'{responses_dir}stripe_new_cs_setup.yaml')
|
||||
response = self.client.post(url)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
@ -132,7 +132,7 @@ class TestSubscriptionSettingsChangePaymentMethod(BaseSubscriptionTestCase):
|
||||
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')
|
||||
rsps._add_from_file(f'{responses_dir}stripe_get_cs_setup.yaml')
|
||||
response = self.client.get(success_url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response['Location'], f'/subscription/{subscription.pk}/manage/')
|
||||
@ -257,8 +257,8 @@ class TestPayExistingOrder(BaseSubscriptionTestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# @_recorder.record(file_path=f'{responses_dir}stripe_create_checkout_session_usd.yaml')
|
||||
# @_recorder.record(file_path=f'{responses_dir}stripe_retrieve_checkout_session_usd.yaml')
|
||||
# @_recorder.record(file_path=f'{responses_dir}stripe_new_cs_usd.yaml')
|
||||
# @_recorder.record(file_path=f'{responses_dir}stripe_get_cs_usd.yaml')
|
||||
@patch(
|
||||
# Make sure background task is executed as a normal function
|
||||
'subscriptions.signals.tasks.send_mail_subscription_status_changed',
|
||||
@ -280,7 +280,7 @@ class TestPayExistingOrder(BaseSubscriptionTestCase):
|
||||
|
||||
url = reverse(self.url_name, kwargs={'order_id': order.pk})
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps._add_from_file(f'{responses_dir}stripe_create_checkout_session_usd.yaml')
|
||||
rsps._add_from_file(f'{responses_dir}stripe_new_cs_usd.yaml')
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
@ -296,13 +296,7 @@ class TestPayExistingOrder(BaseSubscriptionTestCase):
|
||||
)
|
||||
url = url.replace('CHECKOUT_SESSION_ID', checkout_session_id)
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps._add_from_file(f'{responses_dir}stripe_retrieve_checkout_session_usd.yaml')
|
||||
# Replace metadata's "order_id" hardcoded in the response YAML with current order ID,
|
||||
# because it differs depending on whether this test is run alone or with all the tests.
|
||||
for _ in rsps.registered():
|
||||
if '%5D=payment_intent' in _.url:
|
||||
assert '\"order_id\": \"1' in _.body
|
||||
_.body = _.body.replace('\"order_id\": \"1', f'\"order_id\": \"{order.pk}')
|
||||
responses_from_file('stripe_get_cs_usd.yaml', order_id=order.pk, rsps=rsps)
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(order.transaction_set.count(), 1)
|
||||
|
Loading…
Reference in New Issue
Block a user