[BUILD-422] migrate users who does not have membership (#2150)

[BUILD-422] migrate users who does not have membership (#2150)

* BUILD-422 - create a new method into the MembershipHandler class to create a membership with the basic plan, add this method to account email confirmation flow.

* BUILD-412 - add new tests, add new fixtures, create a new script

* BUILD-412 - fix linter error

* BUILD-412 - add a new optional parameter plan to get_or_create membership method from MembershipHandler class, to allow us to create a membership with a plan. Update tests and change the membership creation from the endpoint of email confirmation, to a signal.

* BUILD-412 - remove unnecessary code from create memberships script.

* BUILD-412 - update fixture

* BUILD-422 - create a new method into the MembershipHandler class to create a membership with the basic plan, add this method to account email confirmation flow.

* BUILD-412 - add new tests, add new fixtures, create a new script

* BUILD-412 - fix linter error

* BUILD-412 - add a new optional parameter plan to get_or_create membership method from MembershipHandler class, to allow us to create a membership with a plan. Update tests and change the membership creation from the endpoint of email confirmation, to a signal.

* BUILD-412 - remove unnecessary code from create memberships script.

* BUILD-412 - update fixture

* BUILD-412 - update fixtures, create a new test to cover the script

* Update scripts/tests/conftest.py

Co-authored-by: augdiebold <69124522+augdiebold@users.noreply.github.com>

* BUILD-412 - fix linter error

---------

Co-authored-by: augdiebold <69124522+augdiebold@users.noreply.github.com>
diff --git a/ankihub/conftest.py b/ankihub/conftest.py
index 70fb9f3..0a1433e 100644
--- a/ankihub/conftest.py
+++ b/ankihub/conftest.py
@@ -18,7 +18,13 @@ from ankihub.decks.tests.factories import (
     DeckSubscriptionFactory,
 )
 from ankihub.decks.utils import MEDIA_DISABLED_FIELD_BYPASS_TAG, read_apkg
-from ankihub.memberships.models import Membership, MembershipStatusChoices, Partner
+from ankihub.memberships.models import (
+    FeatureChoices,
+    Membership,
+    MembershipStatusChoices,
+    Partner,
+    Plan,
+)
 from ankihub.memberships.tests.factories import MembershipFactory, PartnerFactory
 from ankihub.users.models import User
 from ankihub.users.tests.factories import UserFactory
@@ -241,6 +247,18 @@ def user_with_inactive_membership(user_with_membership: Membership) -> Membershi
     return user_with_membership
 
 
+@pytest.fixture
+def basic_plan() -> Plan:
+    plan, _ = Plan.objects.get_or_create(
+        name="Basic",
+        slug="basic",
+        features=[
+            FeatureChoices.FREE_DECKS_COLLABORATION,
+        ],
+    )
+    return plan
+
+
 def fields_from_string_list(
     string_list: list[str | None], field_names: list[str] | None = None
 ) -> list[dict[str, Any]]:
diff --git a/ankihub/memberships/services.py b/ankihub/memberships/services.py
index eb1aa91..f49243b 100644
--- a/ankihub/memberships/services.py
+++ b/ankihub/memberships/services.py
@@ -138,7 +138,7 @@ class MembershipHandler:
         return customer
 
     def get_or_create_membership(
-        self, user, payment_method="stripe", organization=None
+        self, user, payment_method="stripe", organization=None, plan=None
     ):
         """Creates a Customer on stripe for the user that started the payment flow,
         passing its id on ankihub on the metadata field. Then it creates an inactive
@@ -160,15 +160,18 @@ class MembershipHandler:
             if payment_method == "stripe":
                 customer = self.get_or_create_stripe_customer(user)
             membership = self.create_membership(
-                user=user, customer=customer, organization=organization
+                user=user, customer=customer, organization=organization, plan=plan
             )
         return membership
 
-    def create_membership(self, user, customer, issuer=None, organization=None):
+    def create_membership(
+        self, user, customer, issuer=None, organization=None, plan=None
+    ):
         membership = Membership.objects.create(
             user=user,
             metadata={"customer": customer} if customer else {},
             issuer=issuer,
+            plan=plan,
         )
 
         if customer and not user.customer_id:
diff --git a/ankihub/memberships/tests/test_services.py b/ankihub/memberships/tests/test_services.py
index b894b4a..ce4239a 100644
--- a/ankihub/memberships/tests/test_services.py
+++ b/ankihub/memberships/tests/test_services.py
@@ -2,7 +2,7 @@ import time
 from datetime import timedelta
 from decimal import Decimal
 from unittest import mock
-from unittest.mock import Mock, call
+from unittest.mock import Mock, call, patch
 
 import pytest
 from django.conf import settings
@@ -19,9 +19,11 @@ from ankihub.memberships.exceptions import (
     OrganizationSubscriptionDoesNotExistException,
 )
 from ankihub.memberships.models import (
+    Membership,
     MembershipStatusChoices,
     MembershipTypesChoices,
     Organization,
+    Plan,
 )
 from ankihub.memberships.services import (
     MembershipHandler,
@@ -468,6 +470,27 @@ def test_get_or_create_stripe_customer_retrieve_if_existent_customer_id(mocker, 
     assert user.customer_id == "cus_test"
 
 
+def test_create_membership_with_basic_plan(user: User, basic_plan: Plan):
+    membership_handler = MembershipHandler()
+
+    assert not Membership.objects.filter(user=user).exists()
+    assert not user.customer_id
+
+    with patch(
+        "ankihub.memberships.services.MembershipHandler.get_or_create_stripe_customer"
+    ) as mocked_get_or_create_stripe_customer:
+        mocked_get_or_create_stripe_customer.return_value = {"id": "customer_id"}
+        membership_handler.get_or_create_membership(user, plan=basic_plan)
+
+        assert Membership.objects.filter(
+            user=user,
+            plan=basic_plan,
+            status=MembershipStatusChoices.INACTIVE,
+            metadata={"customer": {"id": "customer_id"}},
+            membership_type=MembershipTypesChoices.REGULAR,
+        ).exists()
+
+
 @pytest.fixture
 def mock_stripe_api(monkeypatch: MonkeyPatch):
     class StripeResponseMock:
diff --git a/ankihub/users/signals.py b/ankihub/users/signals.py
index fe1c23c..10da327 100644
--- a/ankihub/users/signals.py
+++ b/ankihub/users/signals.py
@@ -7,6 +7,7 @@ from django.dispatch import receiver
 from geoip2.errors import AddressNotFoundError
 
 from ankihub.decks.tasks import discourse_sync_sso
+from ankihub.memberships.models import Plan
 from ankihub.memberships.services import MembershipHandler
 from ankihub.notifications.tasks import send_custom_email_task
 
@@ -60,9 +61,12 @@ def create_stripe_customer(sender, request, email_address, **kwargs):
     if not settings.TESTING:
         user = email_address.user
         log.info("Email confirmed for user.", user=user.id)
+        try:
+            basic_plan = Plan.objects.get(slug="basic")
+        except Plan.DoesNotExist:
+            log.error("Basic plan not found")
+            basic_plan = None
+
         gateway = MembershipHandler()
-        customer_data = gateway.get_or_create_stripe_customer(user)
-        user.set_stripe_customer_id(customer_id=customer_data.get("id"))
-        log.info(
-            "Stripe customer created for user.", user=user.id, customer=customer_data
-        )
+        gateway.get_or_create_membership(user, plan=basic_plan)
+        log.info("Membership created for user.", user=user.id)
diff --git a/ankihub/users/views.py b/ankihub/users/views.py
index c694b50..bfa833c 100644
--- a/ankihub/users/views.py
+++ b/ankihub/users/views.py
@@ -147,6 +147,7 @@ class CustomConfirmEmailView(ConfirmEmailView):
         if next_url:
             self.request.session.pop("next_url", None)
             return next_url
+
         return super().get_redirect_url()
 
 
diff --git a/scripts/create_memberships.py b/scripts/create_memberships.py
new file mode 100644
index 0000000..62c8b73
--- /dev/null
+++ b/scripts/create_memberships.py
@@ -0,0 +1,34 @@
+import logging
+import sys
+
+from ankihub.memberships.models import Membership, Plan
+from ankihub.memberships.services import MembershipHandler
+from ankihub.users.models import User
+
+
+def run():
+    """
+    This script will create memberships with a free plan associated for user that doesn't have memberships.
+    """
+    membership_handler = MembershipHandler()
+    users_without_membership = User.objects.filter(memberships__isnull=True)
+
+    try:
+        basic_plan = Plan.objects.get(slug="basic")
+    except Plan.DoesNotExist:
+        logging.error("Basic plan not found")
+        sys.exit(1)
+
+    new_memberships = []
+
+    for user in users_without_membership:
+        # For each call of get_or_create_stripe_customer, if the user didn't have a customer_id, we will create a new customer in Stripe.
+        # and also update the user.customer_id field on our database.
+        # Thinking about performance, we should use bulk_create to create all memberships at once.
+        customer = membership_handler.get_or_create_stripe_customer(user)
+
+        new_memberships.append(
+            Membership(user=user, plan=basic_plan, metadata={"customer": customer})
+        )
+
+    Membership.objects.bulk_create(new_memberships)
diff --git a/scripts/tests/conftest.py b/scripts/tests/conftest.py
index 137577c..44ef233 100644
--- a/scripts/tests/conftest.py
+++ b/scripts/tests/conftest.py
@@ -4,6 +4,9 @@ from unittest.mock import MagicMock
 import pytest
 from botocore.exceptions import ClientError
 

[... diff too long, it was truncated ...]

GitHub
sha: 0087e67d612f28e2638127d98f26474167fbec7a