diff --git a/readthedocs/organizations/models.py b/readthedocs/organizations/models.py
index 11853d04936..886d722b564 100644
--- a/readthedocs/organizations/models.py
+++ b/readthedocs/organizations/models.py
@@ -115,6 +115,17 @@ class Meta:
def __str__(self):
return self.name
+ @property
+ def stripe_subscription(self):
+ # TODO: remove this once we don't depend on our Subscription models.
+ from readthedocs.subscriptions.models import Subscription
+
+ subscription = Subscription.objects.get_or_create_default_subscription(self)
+ if not subscription:
+ # This only happens during development.
+ return None
+ return self.stripe_customer.subscriptions.latest()
+
def get_absolute_url(self):
return reverse('organization_detail', args=(self.slug,))
diff --git a/readthedocs/subscriptions/templates/subscriptions/subscription_detail.html b/readthedocs/subscriptions/templates/subscriptions/subscription_detail.html
index afcef56d695..1ec8f6009ae 100644
--- a/readthedocs/subscriptions/templates/subscriptions/subscription_detail.html
+++ b/readthedocs/subscriptions/templates/subscriptions/subscription_detail.html
@@ -12,12 +12,12 @@
{% block edit_content %}
- {% if subscription.status == 'trialing' %}
+ {% if stripe_subscription.status == 'trialing' %}
-
{% url 'pricing' as pricing_url %}
{% blocktrans trimmed with pricing_url=pricing_url %}
- You are currently on a trial.
+ You are currently on a trial.
Please choose a paid plan that fits your organization
prior to the end of your trial.
Upgrade your account by clicking on Manage Subscription below.
@@ -26,22 +26,22 @@
{% endif %}
- {% if subscription and subscription.plan and subscription.status != 'canceled' and organization.stripe_id %}
+ {% if stripe_subscription.status != 'canceled' %}
- {% trans "Plan" %}:
-
- {{ subscription.plan.name }}
+ {{ stripe_price.product.name }}
- (${{ subscription.plan.price }} {% trans "per month" %})
+ ({{ stripe_price.human_readable_price }})
- {% if subscription.plan.features.exists %}
+ {% if features %}
- {% trans "Plan Features:" %}
-
- {% for feature in subscription.plan.features.all %}
+ {% for feature in features %}
{{ feature.description_display }}
@@ -49,27 +49,27 @@
{% endif %}
- {% if subscription.start_date %}
+ {% if stripe_subscription.start_date %}
- {% trans "Signed up" %}:
-
- {{ subscription.start_date|timesince }} {% trans "ago" %}
+ {{ stripe_subscription.start_date|timesince }} {% trans "ago" %}
{% endif %}
- {% trans "Subscription Status" %}:
-
- {{ subscription.get_status_display }}
+ {{ stripe_subscription.get_status_display }}
- {% if not subscription.is_trial_ended %}
+ {% if stripe_subscription.status == 'trialing' and stripe_subscription.trial_end %}
- {% trans "Trial ends" %}:
-
- {{ subscription.trial_end_date|date:"SHORT_DATE_FORMAT" }}
+ {{ stripe_subscription.trial_end|date:"SHORT_DATE_FORMAT" }}
- {% elif subscription.end_date %}
+ {% elif subscription_end_date %}
- {% trans "Subscription ends" %}:
-
- {{ subscription.end_date|date:"SHORT_DATE_FORMAT" }}
+ {{ subscription_end_date|date:"SHORT_DATE_FORMAT" }}
{% endif %}
diff --git a/readthedocs/subscriptions/tests/test_views.py b/readthedocs/subscriptions/tests/test_views.py
index e7bacf9fec0..4f366f2363f 100644
--- a/readthedocs/subscriptions/tests/test_views.py
+++ b/readthedocs/subscriptions/tests/test_views.py
@@ -23,6 +23,14 @@ def setUp(self):
self.user = get(User)
self.organization = get(Organization, stripe_id='123', owners=[self.user])
self.plan = get(Plan, published=True, slug=settings.ORG_DEFAULT_SUBSCRIPTION_PLAN_SLUG)
+ self.stripe_subscription = self._create_stripe_subscription(
+ customer_id=self.organization.stripe_id,
+ subscription_id="sub_a1b2c3d4",
+ )
+ self.stripe_customer = self.stripe_subscription.customer
+
+ self.organization.stripe_customer = self.stripe_customer
+ self.organization.save()
self.subscription = get(
Subscription,
organization=self.organization,
@@ -31,11 +39,38 @@ def setUp(self):
)
self.client.force_login(self.user)
+ def _create_stripe_subscription(
+ self, customer_id="cus_a1b2c3", subscription_id="sub_a1b2c3"
+ ):
+ stripe_customer = get(
+ djstripe.Customer,
+ id=customer_id,
+ )
+ stripe_subscription = get(
+ djstripe.Subscription,
+ id=subscription_id,
+ start_date=timezone.now(),
+ current_period_end=timezone.now() + timezone.timedelta(days=30),
+ trial_end=timezone.now() + timezone.timedelta(days=30),
+ status=SubscriptionStatus.active,
+ customer=stripe_customer,
+ )
+ stripe_price = get(
+ djstripe.Price,
+ unit_amount=50000,
+ )
+ stripe_item = get(
+ djstripe.SubscriptionItem,
+ price=stripe_price,
+ subscription=stripe_subscription,
+ )
+ return stripe_subscription
+
def test_active_subscription(self):
resp = self.client.get(reverse('subscription_detail', args=[self.organization.slug]))
self.assertEqual(resp.status_code, 200)
- self.assertEqual(resp.context['subscription'], self.subscription)
- self.assertContains(resp, 'active')
+ self.assertEqual(resp.context["stripe_subscription"], self.stripe_subscription)
+ self.assertContains(resp, "active")
# The subscribe form isn't shown, but the manage susbcription button is.
self.assertContains(resp, 'Manage Subscription')
self.assertNotContains(resp, 'Create Subscription')
@@ -66,25 +101,16 @@ def test_manage_subscription(self, mock_request):
def test_user_without_subscription(
self, customer_create_mock, customer_retrieve_mock
):
- stripe_customer = get(
- djstripe.Customer,
- id="cus_a1b2c3",
- )
- stripe_subscription = get(
- djstripe.Subscription,
- id="sub_a1b2c3",
- start_date=timezone.now(),
- current_period_end=timezone.now() + timezone.timedelta(days=30),
- trial_end=timezone.now() + timezone.timedelta(days=30),
- status=SubscriptionStatus.active,
- customer=stripe_customer,
- )
+ stripe_subscription = self._create_stripe_subscription()
+ stripe_customer = stripe_subscription.customer
stripe_customer.subscribe = mock.MagicMock()
stripe_customer.subscribe.return_value = stripe_subscription
customer_retrieve_mock.return_value = stripe_customer
- self.subscription.delete()
self.organization.refresh_from_db()
+ self.organization.stripe_customer = None
+ self.organization.save()
+ self.subscription.delete()
self.assertFalse(hasattr(self.organization, 'subscription'))
self.assertIsNone(self.organization.stripe_customer)
@@ -106,19 +132,8 @@ def test_user_without_subscription(
def test_user_without_subscription_and_customer(
self, customer_create_mock, customer_retrieve_mock, sync_from_stripe_data_mock
):
- stripe_customer = get(
- djstripe.Customer,
- id="cus_a1b2c3",
- )
- stripe_subscription = get(
- djstripe.Subscription,
- id="sub_a1b2c3",
- start_date=timezone.now(),
- current_period_end=timezone.now() + timezone.timedelta(days=30),
- trial_end=timezone.now() + timezone.timedelta(days=30),
- status=SubscriptionStatus.active,
- customer=stripe_customer,
- )
+ stripe_subscription = self._create_stripe_subscription()
+ stripe_customer = stripe_subscription.customer
stripe_customer.subscribe = mock.MagicMock()
stripe_customer.subscribe.return_value = stripe_subscription
customer_retrieve_mock.return_value = None
@@ -126,6 +141,7 @@ def test_user_without_subscription_and_customer(
# When stripe_id is None, a new customer is created.
self.organization.stripe_id = None
+ self.organization.stripe_customer = None
self.organization.save()
self.subscription.delete()
self.organization.refresh_from_db()
@@ -147,10 +163,12 @@ def test_user_without_subscription_and_customer(
def test_user_with_canceled_subscription(self):
self.subscription.status = 'canceled'
+ self.stripe_subscription.status = SubscriptionStatus.canceled
+ self.stripe_subscription.save()
self.subscription.save()
resp = self.client.get(reverse('subscription_detail', args=[self.organization.slug]))
self.assertEqual(resp.status_code, 200)
- self.assertEqual(resp.context['subscription'], self.subscription)
+ self.assertEqual(resp.context["stripe_subscription"], self.stripe_subscription)
# The Manage Subscription form isn't shown, but the Subscribe is.
self.assertNotContains(resp, 'Manage Subscription')
self.assertContains(resp, 'Create Subscription')
diff --git a/readthedocs/subscriptions/views.py b/readthedocs/subscriptions/views.py
index 175533e31f5..0774c303f6e 100644
--- a/readthedocs/subscriptions/views.py
+++ b/readthedocs/subscriptions/views.py
@@ -9,11 +9,13 @@
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
+from djstripe import models as djstripe
+from djstripe.enums import SubscriptionStatus
from vanilla import DetailView, GenericView
from readthedocs.organizations.views.base import OrganizationMixin
from readthedocs.subscriptions.forms import PlanForm
-from readthedocs.subscriptions.models import Plan, Subscription
+from readthedocs.subscriptions.models import Plan
from readthedocs.subscriptions.utils import get_or_create_stripe_customer
log = structlog.get_logger(__name__)
@@ -23,9 +25,10 @@ class DetailSubscription(OrganizationMixin, DetailView):
"""Detail for the subscription of a organization."""
- model = Subscription
+ model = djstripe.Subscription
form_class = PlanForm
- template_name = 'subscriptions/subscription_detail.html'
+ template_name = "subscriptions/subscription_detail.html"
+ context_object_name = "stripe_subscription"
def get(self, request, *args, **kwargs):
super().get(request, *args, **kwargs)
@@ -56,8 +59,11 @@ def redirect_to_checkout(self, form):
Users can buy a new subscription if the current one
has been deleted after they canceled it.
"""
- subscription = self.get_object()
- if not subscription or subscription.status != 'canceled':
+ stripe_subscription = self.get_object()
+ if (
+ not stripe_subscription
+ or stripe_subscription.status != SubscriptionStatus.canceled
+ ):
raise Http404()
plan = get_object_or_404(Plan, id=form.cleaned_data['plan'])
@@ -103,10 +109,30 @@ def get_object(self):
We retry the operation when the user visits the subscription page.
"""
org = self.get_organization()
- return (
- Subscription.objects
- .get_or_create_default_subscription(org)
- )
+ return org.stripe_subscription
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ stripe_subscription = self.get_object()
+ if stripe_subscription:
+ context[
+ "features"
+ ] = self.get_organization().subscription.plan.features.all()
+
+ stripe_price = stripe_subscription.items.first().price
+ context["stripe_price"] = stripe_price
+
+ # When Stripe marks the subscription as ``past_due``,
+ # it means the usage of RTD service for the current period/month was not paid at all.
+ # Show the end date as the last period the customer paid.
+ context["subscription_end_date"] = stripe_subscription.current_period_end
+ if stripe_subscription.status == SubscriptionStatus.past_due:
+ latest_paid_invoice = stripe_subscription.invoices.filter(
+ paid=True
+ ).first()
+ context["subscription_end_date"] = latest_paid_invoice.period_end
+
+ return context
def get_success_url(self):
return reverse(
@@ -131,18 +157,18 @@ def get_success_url(self):
def post(self, request, *args, **kwargs):
"""Redirect the user to the Stripe billing portal."""
organization = self.get_organization()
- stripe_customer = organization.stripe_id
+ stripe_customer = organization.stripe_customer
return_url = request.build_absolute_uri(self.get_success_url())
try:
billing_portal = stripe.billing_portal.Session.create(
- customer=stripe_customer,
+ customer=stripe_customer.id,
return_url=return_url,
)
return HttpResponseRedirect(billing_portal.url)
except: # noqa
log.exception(
'There was an error connecting to Stripe to create the billing portal session.',
- stripe_customer=stripe_customer,
+ stripe_customer=stripe_customer.id,
organization_slug=organization.slug,
)
messages.error(