Repository: readthedocs/readthedocs.org
Pull request #12148: Upgrade `djstripe`
Author: login: humitos
Created: 2025-04-30T11:17:21Z | Merged: 2025-05-06T11:06:16Z

Closes #9706

Linked issue #9706: Requirements: upgrade `dj-stripe`
There are some extra actions required to upgrade `dj-stripe` to `2.7.0`. They are documented at https://github.com/readthedocs/readthedocs.org/pull/9688#discussion_r1009763745. For now, we are sticking to 2.6.3.
Linked issue comments: user: login: humitos
created_at: 2025-04-29T15:37:51Z
body: Django Admin breaks when upgrading to Django 5.2 without upgrading `djstripe`. See https://read-the-docs.sentry.io/issues/6575661535/
author_association: MEMBER
user: login: humitos
created_at: 2025-04-29T15:39:04Z
body: Note that we need to perform the upgrade in two steps: first to 2.7 and then to 2.8. See https://dj-stripe.dev/2.9/upgrade_dj_stripe/

Ideally, we should upgrade to the latest version.
author_association: MEMBER
user: login: humitos
created_at: 2025-04-30T10:59:41Z
body: I started following the djstripe guide:

### Migrations

1.  We have only 2 migrations that depend on `djstripe` migrations
```
readthedocs/organizations/migrations/0011_add_stripe_subscription_field.py
11:        ("djstripe", "0010_alter_customer_balance"),

readthedocs/organizations/migrations/0010_add_stripe_customer.py
11:        ("djstripe", "0010_alter_customer_balance"),
```
2. The latest version of `djstripe` (2.9.0) has the [`0010_alter_customer_balance`](https://github.com/dj-stripe/dj-stripe/blob/2.9.0/djstripe/migrations/0010_alter_customer_balance.py) migration file


When running the migrations locally, I received this message, so we need to update the test keys:

```
?: (djstripe.I001) You don't have any API Keys in the database. Did you forget to add them?
	HINT: Add STRIPE_TEST_SECRET_KEY and STRIPE_LIVE_SECRET_KEY directly from the Django Admin.
```

It seems there is nothing we need to do related to the migrations.

### Backward incompatibility changes

- In `2.9.0` deprecates `DJSTRIPE_WEBHOOK_VALIDATION` setting, which we are currently using. This needs to be configured through the admin when performing the migration.
- `2.9.0` removes `DJSTRIPE_WEBHOOK_SECRET` which we are using, but it doesn't say exactly what's the replacement. I _understand_ this is done via the Django Admin now for each webhook.

I checked all release notes from 2.6.x to 2.9.x and I only found these affecting us.

---

The steps we should follow are described in https://github.com/readthedocs/readthedocs.org/pull/9688#discussion_r1009763745. I'm copying them here:

> Run the migrations on web extra
> Create an API key on the admin  (/admin/djstripe/apikey/)
>  Release the webs
> Create a webhook endpoint after the release is done (/admin/djstripe/webhookendpoint/), the webhook will be created on stripe automatically.
> Deactivate the old webhook
>
> The old and the new webhook will work at the same time, so we won't lose any events.
>
> The migration is [dj-stripe/dj-stripe//blob/bc0bd5c1/djstripe/migrations/0011_2_7.py](https://github.com/dj-stripe/dj-stripe//blob/bc0bd5c1/djstripe/migrations/0011_2_7.py?rgh-link-date=2022-10-31T19%3A01%3A59Z), there are a couple of new fields that aren't nullable, but if the webhooks fail, stripe will retry them, and by that time the new code would be live.

I think we can target this for the next deploy.
author_association: MEMBER

Diff:
diff --git a/readthedocs/gold/tests/test_signals.py b/readthedocs/gold/tests/test_signals.py
index 5aa4d445f75..1f5699b0e6f 100644
--- a/readthedocs/gold/tests/test_signals.py
+++ b/readthedocs/gold/tests/test_signals.py
@@ -1,3 +1,4 @@
+import requests_mock
 from unittest import mock
 
 import django_dynamic_fixture as fixture
@@ -11,44 +12,21 @@ class GoldSignalTests(TestCase):
     def setUp(self):
         self.user = fixture.get(User)
 
-        # Mocking
-        self.patches = {}
-        self.mocks = {}
-        self.patches["requestor"] = mock.patch("stripe.api_requestor.APIRequestor")
-
-        for patch in self.patches:
-            self.mocks[patch] = self.patches[patch].start()
-
-        self.mocks["request"] = self.mocks["requestor"].return_value
-
-    def tearDown(self):
-        # TODO: refactor to use request_mocks for these. The current mock
-        # pattern breaks tests ran after these if tearDown is not called with
-        # the `.stop` for all the patches. It took me a lot of time to realized
-        # about this problem.
-        for _, patch in self.patches.items():
-            patch.stop()
-
-    def mock_request(self, resp=None):
-        if resp is None:
-            resp = ({}, "")
-        self.mocks["request"].request = mock.Mock(side_effect=resp)
-
-    def test_delete_subscription(self):
+    @requests_mock.Mocker(kw="mock_request")
+    def test_delete_subscription(self, mock_request):
         subscription = fixture.get(GoldUser, user=self.user, stripe_id="cus_123")
         self.assertIsNotNone(subscription)
-        self.mock_request(
-            [
-                ({"id": "cus_123", "object": "customer"}, ""),
-                ({"deleted": True, "customer": "cus_123"}, ""),
-            ]
+        mock_request.get(
+            "https://api.stripe.com/v1/customers/cus_123",
+            json={"id": "cus_123", "object": "customer"},
+        )
+        mock_request.delete(
+            "https://api.stripe.com/v1/customers/cus_123",
+            json={"deleted": True, "customer": "cus_123"},
         )
 
         subscription.delete()
-
-        self.mocks["request"].request.assert_has_calls(
-            [
-                mock.call("get", "/v1/customers/cus_123", {}, mock.ANY),
-                mock.call("delete", "/v1/customers/cus_123", {}, mock.ANY),
-            ]
-        )
+        assert mock_request.request_history[0]._request.method == "GET"
+        assert mock_request.request_history[0]._request.url == "https://api.stripe.com/v1/customers/cus_123"
+        assert mock_request.request_history[1]._request.method == "DELETE"
+        assert mock_request.request_history[1]._request.url == "https://api.stripe.com/v1/customers/cus_123"
diff --git a/readthedocs/subscriptions/tests/test_event_handlers.py b/readthedocs/subscriptions/tests/test_event_handlers.py
index 042f303a2a7..ecadf782cfc 100644
--- a/readthedocs/subscriptions/tests/test_event_handlers.py
+++ b/readthedocs/subscriptions/tests/test_event_handlers.py
@@ -430,16 +430,3 @@ def test_subscription_precedence(self):
 
             self.organization.refresh_from_db()
             assert self.organization.stripe_subscription == stripe_subscription
-
-    def test_register_events(self):
-        def test_func():
-            pass
-
-        with override_settings(RTD_ALLOW_ORGANIZATIONS=False):
-            event_handlers.handler("event")(test_func)
-
-        self.assertEqual(webhooks.registrations["event"], [])
-
-        with override_settings(RTD_ALLOW_ORGANIZATIONS=True):
-            event_handlers.handler("event")(test_func)
-        self.assertEqual(webhooks.registrations["event"], [test_func])
diff --git a/requirements/deploy.txt b/requirements/deploy.txt
index d5e4c51973d..1079e075fed 100644
--- a/requirements/deploy.txt
+++ b/requirements/deploy.txt
@@ -103,7 +103,7 @@ distlib==0.3.9
     #   virtualenv
 dj-pagination==2.5.0
     # via -r requirements/pip.txt
-dj-stripe==2.6.3
+dj-stripe==2.9.0
     # via -r requirements/pip.txt
 django==4.2.20
     # via
@@ -244,9 +244,7 @@ jmespath==1.0.1
     #   boto3
     #   botocore
 jsonfield==3.1.0
-    # via
-    #   -r requirements/pip.txt
-    #   dj-stripe
+    # via -r requirements/pip.txt
 kombu==5.5.3
     # via
     #   -r requirements/pip.txt
@@ -405,7 +403,7 @@ sqlparse==0.5.3
     #   django-debug-toolbar
 stack-data==0.6.3
     # via ipython
-stripe==4.2.0
+stripe==11.6.0
     # via
     #   -r requirements/pip.txt
     #   dj-stripe
@@ -434,6 +432,7 @@ typing-extensions==4.13.2
     #   pydantic
     #   pydantic-core
     #   pygithub
+    #   stripe
     #   typing-inspection
 typing-inspection==0.4.0
     # via
diff --git a/requirements/docker.txt b/requirements/docker.txt
index 255174984b6..4020b5389e6 100644
--- a/requirements/docker.txt
+++ b/requirements/docker.txt
@@ -111,7 +111,7 @@ distlib==0.3.9
     #   virtualenv
 dj-pagination==2.5.0
     # via -r requirements/pip.txt
-dj-stripe==2.6.3
+dj-stripe==2.9.0
     # via -r requirements/pip.txt
 django==4.2.20
     # via
@@ -257,9 +257,7 @@ jmespath==1.0.1
     #   boto3
     #   botocore
 jsonfield==3.1.0
-    # via
-    #   -r requirements/pip.txt
-    #   dj-stripe
+    # via -r requirements/pip.txt
 kombu==5.5.3
     # via
     #   -r requirements/pip.txt
@@ -433,7 +431,7 @@ sqlparse==0.5.3
     #   django-debug-toolbar
 stack-data==0.6.3
     # via ipython
-stripe==4.2.0
+stripe==11.6.0
     # via
     #   -r requirements/pip.txt
     #   dj-stripe
@@ -461,6 +459,7 @@ typing-extensions==4.13.2
     #   pydantic
     #   pydantic-core
     #   pygithub
+    #   stripe
     #   typing-inspection
 typing-inspection==0.4.0
     # via
diff --git a/requirements/pip.in b/requirements/pip.in
index a8a0b4a0a29..9703f0c5b4f 100644
--- a/requirements/pip.in
+++ b/requirements/pip.in
@@ -79,19 +79,14 @@ orjson
 django-gravatar2
 pytz
 
-# We cannot upgrade it until dj-stripe fixes this issue
-# https://github.com/dj-stripe/dj-stripe/issues/1842
-stripe==4.2.0
-
 regex
 
-# NOTE: djstripe overrides the stripe api version
-# globally, when updating make sure our code is compatible
-# with the version used by djstripe.
-# See STRIPE_API_VERSION https://dj-stripe.dev/reference/settings/.
-# dj-stripe==2.7.0 requires some extra actions to be upgrade
-# See https://github.com/readthedocs/readthedocs.org/issues/9706
-dj-stripe==2.6.3
+# Stripe is pinned because 12.0.1 fails with
+#
+# AttributeError: module 'stripe' has no attribute 'UsageRecord'
+stripe<12
+
+dj-stripe==2.9.0
 
 markdown
 
diff --git a/requirements/pip.txt b/requirements/pip.txt
index b220236c2dc..ec1c9470395 100644
--- a/requirements/pip.txt
+++ b/requirements/pip.txt
@@ -66,7 +66,7 @@ distlib==0.3.9
     # via virtualenv
 dj-pagination==2.5.0
     # via -r requirements/pip.in
-dj-stripe==2.6.3
+dj-stripe==2.9.0
     # via -r requirements/pip.in
 django==4.2.20
     # via
@@ -187,9 +187,7 @@ jmespath==1.0.1
     #   boto3
     #   botocore
 jsonfield==3.1.0
-    # via
-    #   -r requirements/pip.in
-    #   dj-stripe
+    # via -r requirements/pip.in
 kombu==5.5.3
     # via celery
 lexid==2021.1006
@@ -296,7 +294,7 @@ sqlparse==0.5.3
     # via
     #   django
     #   django-debug-toolbar
-stripe==4.2.0
+stripe==11.6.0
     # via
     #   -r requirements/pip.in
     #   dj-stripe
@@ -315,6 +313,7 @@ typing-extensions==4.13.2
     #   pydantic
     #   pydantic-core
     #   pygithub
+    #   stripe
     #   typing-inspection
 typing-inspection==0.4.0
     # via pydantic
diff --git a/requirements/testing.txt b/requirements/testing.txt
index 337f437ae9f..24c324eef85 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -108,7 +108,7 @@ distlib==0.3.9
     #   virtualenv
 dj-pagination==2.5.0
     # via -r requirements/pip.txt
-dj-stripe==2.6.3
+dj-stripe==2.9.0
     # via -r requirements/pip.txt
 django==4.2.20
     # via
@@ -253,9 +253,7 @@ jmespath==1.0.1
     #   boto3
     #   botocore
 jsonfield==3.1.0
-    # via
-    #   -r requirements/pip.txt
-    #   dj-stripe
+    # via -r requirements/pip.txt
 kombu==5.5.3
     # via
     #   -r requirements/pip.txt
@@ -445,7 +443,7 @@ sqlparse==0.5.3
     #   -r requirements/pip.txt
     #   django
     #   django-debug-toolbar
-stripe==4.2.0
+stripe==11.6.0
     # via
     #   -r requirements/pip.txt
     #   dj-stripe
@@ -468,6 +466,7 @@ typing-extensions==4.13.2
     #   pydantic-core
     #   pygithub
     #   sphinx
+    #   stripe
     #   typing-inspection
 typing-inspection==0.4.0
     # via


Reviews:
user: login: stsewd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T21:13:37Z
user: login: humitos
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T21:47:46Z
user: login: humitos
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T21:48:57Z
user: login: stsewd
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T22:01:45Z
user: login: humitos
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T06:58:59Z
user: login: humitos
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T09:30:04Z

Review comments:
diff_hunk: @@ -430,16 +430,3 @@ def test_subscription_precedence(self):
 
             self.organization.refresh_from_db()
             assert self.organization.stripe_subscription == stripe_subscription
-
-    def test_register_events(self):
path: readthedocs/subscriptions/tests/test_event_handlers.py
user: login: stsewd
body: Doesn't look like we should delete these?
created_at: 2025-04-30T21:12:20Z
author_association: MEMBER
diff_hunk: @@ -79,19 +79,14 @@ orjson
 django-gravatar2
 pytz
 
-# We cannot upgrade it until dj-stripe fixes this issue
-# https://github.com/dj-stripe/dj-stripe/issues/1842
-stripe==4.2.0
-
 regex
 
-# NOTE: djstripe overrides the stripe api version
path: requirements/pip.in
user: login: stsewd
body: We should keep this note, but we use the stripe package directly in .org.
created_at: 2025-04-30T21:13:24Z
author_association: MEMBER
diff_hunk: @@ -430,16 +430,3 @@ def test_subscription_precedence(self):
 
             self.organization.refresh_from_db()
             assert self.organization.stripe_subscription == stripe_subscription
-
-    def test_register_events(self):
path: readthedocs/subscriptions/tests/test_event_handlers.py
user: login: humitos
body: I deleted it because "webhooks.registration" doesn't exist anymore. They deleted that file completely from the repository and I didn't find a replacement 🤷‍♂️
created_at: 2025-04-30T21:47:46Z
author_association: MEMBER
diff_hunk: @@ -79,19 +79,14 @@ orjson
 django-gravatar2
 pytz
 
-# We cannot upgrade it until dj-stripe fixes this issue
-# https://github.com/dj-stripe/dj-stripe/issues/1842
-stripe==4.2.0
-
 regex
 
-# NOTE: djstripe overrides the stripe api version
path: requirements/pip.in
user: login: humitos
body: The newer version says this is not like that anymore. There is a note in the release notes. 

Maybe we should define it explicitly now?
created_at: 2025-04-30T21:48:57Z
author_association: MEMBER
diff_hunk: @@ -79,19 +79,14 @@ orjson
 django-gravatar2
 pytz
 
-# We cannot upgrade it until dj-stripe fixes this issue
-# https://github.com/dj-stripe/dj-stripe/issues/1842
-stripe==4.2.0
-
 regex
 
-# NOTE: djstripe overrides the stripe api version
path: requirements/pip.in
user: login: stsewd
body: I didn't find any mention of that, can you link me to that?
created_at: 2025-04-30T22:01:45Z
author_association: MEMBER
diff_hunk: @@ -79,19 +79,14 @@ orjson
 django-gravatar2
 pytz
 
-# We cannot upgrade it until dj-stripe fixes this issue
-# https://github.com/dj-stripe/dj-stripe/issues/1842
-stripe==4.2.0
-
 regex
 
-# NOTE: djstripe overrides the stripe api version
path: requirements/pip.in
user: login: humitos
body: Yes. I'll share it once I get to my laptop
created_at: 2025-05-01T06:58:59Z
author_association: MEMBER
diff_hunk: @@ -79,19 +79,14 @@ orjson
 django-gravatar2
 pytz
 
-# We cannot upgrade it until dj-stripe fixes this issue
-# https://github.com/dj-stripe/dj-stripe/issues/1842
-stripe==4.2.0
-
 regex
 
-# NOTE: djstripe overrides the stripe api version
path: requirements/pip.in
user: login: humitos
body: It's under "Other changes" at https://github.com/dj-stripe/dj-stripe/releases/tag/2.8.0

> stripe.api_version is no longer manipulated by dj-stripe.
created_at: 2025-05-06T09:30:04Z
author_association: MEMBER

PR comments:
user: login: humitos
created_at: 2025-05-06T11:00:33Z
body: I'm going to merge this PR now so we can deploy it today. @stsewd @ericholscher I would still appreciate any feedback you may have here. I also suppose we will fix anything we find during deploy, tho 😄 
author_association: MEMBER
user: login: humitos
created_at: 2025-05-06T11:10:02Z
body: Hrm... corporate fails due

```
  File "/home/circleci/readthedocs-corporate/.tox/py312/readthedocs.org/readthedocs/subscriptions/event_handlers.py", line 39, in decorator
    return webhooks.handler(*args, **kwargs)(func)
           ^^^^^^^^^^^^^^^^
AttributeError: module 'djstripe.webhooks' has no attribute 'handler'
```

I know that file was removed but I'm not sure what's the replacement, yet. I will investigate this.
author_association: MEMBER