Stripe checkout #104411

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

View File

@ -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'},
},
}

View File

@ -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."""

View 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

View File

@ -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

View 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

View 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

View File

@ -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):

View File

@ -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(

View File

@ -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)

View File

@ -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))

View File

@ -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)