Anna Sirota
ec2ce855b9
This also updates plan variations fixture (and historical migrations) to match what is currently in production.
603 lines
28 KiB
Python
603 lines
28 KiB
Python
from typing import Tuple
|
||
import os
|
||
|
||
from django.core import mail
|
||
from django.db.models import signals
|
||
from django.test import TestCase
|
||
from django.urls import reverse
|
||
import factory
|
||
import responses
|
||
|
||
from looper.tests.factories import create_customer_with_billing_address
|
||
import looper.models
|
||
|
||
import users.tests.util as util
|
||
|
||
responses_dir = 'subscriptions/tests/_responses/'
|
||
|
||
|
||
def _write_mail(mail, index=0):
|
||
email = mail.outbox[index]
|
||
name = email.subject.replace(' ', '_')
|
||
with open(f'/tmp/{name}.txt', 'w+') as f:
|
||
f.write(str(email.body))
|
||
for content, mimetype in email.alternatives:
|
||
with open(f'/tmp/{name}.{mimetype.replace("/", ".")}', 'w+') as f:
|
||
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 = ['team_plans']
|
||
|
||
def _get_url_for(self, **filter_params) -> Tuple[str, looper.models.PlanVariation]:
|
||
plan_variation = looper.models.PlanVariation.objects.active().get(**filter_params)
|
||
return (
|
||
reverse(
|
||
'subscriptions:join-billing-details',
|
||
kwargs={'plan_variation_id': plan_variation.pk},
|
||
),
|
||
plan_variation,
|
||
)
|
||
|
||
@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/')
|
||
|
||
# Create the admin user used for logging
|
||
self.admin_user = util.create_admin_log_user()
|
||
|
||
self.customer = create_customer_with_billing_address(
|
||
full_name='Алексей Н.',
|
||
company='Testcompany B.V.',
|
||
street_address='Billing street 1',
|
||
extended_address='Floor 1',
|
||
locality='Amsterdam',
|
||
postal_code='1000AA',
|
||
region='North Holland',
|
||
country='NL',
|
||
vat_number='NL-KVK-41202535',
|
||
email='billing@example.com',
|
||
)
|
||
self.user = self.customer.user
|
||
self.billing_address = self.customer.billing_address
|
||
|
||
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')
|
||
|
||
vies_base_url = 'https://ec.europa.eu/taxation_customs/vies'
|
||
wsdl_file = 'checkVatService.wsdl'
|
||
with open(os.path.join(dir_path, wsdl_file), 'r') as f:
|
||
responses.add(
|
||
responses.GET,
|
||
url=f'{vies_base_url}/checkVatService.wsdl',
|
||
body=f.read(),
|
||
content_type='text/xml',
|
||
)
|
||
|
||
post_xml_file = f'checkVatService_POST_{"valid" if is_valid else "invalid"}.xml'
|
||
with open(os.path.join(dir_path, post_xml_file), 'r') as f:
|
||
responses.add(
|
||
responses.POST,
|
||
url=f'{vies_base_url}/services/checkVatService',
|
||
body=f.read(),
|
||
content_type='text/xml',
|
||
status=500 if is_broken else 200,
|
||
)
|
||
|
||
def _assert_required_billing_details_updated(self, user):
|
||
customer = user.customer
|
||
address = customer.billing_address
|
||
self.assertEqual(address.full_name, 'New Full Name')
|
||
self.assertEqual(address.street_address, 'MAIN ST 1')
|
||
self.assertEqual(address.locality, 'Amsterdam')
|
||
self.assertEqual(address.postal_code, '1000 AA')
|
||
self.assertEqual(address.country, 'NL')
|
||
|
||
def _assert_billing_details_form_displayed(self, response):
|
||
self.assertNotContains(response, 'Sign in with Blender ID')
|
||
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_not_displayed(self, response):
|
||
self.assertNotContains(response, 'name="gateway" value="bank"')
|
||
|
||
def _assert_pay_via_bank_displayed(self, response):
|
||
self.assertContains(response, 'name="gateway" value="bank"')
|
||
|
||
def _assert_pricing_has_been_updated(self, response):
|
||
self.assertContains(response, 'Pricing has been updated')
|
||
self._assert_billing_details_form_displayed(response)
|
||
|
||
def _assert_plan_selector_displayed(self, response):
|
||
self.assertContains(response, 'Step 1: Configure your plan.', html=True)
|
||
self.assertContains(
|
||
response,
|
||
'<option selected value="1" title="This subscription is renewed automatically. You can stop or cancel a subscription any time.">Automatic renewal</option>',
|
||
html=True,
|
||
)
|
||
self.assertContains(
|
||
response,
|
||
'<option value="2" title="This subscription is renewed manually. You can leave it on-hold, or renew it when convenient.">Manual renewal</option>',
|
||
html=True,
|
||
)
|
||
|
||
def _assert_team_plan_selector_displayed(self, response):
|
||
self.assertContains(response, 'Step 1: Configure your team plan.', html=True)
|
||
self.assertContains(
|
||
response,
|
||
'<option selected value="4" title="This subscription is renewed manually. You can leave it on-hold, or renew it when convenient.">Basic - Manual Renewal</option>',
|
||
html=True,
|
||
)
|
||
self.assertContains(
|
||
response,
|
||
'<option value="5" title="This subscription is renewed automatically. You can stop or cancel a subscription any time.">Bronze</option>',
|
||
html=True,
|
||
)
|
||
self.assertContains(
|
||
response,
|
||
'<option value="6" title="This subscription is renewed manually. You can leave it on-hold, or renew it when convenient.">Bronze - Manual Renewal</option>',
|
||
html=True,
|
||
)
|
||
self.assertContains(
|
||
response,
|
||
'<option value="7" title="This subscription is renewed manually. You can leave it on-hold, or renew it when convenient.">Silver</option>',
|
||
html=True,
|
||
)
|
||
self.assertContains(
|
||
response, '<span class="fw-bold x-team-seats">unlimited</span>', html=True
|
||
)
|
||
|
||
def _assert_continue_to_billing_displayed(self, response):
|
||
self.assertContains(response, 'Continue to Billing')
|
||
|
||
def _assert_continue_to_payment_displayed(self, response):
|
||
self.assertContains(response, 'Continue to Payment')
|
||
|
||
def _assert_plan_selector_with_sign_in_cta_displayed(self, response):
|
||
self._assert_plan_selector_displayed(response)
|
||
|
||
self.assertContains(response, 'Sign in with Blender ID')
|
||
self.assertNotContains(response, 'Continue to Payment')
|
||
self.assertNotContains(response, 'id_street_address')
|
||
self.assertNotContains(response, 'id_full_name')
|
||
|
||
def _assert_team_plan_selector_with_sign_in_cta_displayed(self, response):
|
||
self._assert_team_plan_selector_displayed(response)
|
||
|
||
self.assertContains(response, 'Sign in with Blender ID')
|
||
self.assertNotContains(response, 'Continue to Payment')
|
||
self.assertNotContains(response, 'id_street_address')
|
||
self.assertNotContains(response, 'id_full_name')
|
||
|
||
def _assert_default_variation_selected_no_tax_usd(self, response):
|
||
self._assert_plan_selector_no_tax(response)
|
||
self.assertContains(
|
||
response,
|
||
'<option selected data-renewal-period="1 month" data-currency-symbol="$" data-plan-id="1" data-price="11.50" data-next-url="/join/plan-variation/1/" value="1">Every 1 month</option>',
|
||
html=True,
|
||
)
|
||
self.assertContains(
|
||
response,
|
||
'<span class="x-price">$ 11.50</span>',
|
||
html=True,
|
||
)
|
||
|
||
def _assert_default_variation_selected_tax_21_eur(self, response):
|
||
self.assertContains(
|
||
response,
|
||
'<option selected data-renewal-period="1 month" data-currency-symbol="€" data-plan-id="1" data-price="11.50" data-price-tax="2.00" data-tax-rate="21" data-tax-display-name="VAT" data-next-url="/join/plan-variation/2/" value="2">Every 1 month</option>',
|
||
html=True,
|
||
)
|
||
self.assertContains(
|
||
response,
|
||
'<span class="x-price">€ 11.50</span>',
|
||
html=True,
|
||
)
|
||
self.assertContains(
|
||
response,
|
||
'<span class="x-price-tax">Inc. 21% VAT (€ 2.00)</span>',
|
||
html=True,
|
||
)
|
||
|
||
def _assert_default_team_variation_selected_tax_21_eur(self, response):
|
||
self.assertContains(
|
||
response,
|
||
'<option selected data-team-seats="unlimited" data-renewal-period="1 year" data-currency-symbol="€" data-plan-id="4" data-price="1200.00" data-price-tax="208.26" data-tax-rate="21" data-tax-display-name="VAT" data-next-url="/join/team/plan-variation/28/" value="28">Every 1 year</option>',
|
||
html=True,
|
||
)
|
||
self.assertContains(
|
||
response,
|
||
'<span class="x-price">€ 1200.00</span>',
|
||
html=True,
|
||
)
|
||
self.assertContains(
|
||
response,
|
||
'<span class="x-price-tax">Inc. 21% VAT (€ 208.26)</span>',
|
||
html=True,
|
||
)
|
||
|
||
def _assert_default_variation_selected_tax_20_eur(self, response):
|
||
with open('/tmp/response.html', 'wb+') as f:
|
||
f.write(response.content)
|
||
self.assertContains(
|
||
response,
|
||
'<option selected data-renewal-period="1 month" data-currency-symbol="€" data-plan-id="1" data-price="11.50" data-price-tax="1.92" data-tax-rate="20" data-tax-display-name="VAT" data-next-url="/join/plan-variation/2/" value="2">Every 1 month</option>',
|
||
html=True,
|
||
)
|
||
self.assertContains(
|
||
response,
|
||
'<span class="x-price">€ 11.50</span>',
|
||
html=True,
|
||
)
|
||
self.assertContains(
|
||
response,
|
||
'<span class="x-price-tax">Inc. 20% VAT (€ 1.92)</span>',
|
||
html=True,
|
||
)
|
||
|
||
def _assert_default_variation_selected_tax_19_eur(self, response):
|
||
self.assertContains(
|
||
response,
|
||
'<option selected data-renewal-period="1 month" data-currency-symbol="€" data-plan-id="1" data-price="11.50" data-price-tax="1.84" data-tax-rate="19" data-tax-display-name="VAT" data-next-url="/join/plan-variation/2/" value="2">Every 1 month</option>',
|
||
html=True,
|
||
)
|
||
self.assertContains(
|
||
response,
|
||
'<span class="x-price">€ 11.50</span>',
|
||
html=True,
|
||
)
|
||
self.assertContains(
|
||
response,
|
||
'<span class="x-price-tax">Inc. 19% VAT (€ 1.84)</span>',
|
||
html=True,
|
||
)
|
||
|
||
def _assert_total_default_variation_selected_eur(self, response):
|
||
self.assertContains(response, '<h3 class="mb-0">Total</h3>', html=True)
|
||
self.assertContains(response, '<span class="x-price">€ 11.50</span>', html=True)
|
||
self.assertContains(response, '/ <span class="x-price-period">1 month</span>', html=True)
|
||
|
||
def _assert_total_default_variation_selected_no_tax_eur(self, response):
|
||
self._assert_total_default_variation_selected_eur(response)
|
||
self.assertContains(response, 'Automatic ')
|
||
self.assertContains(response, '/ <span class="x-price-period">1 month</span>', html=True)
|
||
|
||
self.assertContains(response, '<h3 class="mb-0">Total</h3>', html=True)
|
||
self.assertContains(response, '<span class="x-price">€ 11.50</span>', html=True)
|
||
self.assertNotContains(response, 'Inc.')
|
||
|
||
def _assert_total_default_variation_selected_tax_21_eur(self, response):
|
||
self._assert_total_default_variation_selected_eur(response)
|
||
self.assertContains(
|
||
response, '<span class="x-price-tax">Inc. 21% VAT (€ 2.00)</span>', html=True
|
||
)
|
||
self.assertContains(response, 'Automatic ')
|
||
self.assertContains(response, '/ <span class="x-price-period">1 month</span>', html=True)
|
||
|
||
def _assert_total_default_variation_selected_tax_19_eur(self, response):
|
||
self._assert_total_default_variation_selected_eur(response)
|
||
self.assertContains(
|
||
response, '<span class="x-price-tax">Inc. 19% VAT (€ 1.84)</span>', html=True
|
||
)
|
||
self.assertContains(response, 'Automatic ')
|
||
self.assertContains(response, '/ <span class="x-price-period">1 month</span>', html=True)
|
||
|
||
def _assert_total_default_variation_selected_tax_19_eur_reverse_charged(self, response):
|
||
self.assertContains(response, '<h3 class="mb-0">Total</h3>', html=True)
|
||
self.assertContains(response, '<span class="x-price">€ 9.66</span>', html=True)
|
||
|
||
def _assert_total_default_variation_selected_tax_21_eur_reverse_charged(self, response):
|
||
self.assertContains(response, '<h3 class="mb-0">Total</h3>', html=True)
|
||
self.assertContains(response, '<span class="x-price">€ 8.18</span>', html=True)
|
||
|
||
def _assert_total_default_variation_selected_usd(self, response):
|
||
self.assertContains(response, '<h3 class="mb-0">Total</h3>', html=True)
|
||
self.assertContains(response, '<span class="x-price">$ 11.50</span>', html=True)
|
||
self.assertContains(response, 'Automatic ')
|
||
self.assertNotContains(response, 'Inc.')
|
||
|
||
def _assert_plan_selector_no_tax(self, response):
|
||
self.assertNotContains(response, 'Inc. ')
|
||
self.assertContains(
|
||
response,
|
||
'<span class="x-price-tax"></span>',
|
||
html=True,
|
||
)
|
||
|
||
def _assert_form_us_address_is_displayed(self, response):
|
||
self.assertContains(
|
||
response,
|
||
'<option value="US" selected>United States of America</option>',
|
||
html=True,
|
||
)
|
||
self.assertContains(
|
||
response,
|
||
'<option value="NY" selected>New York</option>',
|
||
html=True,
|
||
)
|
||
self.assertContains(
|
||
response,
|
||
'<input type="text" name="postal_code" value="12001" maxlength="255" placeholder="ZIP/Postal code" class="form-control" id="id_postal_code">',
|
||
html=True,
|
||
)
|
||
|
||
def _assert_transactionless_done_page_displayed(self, response_redirect):
|
||
# Catch unexpected form errors so that they are displayed
|
||
# FIXME(anna): context is modified by the code that renders email, cannot access the form
|
||
# self.assertEqual(
|
||
# response_redirect.context['form'].errors if response_redirect.context else {},
|
||
# {},
|
||
# )
|
||
self.assertEqual(response_redirect.status_code, 302)
|
||
# Follow the redirect
|
||
response = self.client.get(response_redirect['Location'])
|
||
self.assertContains(response, '<h2 class="h3">Bank details:</h2>', html=True)
|
||
self.assertContains(response, 'on hold')
|
||
self.assertContains(response, 'NL07 INGB 0008 4489 82')
|
||
subscription = response.wsgi_request.user.customer.subscription_set.first()
|
||
self.assertContains(
|
||
response, f'Blender Studio order-{subscription.latest_order().display_number}'
|
||
)
|
||
|
||
def _assert_done_page_displayed(self, response_redirect):
|
||
# Catch unexpected form errors so that they are displayed
|
||
# FIXME(anna): context is modified by the code that renders email, cannot access the form
|
||
# self.assertEqual(
|
||
# response_redirect.context['form'].errors if response_redirect.context else {},
|
||
# {},
|
||
# )
|
||
self.assertEqual(response_redirect.status_code, 302)
|
||
# Follow the redirect
|
||
response = self.client.get(response_redirect['Location'])
|
||
self.assertContains(response, 'Welcome to Blender Studio')
|
||
self.assertNotContains(response, 'Bank details')
|
||
|
||
def _assert_no_emails_sent(self):
|
||
self.assertEqual(len(mail.outbox), 0)
|
||
|
||
def _assert_bank_transfer_email_is_sent(self, subscription):
|
||
customer = subscription.customer
|
||
self.assertEqual(len(mail.outbox), 1)
|
||
_write_mail(mail)
|
||
email = mail.outbox[0]
|
||
self.assertEqual(email.to, [customer.billing_address.email])
|
||
# TODO(anna): set the correct reply_to
|
||
self.assertEqual(email.reply_to, [])
|
||
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
||
self.assertEqual(email.from_email, 'webmaster@localhost')
|
||
self.assertEqual(email.subject, 'Blender Studio Subscription Bank Payment')
|
||
self.assertEqual(email.alternatives[0][1], 'text/html')
|
||
for email_body in (email.body, email.alternatives[0][0]):
|
||
self.assertIn(
|
||
f'Blender Studio order-{subscription.latest_order().number}',
|
||
email_body,
|
||
)
|
||
self.assertIn('NL07 INGB 0008 4489 82', email_body)
|
||
self.assertIn('Dear Jane Doe,', email_body)
|
||
self.assertIn(reverse('user-settings-billing'), email_body)
|
||
self.assertIn('Manual renewal subscription', email_body)
|
||
self.assertIn('is currently on hold', email_body)
|
||
|
||
def _assert_bank_transfer_email_is_sent_tax_21(self, subscription):
|
||
email = mail.outbox[0]
|
||
for email_body in (email.body, email.alternatives[0][0]):
|
||
self.assertIn('€\xa017.00 per month', email_body)
|
||
self.assertIn('Inc. 21% VAT', email_body)
|
||
self.assertIn('Please send your payment of €\xa017.00 to', email_body)
|
||
self.assertIn('Recurring total: €\xa017.00', email_body.replace(' ', ' '))
|
||
|
||
def _assert_bank_transfer_email_is_sent_tax_21_eur_reverse_charged(self, subscription):
|
||
email = mail.outbox[0]
|
||
for email_body in (email.body, email.alternatives[0][0]):
|
||
# "Original" subscription price must not be displayed anywhere, only the tax-exc one
|
||
self.assertNotIn('32.00', email_body)
|
||
self.assertNotIn('21%', email_body)
|
||
self.assertNotIn('Inc.', email_body)
|
||
self.assertNotIn('VAT', email_body)
|
||
self.assertIn('€\xa030.58 per 3 months', email_body)
|
||
self.assertIn('Please send your payment of €\xa030.58 to', email_body)
|
||
self.assertIn('Recurring total: €\xa030.58', email_body.replace(' ', ' '))
|
||
|
||
def _assert_subscription_activated_email_is_sent(self, subscription):
|
||
customer = subscription.customer
|
||
self.assertEqual(len(mail.outbox), 1)
|
||
_write_mail(mail)
|
||
email = mail.outbox[0]
|
||
self.assertEqual(email.to, [customer.billing_address.email])
|
||
# TODO(anna): set the correct reply_to
|
||
self.assertEqual(email.reply_to, [])
|
||
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
||
self.assertEqual(email.from_email, 'webmaster@localhost')
|
||
self.assertEqual(email.subject, 'Blender Studio Subscription Activated')
|
||
self.assertEqual(email.alternatives[0][1], 'text/html')
|
||
for email_body in (email.body, email.alternatives[0][0]):
|
||
self.assertIn('activated', email_body)
|
||
self.assertIn(f'Dear {customer.billing_address.full_name},', email_body)
|
||
self.assertIn(reverse('user-settings-billing'), email_body)
|
||
self.assertIn('Automatic renewal subscription', email_body)
|
||
self.assertIn('Blender Studio Team', email_body)
|
||
|
||
def _assert_team_subscription_activated_email_is_sent(self, subscription):
|
||
customer = subscription.customer
|
||
self.assertEqual(len(mail.outbox), 1)
|
||
_write_mail(mail)
|
||
email = mail.outbox[0]
|
||
self.assertEqual(email.to, [customer.billing_address.email])
|
||
# TODO(anna): set the correct reply_to
|
||
self.assertEqual(email.reply_to, [])
|
||
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
||
self.assertEqual(email.from_email, 'webmaster@localhost')
|
||
self.assertEqual(email.subject, 'Blender Studio Subscription Activated')
|
||
self.assertEqual(email.alternatives[0][1], 'text/html')
|
||
for email_body in (email.body, email.alternatives[0][0]):
|
||
self.assertIn('activated', email_body)
|
||
self.assertIn(f'Dear {customer.billing_address.full_name},', email_body)
|
||
self.assertIn(reverse('user-settings-billing'), email_body)
|
||
self.assertIn('Your Silver subscription has been activated', email_body)
|
||
self.assertIn('Blender Studio Team', email_body)
|
||
|
||
def _assert_subscription_deactivated_email_is_sent(self, subscription):
|
||
customer = subscription.customer
|
||
self.assertEqual(len(mail.outbox), 1)
|
||
_write_mail(mail)
|
||
email = mail.outbox[0]
|
||
self.assertEqual(email.to, [customer.billing_address.email])
|
||
# TODO(anna): set the correct reply_to
|
||
self.assertEqual(email.reply_to, [])
|
||
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
||
self.assertEqual(email.from_email, 'webmaster@localhost')
|
||
self.assertEqual(email.subject, 'Blender Studio Subscription Deactivated')
|
||
self.assertEqual(email.alternatives[0][1], 'text/html')
|
||
for email_body in (email.body, email.alternatives[0][0]):
|
||
self.assertIn('deactivated', email_body)
|
||
self.assertIn('Dear Алексей Н.,', email_body)
|
||
self.assertIn(reverse('user-settings-billing'), email_body)
|
||
self.assertIn('Blender Studio Team', email_body)
|
||
|
||
def _assert_payment_soft_failed_email_is_sent(self, subscription):
|
||
customer = subscription.customer
|
||
user = customer.user
|
||
self.assertEqual(len(mail.outbox), 1)
|
||
_write_mail(mail)
|
||
email = mail.outbox[0]
|
||
self.assertEqual(email.to, [user.customer.billing_address.email])
|
||
# TODO(anna): set the correct reply_to
|
||
self.assertEqual(email.reply_to, [])
|
||
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
||
self.assertEqual(email.from_email, 'webmaster@localhost')
|
||
self.assertEqual(
|
||
email.subject, "Blender Studio Subscription: payment failed (but we'll try again)"
|
||
)
|
||
self.assertEqual(email.alternatives[0][1], 'text/html')
|
||
for email_body in (email.body, email.alternatives[0][0]):
|
||
self.assertIn(f'Dear {user.customer.billing_address.full_name},', email_body)
|
||
self.assertIn('Automatic payment', email_body)
|
||
self.assertIn('failed', email_body)
|
||
self.assertIn('try again', email_body)
|
||
self.assertIn('1 of 3', email_body)
|
||
self.assertIn(
|
||
reverse(
|
||
'subscriptions:pay-existing-order',
|
||
kwargs={'order_id': subscription.latest_order().pk},
|
||
),
|
||
email_body,
|
||
)
|
||
self.assertIn(reverse('user-settings-billing'), email_body)
|
||
self.assertIn('Blender Studio Team', email_body)
|
||
|
||
def _assert_payment_failed_email_is_sent(self, subscription):
|
||
customer = subscription.customer
|
||
user = customer.user
|
||
self.assertEqual(len(mail.outbox), 1)
|
||
_write_mail(mail)
|
||
email = mail.outbox[0]
|
||
self.assertEqual(email.to, [user.customer.billing_address.email])
|
||
# TODO(anna): set the correct reply_to
|
||
self.assertEqual(email.reply_to, [])
|
||
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
||
self.assertEqual(email.from_email, 'webmaster@localhost')
|
||
self.assertEqual(email.subject, 'Blender Studio Subscription: payment failed')
|
||
self.assertEqual(email.alternatives[0][1], 'text/html')
|
||
for email_body in (email.body, email.alternatives[0][0]):
|
||
self.assertIn(f'Dear {user.customer.billing_address.full_name},', email_body)
|
||
self.assertIn('Automatic payment', email_body)
|
||
self.assertIn('failed', email_body)
|
||
self.assertIn('3 times', email_body)
|
||
self.assertIn(
|
||
reverse(
|
||
'subscriptions:pay-existing-order',
|
||
kwargs={'order_id': subscription.latest_order().pk},
|
||
),
|
||
email_body,
|
||
)
|
||
self.assertIn(reverse('user-settings-billing'), email_body)
|
||
self.assertIn('Blender Studio Team', email_body)
|
||
|
||
def _assert_payment_paid_email_is_sent(self, subscription):
|
||
customer = subscription.customer
|
||
user = customer.user
|
||
self.assertEqual(len(mail.outbox), 1)
|
||
_write_mail(mail)
|
||
email = mail.outbox[0]
|
||
self.assertEqual(email.to, [user.customer.billing_address.email])
|
||
# TODO(anna): set the correct reply_to
|
||
self.assertEqual(email.reply_to, [])
|
||
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
||
self.assertEqual(email.from_email, 'webmaster@localhost')
|
||
self.assertEqual(email.subject, 'Blender Studio Subscription: payment received')
|
||
self.assertEqual(email.alternatives[0][1], 'text/html')
|
||
for email_body in (email.body, email.alternatives[0][0]):
|
||
self.assertIn(f'Dear {user.customer.billing_address.full_name},', email_body)
|
||
self.assertIn('Automatic monthly payment', email_body)
|
||
self.assertIn('successful', email_body)
|
||
self.assertIn('$\xa011.10', email_body)
|
||
self.assertIn(
|
||
reverse(
|
||
'subscriptions:receipt', kwargs={'order_id': subscription.latest_order().pk}
|
||
),
|
||
email_body,
|
||
)
|
||
self.assertIn(reverse('user-settings-billing'), email_body)
|
||
self.assertIn('Blender Studio Team', email_body)
|
||
|
||
def _assert_managed_subscription_notification_email_is_sent(self, subscription):
|
||
customer = subscription.customer
|
||
user = customer.user
|
||
self.assertEqual(len(mail.outbox), 1)
|
||
_write_mail(mail)
|
||
email = mail.outbox[0]
|
||
self.assertEqual(email.to, ['admin@example.com'])
|
||
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
||
self.assertEqual(email.from_email, 'webmaster@localhost')
|
||
self.assertEqual(email.subject, 'Blender Studio managed subscription needs attention')
|
||
self.assertEqual(email.alternatives[0][1], 'text/html')
|
||
for email_body in (email.body, email.alternatives[0][0]):
|
||
self.assertIn(f'{user.customer.billing_address.full_name} has', email_body)
|
||
self.assertIn('its next payment date', email_body)
|
||
self.assertIn('$\xa011.10', email_body)
|
||
self.assertIn(
|
||
f'/admin/looper/subscription/{subscription.pk}/change',
|
||
email_body,
|
||
)
|
||
|
||
def _assert_subscription_expired_email_is_sent(self, subscription):
|
||
customer = subscription.customer
|
||
user = customer.user
|
||
self.assertEqual(len(mail.outbox), 1)
|
||
_write_mail(mail)
|
||
email = mail.outbox[0]
|
||
self.assertEqual(email.to, [user.email])
|
||
self.assertEqual(email.from_email, 'webmaster@localhost')
|
||
self.assertEqual(email.subject, 'We miss you at Blender Studio')
|
||
self.assertEqual(email.alternatives[0][1], 'text/html')
|
||
for email_body in (email.body, email.alternatives[0][0]):
|
||
self.assertIn(f'Dear {user.customer.billing_address.full_name}', email_body)
|
||
self.assertIn(f'#{subscription.pk}', email_body)
|
||
self.assertIn('has expired', email_body)
|
||
self.assertIn(
|
||
'/join/?source=subscription_expired_email',
|
||
email_body,
|
||
)
|