Skip to content

Commit 974ea6b

Browse files
committed
Subscriptions: attach stripe subscription to organizations
Closes #9652 Ref #9313 (comment)
1 parent a609d9a commit 974ea6b

File tree

7 files changed

+79
-3
lines changed

7 files changed

+79
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Generated by Django 3.2.16 on 2022-11-21 22:52
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("djstripe", "0010_alter_customer_balance"),
11+
("organizations", "0010_add_stripe_customer"),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name="historicalorganization",
17+
name="stripe_subscription",
18+
field=models.ForeignKey(
19+
blank=True,
20+
db_constraint=False,
21+
null=True,
22+
on_delete=django.db.models.deletion.DO_NOTHING,
23+
related_name="+",
24+
to="djstripe.subscription",
25+
verbose_name="Stripe subscription",
26+
),
27+
),
28+
migrations.AddField(
29+
model_name="organization",
30+
name="stripe_subscription",
31+
field=models.OneToOneField(
32+
blank=True,
33+
null=True,
34+
on_delete=django.db.models.deletion.SET_NULL,
35+
related_name="rtd_organization",
36+
to="djstripe.subscription",
37+
verbose_name="Stripe subscription",
38+
),
39+
),
40+
]

readthedocs/organizations/models.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.urls import reverse
77
from django.utils.crypto import salted_hmac
88
from django.utils.translation import gettext_lazy as _
9+
from djstripe.enums import SubscriptionStatus
910

1011
from readthedocs.core.history import ExtraHistoricalRecords
1112
from readthedocs.core.permissions import AdminPermission
@@ -101,6 +102,14 @@ class Organization(models.Model):
101102
null=True,
102103
blank=True,
103104
)
105+
stripe_subscription = models.OneToOneField(
106+
"djstripe.Subscription",
107+
verbose_name=_("Stripe subscription"),
108+
on_delete=models.SET_NULL,
109+
related_name="rtd_organization",
110+
null=True,
111+
blank=True,
112+
)
104113

105114
# Managers
106115
objects = OrganizationQuerySet.as_manager()
@@ -115,15 +124,20 @@ class Meta:
115124
def __str__(self):
116125
return self.name
117126

118-
@property
119-
def stripe_subscription(self):
127+
def get_or_create_stripe_subscription(self):
120128
# TODO: remove this once we don't depend on our Subscription models.
121129
from readthedocs.subscriptions.models import Subscription
122130

123131
subscription = Subscription.objects.get_or_create_default_subscription(self)
124132
if not subscription:
125133
# This only happens during development.
126134
return None
135+
136+
active_subscription = self.stripe_customer.subscriptions.filter(
137+
status=SubscriptionStatus.active
138+
).first()
139+
if active_subscription:
140+
return active_subscription
127141
return self.stripe_customer.subscriptions.latest()
128142

129143
def get_absolute_url(self):

readthedocs/subscriptions/event_handlers.py

+9
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,15 @@ def checkout_completed(event):
155155
return
156156

157157
stripe_subscription_id = event.data["object"]["subscription"]
158+
stripe_subscription = djstripe.Subscription.objects.filter(
159+
id=stripe_subscription_id
160+
).first()
161+
if not stripe_subscription:
162+
log.info("Stripe subscription not found.")
163+
return
164+
organization.stripe_subscription = stripe_subscription
165+
organization.save()
166+
158167
_update_subscription_from_stripe(
159168
rtd_subscription=organization.subscription,
160169
stripe_subscription_id=stripe_subscription_id,

readthedocs/subscriptions/tests/test_event_handlers.py

+3
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,14 @@ def test_subscription_checkout_completed_event(self):
168168
status=SubscriptionStatus.canceled,
169169
)
170170

171+
self.assertIsNone(self.organization.stripe_subscription)
171172
event_handlers.checkout_completed(event=event)
172173

