diff --git a/HISTORY.rst b/HISTORY.rst index a79c4c7258..86a38e86b9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -8,7 +8,7 @@ History * Support for Django 1.6 and lower is now deprecated. * Improved test harness now tests coverage and pep8 -* SubscribeFormView no longer populates self.error with form errors +* SubscribeFormView and ChangePlanView no longer populate self.error with form errors 0.5.0 (2015-05-25) --------------------- diff --git a/djstripe/settings.py b/djstripe/settings.py index 99dcd7e529..66d422d582 100644 --- a/djstripe/settings.py +++ b/djstripe/settings.py @@ -25,7 +25,7 @@ PASSWORD_MIN_LENGTH = getattr(settings, 'DJSTRIPE_PASSWORD_MIN_LENGTH', 6) PRORATION_POLICY = getattr(settings, 'DJSTRIPE_PRORATION_POLICY', False) -PRORATION_POLICY_FOR_UPGRADES = getattr(settings, 'DJSTRIPE_PRORATION_POLICY_FOR_UPGRADES', False) +get_proration_policy_for_upgrades = (lambda: getattr(settings, 'DJSTRIPE_PRORATION_POLICY_FOR_UPGRADES', False)) CANCELLATION_AT_PERIOD_END = not PRORATION_POLICY # TODO - need to find a better way to do this SEND_INVOICE_RECEIPT_EMAILS = getattr(settings, "DJSTRIPE_SEND_INVOICE_RECEIPT_EMAILS", True) diff --git a/djstripe/views.py b/djstripe/views.py index 83061194eb..252e38a9b9 100644 --- a/djstripe/views.py +++ b/djstripe/views.py @@ -29,7 +29,7 @@ from .settings import PLAN_LIST from .settings import CANCELLATION_AT_PERIOD_END from .settings import subscriber_request_callback -from .settings import PRORATION_POLICY_FOR_UPGRADES +from .settings import get_proration_policy_for_upgrades from .sync import sync_subscriber @@ -163,7 +163,11 @@ def post(self, request, *args, **kwargs): class ChangePlanView(LoginRequiredMixin, FormValidMessageMixin, SubscriptionMixin, FormView): - # TODO - needs tests + """ + TODO: This logic should be in form_valid() instead of post(). + + Also, this should be combined with SubscribeFormView. + """ form_class = PlanForm template_name = "djstripe/subscribe_form.html" @@ -172,22 +176,24 @@ class ChangePlanView(LoginRequiredMixin, FormValidMessageMixin, SubscriptionMixi def post(self, request, *args, **kwargs): form = PlanForm(request.POST) - customer = subscriber_request_callback(request).customer + try: + customer = subscriber_request_callback(request).customer + except Customer.DoesNotExist as exc: + form.add_error(None, "You must already be subscribed to a plan before you can change it.") + return self.form_invalid(form) + if form.is_valid(): try: - # When a customer upgrades their plan, and PRORATION_POLICY_FOR_UPGRADES is set to True, - # then we force the proration of his current plan and use it towards the upgraded plan, - # no matter what PRORATION_POLICY is set to. - if PRORATION_POLICY_FOR_UPGRADES: + # When a customer upgrades their plan, and DJSTRIPE_PRORATION_POLICY_FOR_UPGRADES is set to True, + # we force the proration of the current plan and use it towards the upgraded plan, + # no matter what DJSTRIPE_PRORATION_POLICY is set to. + if get_proration_policy_for_upgrades(): current_subscription_amount = customer.current_subscription.amount selected_plan_name = form.cleaned_data["plan"] selected_plan = next( (plan for plan in PLAN_LIST if plan["plan"] == selected_plan_name) ) - selected_plan_price = selected_plan["price"] - - if not isinstance(selected_plan["price"], decimal.Decimal): - selected_plan_price = selected_plan["price"] / decimal.Decimal("100") + selected_plan_price = selected_plan["price"] / decimal.Decimal("100") # Is it an upgrade? if selected_plan_price > current_subscription_amount: @@ -196,11 +202,9 @@ def post(self, request, *args, **kwargs): customer.subscribe(selected_plan_name) else: customer.subscribe(form.cleaned_data["plan"]) - except stripe.StripeError as e: - self.error = e.message + except stripe.StripeError as exc: + form.add_error(None, str(exc)) return self.form_invalid(form) - except Exception as e: - raise e return self.form_valid(form) else: return self.form_invalid(form) diff --git a/runtests.py b/runtests.py index fa46b5e684..a6acd2c6e6 100644 --- a/runtests.py +++ b/runtests.py @@ -73,6 +73,14 @@ "currency": "usd", "interval": "month" }, + "test3": { + "stripe_plan_id": "test_id_3", + "name": "Test Plan 3", + "description": "Test plan for deletion. You'll have to sync plans manually in your test to ensure this plan exists.", + "price": 5000, # $50.00 + "currency": "usd", + "interval": "month" + }, "unidentified_test_plan": { "name": "Unidentified Test Plan", "description": "A test plan with no ID.", diff --git a/tests/test_integrations/test_views.py b/tests/test_integrations/test_views.py index 6c9e6e2453..f604a03fef 100644 --- a/tests/test_integrations/test_views.py +++ b/tests/test_integrations/test_views.py @@ -12,10 +12,12 @@ from django.core.urlresolvers import reverse from django.test.client import RequestFactory from django.test.testcases import TestCase +from django.test.utils import override_settings from djstripe import settings as djstripe_settings from djstripe.models import Customer from djstripe.views import ChangeCardView, HistoryView +from djstripe.sync import sync_plans if settings.STRIPE_PUBLIC_KEY and settings.STRIPE_SECRET_KEY: @@ -219,3 +221,99 @@ def test_post_form_invalid(self): self.assertEqual(200, response.status_code) self.assertIn("plan", response.context["form"].errors) self.assertIn("This field is required.", response.context["form"].errors["plan"]) + + class ChangePlanViewTest(TestCase): + + def setUp(self): + self.url = reverse("djstripe:change_plan") + self.user1 = get_user_model().objects.create_user(username="testuser1", + email="test@example.com", + password="123") + self.user2 = get_user_model().objects.create_user(username="testuser2", + email="test@example.com", + password="123") + + token = stripe.Token.create( + card={ + "number": '4242424242424242', + "exp_month": 12, + "exp_year": 2016, + "cvc": '123' + }, + ) + + customer, created = Customer.get_or_create(subscriber=self.user1) + customer.update_card(token.id) + customer.subscribe("test") + + def test_post_new_sub_no_proration(self): + self.assertTrue(self.client.login(username="testuser2", password="123")) + response = self.client.post(self.url, {"plan": "test0"}) + self.assertEqual(200, response.status_code) + self.assertIn("form", response.context) + self.assertIn("You must already be subscribed to a plan before you can change it.", response.context["form"].errors["__all__"]) + + def test_change_sub_no_proration(self): + self.assertTrue(self.client.login(username="testuser1", password="123")) + response = self.client.post(self.url, {"plan": "test0"}) + self.assertRedirects(response, reverse("djstripe:history")) + + customer = Customer.objects.get(subscriber=self.user1) + self.assertEqual("test0", customer.current_subscription.plan) + customer.subscribe("test") # revert + + def test_post_form_invalid(self): + self.assertTrue(self.client.login(username="testuser1", password="123")) + response = self.client.post(self.url) + self.assertEqual(200, response.status_code) + self.assertIn("plan", response.context["form"].errors) + self.assertIn("This field is required.", response.context["form"].errors["plan"]) + + @override_settings(DJSTRIPE_PRORATION_POLICY_FOR_UPGRADES=True) + def test_change_sub_with_proration_downgrade(self): + self.assertTrue(self.client.login(username="testuser1", password="123")) + response = self.client.post(self.url, {"plan": "test0"}) + self.assertRedirects(response, reverse("djstripe:history")) + + customer = Customer.objects.get(subscriber=self.user1) + self.assertEqual("test0", customer.current_subscription.plan) + customer.subscribe("test") # revert + + @override_settings(DJSTRIPE_PRORATION_POLICY_FOR_UPGRADES=True) + def test_change_sub_with_proration_upgrade(self): + self.assertTrue(self.client.login(username="testuser1", password="123")) + response = self.client.post(self.url, {"plan": "test2"}) + self.assertRedirects(response, reverse("djstripe:history")) + + customer = Customer.objects.get(subscriber=self.user1) + self.assertEqual("test2", customer.current_subscription.plan) + customer.subscribe("test") # revert + + def test_change_sub_same_plan(self): + self.assertTrue(self.client.login(username="testuser1", password="123")) + response = self.client.post(self.url, {"plan": "test"}) + self.assertRedirects(response, reverse("djstripe:history")) + + customer = Customer.objects.get(subscriber=self.user1) + self.assertEqual("test", customer.current_subscription.plan) + + @override_settings(DJSTRIPE_PRORATION_POLICY_FOR_UPGRADES=True) + def test_change_sub_with_proration_same_plan(self): + self.assertTrue(self.client.login(username="testuser1", password="123")) + response = self.client.post(self.url, {"plan": "test"}) + self.assertRedirects(response, reverse("djstripe:history")) + + customer = Customer.objects.get(subscriber=self.user1) + self.assertEqual("test", customer.current_subscription.plan) + + def test_change_sub_stripe_error(self): + self.assertTrue(self.client.login(username="testuser1", password="123")) + + sync_plans() + plan = stripe.Plan.retrieve("test_id_3") + plan.delete() + + response = self.client.post(self.url, {"plan": "test3"}) + self.assertEqual(200, response.status_code) + self.assertIn("form", response.context) + self.assertIn("No such plan: test_id_3", response.context["form"].errors["__all__"])