173174
subscription.refresh_from_db()
175+
self.organization.refresh_from_db()
174176
self.assertEqual(subscription.stripe_id, stripe_subscription.id)
175177
self.assertEqual(subscription.status, SubscriptionStatus.active)
178+
self.assertEqual(self.organization.stripe_subscription, stripe_subscription)
176179

177180
@mock.patch("readthedocs.subscriptions.event_handlers.cancel_stripe_subscription")
178181
def test_cancel_trial_subscription_after_trial_has_ended(

readthedocs/subscriptions/tests/test_views.py

+7
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def setUp(self):
3434
self.stripe_customer = self.stripe_subscription.customer
3535

3636
self.organization.stripe_customer = self.stripe_customer
37+
self.organization.stripe_subscription = self.stripe_subscription
3738
self.organization.save()
3839
self.subscription = get(
3940
Subscription,
@@ -113,10 +114,12 @@ def test_user_without_subscription(
113114

114115
self.organization.refresh_from_db()
115116
self.organization.stripe_customer = None
117+
self.organization.stripe_subscription = None
116118
self.organization.save()
117119
self.subscription.delete()
118120
self.assertFalse(hasattr(self.organization, 'subscription'))
119121
self.assertIsNone(self.organization.stripe_customer)
122+
self.assertIsNone(self.organization.stripe_subscription)
120123

121124
resp = self.client.get(reverse('subscription_detail', args=[self.organization.slug]))
122125
self.assertEqual(resp.status_code, 200)
@@ -125,6 +128,7 @@ def test_user_without_subscription(
125128
self.assertEqual(subscription.status, 'active')
126129
self.assertEqual(subscription.stripe_id, 'sub_a1b2c3')
127130
self.assertEqual(self.organization.stripe_customer, stripe_customer)
131+
self.assertEqual(self.organization.stripe_subscription, stripe_subscription)
128132
customer_retrieve_mock.assert_called_once()
129133
customer_create_mock.assert_not_called()
130134

@@ -146,12 +150,14 @@ def test_user_without_subscription_and_customer(
146150
# When stripe_id is None, a new customer is created.
147151
self.organization.stripe_id = None
148152
self.organization.stripe_customer = None
153+
self.organization.stripe_subscription = None
149154
self.organization.save()
150155
self.subscription.delete()
151156
self.organization.refresh_from_db()
152157
self.assertFalse(hasattr(self.organization, 'subscription'))
153158
self.assertIsNone(self.organization.stripe_id)
154159
self.assertIsNone(self.organization.stripe_customer)
160+
self.assertIsNone(self.organization.stripe_subscription)
155161

156162
customer_retrieve_mock.reset_mock()
157163
resp = self.client.get(reverse('subscription_detail', args=[self.organization.slug]))
@@ -162,6 +168,7 @@ def test_user_without_subscription_and_customer(
162168
self.assertEqual(subscription.stripe_id, 'sub_a1b2c3')
163169
self.assertEqual(self.organization.stripe_id, 'cus_a1b2c3')
164170
self.assertEqual(self.organization.stripe_customer, stripe_customer)
171+
self.assertEqual(self.organization.stripe_subscription, stripe_subscription)
165172
customer_create_mock.assert_called_once()
166173
customer_retrieve_mock.assert_not_called()
167174

readthedocs/subscriptions/utils.py

+3
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,7 @@ def get_or_create_stripe_subscription(organization):
8686
trial_period_days=settings.RTD_ORG_TRIAL_PERIOD_DAYS,
8787
)
8888
stripe_subscription = stripe_customer.subscriptions.latest()
89+
if organization.stripe_subscription != stripe_subscription:
90+
organization.stripe_subscription = stripe_subscription
91+
organization.save()
8992
return stripe_subscription

readthedocs/subscriptions/views.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def get_object(self):
109109
We retry the operation when the user visits the subscription page.
110110
"""
111111
org = self.get_organization()
112-
return org.stripe_subscription
112+
return org.get_or_create_stripe_subscription()
113113

114114
def get_context_data(self, **kwargs):
115115
context = super().get_context_data(**kwargs)

0 commit comments

Comments
 (0)