Repository: fighthealthinsurance/fighthealthinsurance
Pull request #377: Allow double submit during signup (aka back button from stripe) & fix joining existing domain
Author: login: holdenk
Created: 2025-04-12T18:00:56Z | Merged: 2025-04-15T20:16:05Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Enhanced professional user sign-up flow with improved validation, domain checks, and streamlined session management.
  - Introduced new optional fields to support flexible user input and domain configurations.
  - Added a new model to track Stripe checkout sessions for professional user sign-ups.

- **Bug Fixes**
  - Refined error messaging to prevent duplicate account and domain creation, ensuring clearer guidance during sign-up issues.

- **Tests**
  - Expanded automated tests covering various sign-up scenarios, including retries, active domain handling, and session conflicts.
  - Improved assertions in existing tests to align with updated error response structures.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/fhi_users/auth/rest_auth_views.py b/fhi_users/auth/rest_auth_views.py
index d40e858db..3222424ac 100644
--- a/fhi_users/auth/rest_auth_views.py
+++ b/fhi_users/auth/rest_auth_views.py
@@ -37,6 +37,7 @@
     ResetToken,
     UserRole,
     UserContactInfo,
+    PendingProStripeCheckoutSession,
 )
 from fighthealthinsurance.models import StripeRecoveryInfo
 from fhi_users.auth import rest_serializers as serializers
@@ -302,7 +303,9 @@ def create_professional_in_current_domain(self, request: Request) -> Response:
                 )
                 return Response(
                     common_serializers.ErrorSerializer(
-                        {"error": "A user with this email already exists"}
+                        {
+                            "error": "A user with this email already exists in this practice/domain."
+                        }
                     ).data,
                     status=status.HTTP_400_BAD_REQUEST,
                 )
@@ -840,6 +843,15 @@ def perform_create(self, request: Request, serializer: Serializer) -> Response:
         visible_phone_number: Optional[str] = user_signup_info["visible_phone_number"]  # type: ignore
         new_domain: bool = bool(data["make_new_domain"])  # type: ignore
         user_domain_opt: Optional[UserDomain] = None
+        email: str = user_signup_info["email"]  # type: ignore
+        if not request.session:
+            logger.debug("No session?")
+        elif not request.session.session_key:
+            logger.debug("Making session")
+            request.session.create()
+        session_key = request.session.session_key
+
+        logger.debug(f"Performing create for session: {session_key}")
 
         if not validate_password(user_signup_info["password"]):
             return Response(
@@ -847,6 +859,71 @@ def perform_create(self, request: Request, serializer: Serializer) -> Response:
                 status=status.HTTP_400_BAD_REQUEST,
             )
 
+        # Check if this user is trying to sign up again (possibly after pressing back from Stripe)
+        existing_checkout = (
+            PendingProStripeCheckoutSession.objects.filter(
+                email=email,
+                django_session_id=session_key,
+                visible_phone_number=visible_phone_number,
+            )
+            .order_by("-created_at")
+            .first()
+        )
+
+        # If we have an existing checkout for this email, let's clean up before creating again
+        if existing_checkout:
+            logger.info(
+                f"Found existing checkout for {email}, cleaning up before recreating"
+            )
+            try:
+                # Clean up any existing user objects
+                professional_user = existing_checkout.professional_user
+                if not professional_user or not professional_user.user:
+                    raise Exception("User not found?")
+                user_to_delete = professional_user.user
+
+                # Delete relationships first
+                ProfessionalDomainRelation.objects.filter(
+                    professional=professional_user
+                ).delete()
+
+                # Delete the professional user
+                professional_user.delete()
+
+                # Delete extra properties
+                ExtraUserProperties.objects.filter(user=user_to_delete).delete()
+
+                # Delete the user
+                user_to_delete.delete()
+                logger.info(f"Deleted existing user for {email} during signup retry")
+                # Clean up domain if it's a new domain
+                if new_domain and existing_checkout.domain:
+                    try:
+                        domain_to_delete = existing_checkout.domain
+                        if not domain_to_delete.active:
+                            domain_to_delete.delete()
+                            logger.info(
+                                f"Deleted inactive domain {domain_to_delete.name} during signup retry"
+                            )
+                        else:
+                            return Response(
+                                common_serializers.ErrorSerializer(
+                                    {
+                                        "error": "Domain is active, cannot delete, please contact support42@fighthealthinsurance.com"
+                                    }
+                                ).data,
+                                status=status.HTTP_400_BAD_REQUEST,
+                            )
+                    except UserDomain.DoesNotExist:
+                        logger.error("Domain doesn't exist for cleanup")
+            except Exception as e:
+                logger.error(f"Error cleaning up existing data for {email}: {str(e)}")
+                raise e
+        else:
+            logger.debug("No existing checkout")
+
+        # Ok now back to the regular flow
+        # Here we check if the user is joining an existing domain
         if not new_domain:
             # In practice the serializer may enforce these for us
             try:
@@ -872,13 +949,15 @@ def perform_create(self, request: Request, serializer: Serializer) -> Response:
                     )
         else:
             if UserDomain.find_by_name(name=domain_name).count() != 0:
+                # Check if this is the domain we created previously
+                existing_domain = UserDomain.find_by_name(name=domain_name).get()
                 return Response(
                     common_serializers.ErrorSerializer(
                         {"error": "Domain already exists"}
                     ).data,
                     status=status.HTTP_400_BAD_REQUEST,
                 )
-            if (
+            elif (
                 UserDomain.objects.filter(
                     visible_phone_number=visible_phone_number
                 ).count()
@@ -892,53 +971,54 @@ def perform_create(self, request: Request, serializer: Serializer) -> Response:
                     ).data,
                     status=status.HTTP_400_BAD_REQUEST,
                 )
-            if "user_domain" not in data:
-                return Response(
-                    common_serializers.ErrorSerializer(
-                        {
-                            "error": "Need domain info when making a new domain or solo provider",
-                        }
-                    ).data,
-                    status=status.HTTP_400_BAD_REQUEST,
-                )
-            user_domain_info: dict[str, Optional[str]] = data["user_domain"]  # type: ignore
-            if domain_name != user_domain_info["name"]:
-                if user_domain_info["name"] is None:
-                    user_domain_info["name"] = domain_name
-                else:
-                    return Response(
-                        common_serializers.ErrorSerializer(
-                            {
-                                "error": "Domain name and user domain name must match",
-                            }
-                        ).data,
-                        status=status.HTTP_400_BAD_REQUEST,
-                    )
-            # We want to allow null
-            if user_domain_info["name"] == "":
-                user_domain_info["name"] = None
-            if visible_phone_number != user_domain_info["visible_phone_number"]:
-                if user_domain_info["visible_phone_number"] is None:
-                    user_domain_info["visible_phone_number"] = visible_phone_number
-                else:
+
+            if user_domain_opt is None:
+                if "user_domain" not in data:
                     return Response(
                         common_serializers.ErrorSerializer(
                             {
-                                "error": "Visible phone number and user domain visible phone number must match",
+                                "error": "Need domain info when making a new domain or solo provider",
                             }
                         ).data,
                         status=status.HTTP_400_BAD_REQUEST,
                     )
-            user_domain_opt = UserDomain.objects.create(
-                active=False,
-                **user_domain_info,
-            )
+                user_domain_info: dict[str, Optional[str]] = data["user_domain"]  # type: ignore
+                if domain_name != user_domain_info["name"]:
+                    if user_domain_info["name"] is None:
+                        user_domain_info["name"] = domain_name
+                    else:
+                        return Response(
+                            common_serializers.ErrorSerializer(
+                                {
+                                    "error": "Domain name and user domain name must match",
+                                }
+                            ).data,
+                            status=status.HTTP_400_BAD_REQUEST,
+                        )
+                # We want to allow null
+                if user_domain_info["name"] == "":
+                    user_domain_info["name"] = None
+                if visible_phone_number != user_domain_info["visible_phone_number"]:
+                    if user_domain_info["visible_phone_number"] is None:
+                        user_domain_info["visible_phone_number"] = visible_phone_number
+                    else:
+                        return Response(
+                            common_serializers.ErrorSerializer(
+                                {
+                                    "error": "Visible phone number and user domain visible phone number must match",
+                                }
+                            ).data,
+                            status=status.HTTP_400_BAD_REQUEST,
+                        )
+                user_domain_opt = UserDomain.objects.create(
+                    active=False,
+                    **user_domain_info,
+                )
 
         if not user_domain_opt:
             raise Exception("No user domain found or created")
         user_domain: UserDomain = user_domain_opt  # type: ignore
         raw_username: str = user_signup_info["email"]  # type: ignore
-        email: str = user_signup_info["email"]  # type: ignore
         password: str = user_signup_info["password"]  # type: ignore
         first_name: str = user_signup_info["first_name"]  # type: ignore
         last_name: str = user_signup_info["last_name"]  # type: ignore
@@ -963,6 +1043,14 @@ def perform_create(self, request: Request, serializer: Serializer) -> Response:
             pending_domain_relation=not new_domain,
         )
 
+        # If the domain is not new we don't need billing info
+        if not new_domain:
+            return Response(
+                serializers.ProfessionalSignupResponseSerializer(
+                    {"next_url": "https://www.fightpaperwork.com/auth/login"}
+                ).data,
+                status=status.HTTP_201_CREATED,
+            )
         if not (settings.DEBUG and data["skip_stripe"]):
             checkout_session = self.create_stripe_checkout_session(
                 email,
@@ -977,7 +1065,15 @@ def perform_create(self, request: Request, serializer: Serializer) -> Response:
             extra_user_properties = ExtraUserProperties.objects.create(
                 user=user, email_verified=False
             )
-            subscription_id = checkout_session.subscription
+            # Store the checkout session information
+            PendingProStripeCheckoutSession.objects.create(
+                stripe_session_id=checkout_session.id,
+                django_session_id=session_key,
+                email=email,
+                domain_id=str(user_domain.id),
+                professional_user_id=professional_user.id,
+                visible_phone_number=visible_phone_number,  # type: ignore
+            )
             return Response(
                 serializers.ProfessionalSignupResponseSerializer(
                     {"next_url": checkout_session.url}
@@ -1236,16 +1332,26 @@ def perform_create(self, request: Request, serializer) -> Response:
         if "patient_phone_number" in validated_data:
             patient_phone_number = validated_data.pop("patient_phone_number")
         domain_id = auth_utils.get_domain_id_from_request(request)
-        user = create_user(
-            email=validated_data["email"],
-            raw_username=validated_data["username"],
-            first_name=validated_data.get("firstname", ""),
-            last_name=validated_data.get("lastname", ""),
-            domain_name=domain_name,
-            phone_number=provider_phone_number,
-            password=validated_data["password"],
-            domain_id=domain_id,
-        )
+        try:
+            user = create_user(
+                email=validated_data["email"],
+                raw_username=validated_data["username"],
+                first_name=validated_data.get("first_name", ""),
+                last_name=validated_data.get("last_name", ""),
+                domain_name=domain_name,
+                phone_number=provider_phone_number,
+                password=validated_data["password"],
+                domain_id=domain_id,
+            )
+        except IntegrityError:
+
+            logger.opt(exception=True).error("Integrity error when creating user")
+            return Response(
+                common_serializers.ErrorSerializer(
+                    {"error": "A user with this email already exists"}
+                ).data,
+                status=status.HTTP_400_BAD_REQUEST,
+            )
         country = "USA"
         if "country" in validated_data:
             country = validated_data["country"]
diff --git a/fhi_users/auth/rest_serializers.py b/fhi_users/auth/rest_serializers.py
index 593ca9d59..3f4b3e691 100644
--- a/fhi_users/auth/rest_serializers.py
+++ b/fhi_users/auth/rest_serializers.py
@@ -155,6 +155,10 @@ class UserDomainSerializer(serializers.ModelSerializer):
     Serializer for domain information, excluding sensitive fields.
     """
 
+    # Override name and visible phone so we can re-create.
+    name = serializers.CharField(required=False, allow_blank=True)
+    visible_phone_number = serializers.CharField(required=False, allow_blank=True)
+
     class Meta(object):
         model = UserDomain
         exclude = (
diff --git a/fhi_users/migrations/0040_pendingprostripecheckoutsession.py b/fhi_users/migrations/0040_pendingprostripecheckoutsession.py
new file mode 100644
index 000000000..6697afdfb
--- /dev/null
+++ b/fhi_users/migrations/0040_pendingprostripecheckoutsession.py
@@ -0,0 +1,64 @@
+# Generated by Django 5.1.5 on 2025-04-12 01:36
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("fhi_users", "0039_alter_professionaldomainrelation_professional_type"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="PendingProStripeCheckoutSession",
+            fields=[
+                ("id", models.AutoField(primary_key=True, serialize=False)),
+                ("stripe_session_id", models.CharField(max_length=255, unique=True)),
+                (
+                    "django_session_id",
+                    models.CharField(blank=True, max_length=255, null=True),
+                ),
+                ("email", models.EmailField(max_length=254)),
+                ("visible_phone_number", models.CharField(max_length=150)),
+                ("created_at", models.DateTimeField(auto_now_add=True)),
+                ("completed", models.BooleanField(default=False)),
+                ("completed_at", models.DateTimeField(blank=True, null=True)),
+                ("metadata", models.JSONField(blank=True, null=True)),
+                (
+                    "domain",
+                    models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        to="fhi_users.userdomain",
+                    ),
+                ),
+                (
+                    "professional_user",
+                    models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        to="fhi_users.professionaluser",
+                    ),
+                ),
+            ],
+            options={
+                "verbose_name": "Stripe Checkout Session",
+                "verbose_name_plural": "Stripe Checkout Sessions",
+                "indexes": [
+                    models.Index(fields=["email"], name="fhi_users_p_email_ec97fe_idx"),
+                    models.Index(
+                        fields=["stripe_session_id"],
+                        name="fhi_users_p_stripe__21bd8a_idx",
+                    ),
+                    models.Index(
+                        fields=["django_session_id"],
+                        name="fhi_users_p_django__f8e466_idx",
+                    ),
+                ],
+            },
+        ),
+    ]
diff --git a/fhi_users/models.py b/fhi_users/models.py
index acd6a5272..8d6e9bae7 100644
--- a/fhi_users/models.py
+++ b/fhi_users/models.py
@@ -280,3 +280,44 @@ def save(self, *args, **kwargs):
             else:
                 self.expires_at = datetime.datetime.now() + datetime.timedelta(hours=24)
         super().save(*args, **kwargs)
+
+
+class PendingProStripeCheckoutSession(models.Model):
+    """
+    Track Stripe checkout sessions for professional user signups and other purchases.
+    This helps handle cases where users press back from Stripe checkout and retry.
+    """
+
+    id = models.AutoField(primary_key=True)
+    # Stripe-specific fields
+    stripe_session_id = models.CharField(max_length=255, unique=True)
+    # Django session ID if available
+    django_session_id = models.CharField(max_length=255, null=True, blank=True)
+    # User information
+    email = models.EmailField()
+    visible_phone_number = models.CharField(max_length=150, null=False, blank=False)
+    # Related models as foreign keys
+    domain = models.ForeignKey(
+        UserDomain, on_delete=models.SET_NULL, null=True, blank=True
+    )
+    professional_user = models.ForeignKey(
+        ProfessionalUser, on_delete=models.SET_NULL, null=True, blank=True
+    )
+    # Tracking metadata
+    created_at = models.DateTimeField(auto_now_add=True)
+    completed = models.BooleanField(default=False)
+    completed_at = models.DateTimeField(null=True, blank=True)
+    # Optional additional data
+    metadata = models.JSONField(null=True, blank=True)
+
+    class Meta:
+        verbose_name = "Stripe Checkout Session"
+        verbose_name_plural = "Stripe Checkout Sessions"
+        indexes = [
+            models.Index(fields=["email"]),
+            models.Index(fields=["stripe_session_id"]),
+            models.Index(fields=["django_session_id"]),
+        ]
+
+    def __str__(self):
+        return f"Stripe Session {self.stripe_session_id} for {self.email}"
diff --git a/fighthealthinsurance/common_view_logic.py b/fighthealthinsurance/common_view_logic.py
index 70e7df482..a109f20cc 100644
--- a/fighthealthinsurance/common_view_logic.py
+++ b/fighthealthinsurance/common_view_logic.py
@@ -1619,7 +1619,9 @@ async def generate_appeals(cls, parameters) -> AsyncIterator[str]:
             denial.plan_context = " ".join(set(plan_context))
         # Update the denial object with the received parameter if it differs
         if denial.professional_to_finish != professional_to_finish:
-            logger.info(f"Updating denial {denial.denial_id} professional_to_finish from {denial.professional_to_finish} to {professional_to_finish}")
+            logger.info(
+                f"Updating denial {denial.denial_id} professional_to_finish from {denial.professional_to_finish} to {professional_to_finish}"
+            )
             denial.professional_to_finish = professional_to_finish
         await denial.asave()
 
@@ -1804,13 +1806,28 @@ def handle_checkout_session_expired(request, session):
                 item = "Fight Health Insurance Fax"
             elif payment_type == "professional_domain_subscription":
                 item = "Fight Paperwork Professional Domain Subscription"
+                # Check if the domain is already active (due to another checkout session)
+                domain_id = metadata.get("domain_id")
+                if domain_id:
+                    try:
+                        domain = UserDomain.objects.get(id=domain_id)
+                        if domain.active and domain.stripe_subscription_id:
+                            logger.info(
+                                f"Domain {domain_id} is already active with subscription, ignoring expired checkout"
+                            )
+                            return
+                    except UserDomain.DoesNotExist:
+                        logger.info(
+                            f"Domain {domain_id} no longer exists, might have been recreated"
+                        )
+
                 # Temporary until the FPW UI is ready
                 finish_base_link = (
                     "https://www.fightpaperwork.com/stripe/finish-checkout"
                 )
                 params = urlencode(
                     {
-                        "domain_id": metadata.get("domain_id"),
+                        "domain_id": domain_id,
                         "professional_id": metadata.get("professional_id"),
                     }
                 )
@@ -1860,6 +1877,32 @@ def handle_checkout_session_expired(request, session):
                 )
                 return
 
+            # For professional domain subscriptions, check if the user has successfully
+            # created a domain in another session
+            if payment_type == "professional_domain_subscription" and metadata.get(
+                "professional_id"
+            ):
+                try:
+                    professional_id = metadata.get("professional_id")
+                    professional = ProfessionalUser.objects.get(id=professional_id)
+                    if professional:
+                        # Check if the user has active domains
+                        active_domains = UserDomain.objects.filter(
+                            professionaldomainrelation__professional=professional,
+                            professionaldomainrelation__active_domain_relation=True,
+                            active=True,
+                        ).exists()
+
+                        if active_domains:
+                            logger.info(
+                                f"User {professional} already has active domains, not creating LostStripeSession"
+                            )
+                            return
+                except Exception as e:
+                    logger.opt(exception=True).warning(
+                        f"Error checking for active domains for user {email}: {e}"
+                    )
+
             lost_session = LostStripeSession.objects.create(
                 payment_type=payment_type,
                 email=email,
diff --git a/fighthealthinsurance/ml/ml_models.py b/fighthealthinsurance/ml/ml_models.py
index 80d4297f4..6fed5f35e 100644
--- a/fighthealthinsurance/ml/ml_models.py
+++ b/fighthealthinsurance/ml/ml_models.py
@@ -276,31 +276,29 @@ def __init__(
         self.backup_api_base = backup_api_base
         self._expensive = expensive
 
-    def get_system_prompts(
-        self, prompt_type: str, prof_pov=False
-    ) -> list[str]:
+    def get_system_prompts(self, prompt_type: str, prof_pov=False) -> list[str]:
         """
         Get the appropriate system prompt based on type and audience.
 
         Args:
             prompt_type: The type of prompt to get (e.g., 'full', 'questions', etc.)
-            prof_pov: Generating the appeal from the patient (False) or professional (True) point of view 
+            prof_pov: Generating the appeal from the patient (False) or professional (True) point of view
 
         Returns:
             The system prompt as a string, or the first prompt if multiple are available
         """
         key = prompt_type
-        prompt = "Your are a helpful assistant with extensive medical knowledge who loves helping patients." 
+        prompt = "Your are a helpful assistant with extensive medical knowledge who loves helping patients."
         # If the prompt type is 'full' and the professional point of view is requested, use a different prompt.
-        if prof_pov or (prof_pov and f"{prompt_type}_not_patient" in self.system_prompts_map):
+        if prof_pov or (
+            prof_pov and f"{prompt_type}_not_patient" in self.system_prompts_map
+        ):
             key = f"{prompt_type}_not_patient"
-            prompt = "You are a medical expert writing on behalf of a patient. Write as the healthcare professional in first person (“I”)—advocating clearly, clinically, and persuasively for the patient’s medical needs. Emphasize medical necessity, clinical evidence, and patient benefit. Maintain a professional, authoritative tone and keep the focus on the appeal." 
+            prompt = "You are a medical expert writing on behalf of a patient. Write as the healthcare professional in first person (“I”)—advocating clearly, clinically, and persuasively for the patient’s medical needs. Emphasize medical necessity, clinical evidence, and patient benefit. Maintain a professional, authoritative tone and keep the focus on the appeal."
         logger.debug(f"GET SYS PROMPTS > {prompt}")
         return self.system_prompts_map.get(
             key,
-            [
-                prompt
-            ],
+            [prompt],
         )
 
     def bad_result(self, result: Optional[str], infer_type: str) -> bool:
diff --git a/fighthealthinsurance/rest_mixins.py b/fighthealthinsurance/rest_mixins.py
index b4ee8c4a8..fad948428 100644
--- a/fighthealthinsurance/rest_mixins.py
+++ b/fighthealthinsurance/rest_mixins.py
@@ -4,6 +4,8 @@
 from rest_framework.response import Response
 from rest_framework.exceptions import ValidationError
 
+from loguru import logger
+
 
 class SerializerMixin:
     serializer_class: typing.Optional[typing.Type[Serializer]] = None
@@ -23,9 +25,13 @@ def perform_create(self, request, serializer: Serializer) -> Response:
         raise NotImplementedError("Subclasses must implement perform_create()")
 
     def create(self, request) -> Response:
-        request_serializer = self.deserialize(data=request.data)
-        request_serializer.is_valid(raise_exception=True)
-        return self.perform_create(request, request_serializer)
+        try:
+            request_serializer = self.deserialize(data=request.data)
+            request_serializer.is_valid(raise_exception=True)
+            return self.perform_create(request, request_serializer)
+        except Exception as e:
+            logger.opt(exception=True).error(f"Error during request serialization: {e}")
+            raise ValidationError("Invalid data provided")
 
 
 class DeleteMixin(SerializerMixin):
diff --git a/tests/async/test_rest_auth_views.py b/tests/async/test_rest_auth_views.py
index 77f6783a5..89a1e761e 100644
--- a/tests/async/test_rest_auth_views.py
+++ b/tests/async/test_rest_auth_views.py
@@ -18,6 +18,7 @@
     ResetToken,
     ProfessionalUser,
     ProfessionalDomainRelation,
+    PendingProStripeCheckoutSession,
 )
 
 
@@ -159,6 +160,33 @@ def test_create_patient_user_view(self) -> None:
             )
         )
 
+    def test_patient_user_creates_with_correct_name(self) -> None:
+        """Test that patient user is created with the correct first and last name."""
+        url = reverse("patient_user-list")
+        # Test with first_name and last_name
+        data = {
+            "username": "nameuser1@example.com",
+            "password": "SecurePassword123",
+            "email": "nameuser1@example.com",
+            "first_name": "Test",
+            "last_name": "User",
+            "provider_phone_number": "1234567890",
+            "country": "USA",
+            "state": "CA",
+            "city": "Test City",
+            "address1": "123 Test St",
+            "zipcode": "12345",
+            "domain_name": "testdomain",
+        }
+        response = self.client.post(url, data, format="json")
+        self.assertIn(response.status_code, range(200, 300))
+        self.assertEqual(response.json()["status"], "pending")
+
+        # Verify user was created with correct name
+        new_user1 = User.objects.get(email="nameuser1@example.com")
+        self.assertEqual(new_user1.first_name, "Test")
+        self.assertEqual(new_user1.last_name, "User")
+
     def test_verify_email_view(self) -> None:
         url = reverse("rest_verify_email-verify")
         VerificationToken.objects.create(
@@ -881,6 +909,238 @@ def test_end_to_end_happy_path(self):
 
         # Now we need to call the websocket...
 
+    def test_retry_signup_with_same_email_and_domain(self):
+        """Test that a user can press back and retry the signup from Stripe checkout."""
+        # Do initial signup
+        domain_name = "retry_domain"
+        phone_number = "5551234567"
+        email = "retry@example.com"
+
+        signup_url = reverse("professional_user-list")
+        data = {
+            "user_signup_info": {
+                "username": email,
+                "password": "temp12345",
+                "email": email,
+                "first_name": "Retry",
+                "last_name": "User",
+                "domain_name": domain_name,
+                "visible_phone_number": phone_number,
+                "continue_url": "http://example.com/continue",
+            },
+            "make_new_domain": True,
+            "user_domain": {
+                "name": domain_name,
+                "visible_phone_number": phone_number,
+                "internal_phone_number": "5559876543",
+                "display_name": "Retry Domain",
+                "country": "USA",
+                "state": "CA",
+                "city": "RetryCity",
+                "address1": "123 Retry St",
+                "zipcode": "12345",
+            },
+        }
+
+        # First signup attempt
+        response = self.client.post(signup_url, data, format="json")
+        self.assertEqual(response.status_code, 201)
+        self.assertIn("next_url", response.json())
+
+        # Check that objects were created
+        domain = UserDomain.objects.get(name=domain_name)
+        self.assertFalse(domain.active)  # Domain should be inactive initially
+
+        user = User.objects.get(email=email)
+        professional_user = ProfessionalUser.objects.get(user=user)
+        self.assertFalse(professional_user.active)
+
+        # Store the IDs for comparison later
+        domain_id = domain.id
+        user_id = user.id
+        pro_user_id = professional_user.id
+
+        # Verify a checkout session was created
+        checkout_session = PendingProStripeCheckoutSession.objects.filter(
+            email=email
+        ).first()
+        self.assertIsNotNone(checkout_session)
+        self.assertEqual(checkout_session.email, email)
+        self.assertEqual(checkout_session.professional_user_id, professional_user.id)
+        self.assertEqual(str(checkout_session.domain_id), str(domain.id))
+
+        # Now simulate a second signup attempt (as if user pressed back from Stripe)
+        response = self.client.post(signup_url, data, format="json")
+        self.assertEqual(response.status_code, 201)
+        self.assertIn("next_url", response.json())
+
+        # Check that we don't have duplicate objects
+        self.assertEqual(UserDomain.objects.filter(name=domain_name).count(), 1)
+        self.assertEqual(User.objects.filter(email=email).count(), 1)
+
+        # Get the objects again
+        domain = UserDomain.objects.get(name=domain_name)
+        user = User.objects.get(email=email)
+        professional_user = ProfessionalUser.objects.get(user=user)
+
+        # Verify we have two checkout sessions (original + retry)
+        self.assertEqual(
+            PendingProStripeCheckoutSession.objects.filter(email=email).count(), 2
+        )
+
+    def test_retry_signup_with_active_domain_fails(self):
+        """Test that a domain can only be reused if it's still pending/inactive."""
+        # Initial signup
+        domain_name = "active_domain"
+        phone_number = "5551111111"
+        email = "active@example.com"
+
+        signup_url = reverse("professional_user-list")
+        data = {
+            "user_signup_info": {
+                "username": email,
+                "password": "temp12345",
+                "email": email,
+                "first_name": "Active",
+                "last_name": "User",
+                "domain_name": domain_name,
+                "visible_phone_number": phone_number,
+                "continue_url": "http://example.com/continue",
+            },
+            "make_new_domain": True,
+            "user_domain": {
+                "name": domain_name,
+                "visible_phone_number": phone_number,
+                "internal_phone_number": "5559998888",
+                "display_name": "Active Domain",
+                "country": "USA",
+                "state": "CA",
+                "city": "ActiveCity",
+                "address1": "123 Active St",
+                "zipcode": "12345",
+            },
+        }
+
+        # First signup attempt
+        response = self.client.post(signup_url, data, format="json")
+        self.assertEqual(response.status_code, 201)
+
+        # Manually activate the domain (simulating completed Stripe checkout)
+        domain = UserDomain.objects.get(name=domain_name)
+        domain.active = True
+        domain.stripe_subscription_id = "sub_12345"
+        domain.save()
+
+        # Attempt second signup with same domain name
+        response = self.client.post(signup_url, data, format="json")
+        self.assertEqual(response.status_code, 400)
+        self.assertIn("Domain is active, cannot delete", response.json()["error"])
+
+    def test_signup_with_different_email_same_domain_fails(self):
+        """Test that a different user cannot use the same domain name."""
+        # Initial signup
+        domain_name = "shared_domain"
+        phone_number = "5552222222"
+        email = "first@example.com"
+
+        signup_url = reverse("professional_user-list")
+        data = {
+            "user_signup_info": {
+                "username": email,
+                "password": "temp12345",
+                "email": email,
+                "first_name": "First",
+                "last_name": "User",
+                "domain_name": domain_name,
+                "visible_phone_number": phone_number,
+                "continue_url": "http://example.com/continue",
+            },
+            "make_new_domain": True,
+            "user_domain": {
+                "name": domain_name,
+                "visible_phone_number": phone_number,
+                "internal_phone_number": "5557778888",
+                "display_name": "Shared Domain",
+                "country": "USA",
+                "state": "CA",
+                "city": "SharedCity",
+                "address1": "123 Shared St",
+                "zipcode": "12345",
+            },
+        }
+
+        # First user signup
+        response = self.client.post(signup_url, data, format="json")
+        self.assertEqual(response.status_code, 201)
+
+        # Second user with same domain name
+        data2 = data.copy()
+        data2["user_signup_info"] = data["user_signup_info"].copy()
+        data2["user_signup_info"]["email"] = "second@example.com"
+        data2["user_signup_info"]["username"] = "second@example.com"
+
+        # Attempt signup with different email but same domain name
+        response = self.client.post(signup_url, data2, format="json")
+        self.assertEqual(response.status_code, 400)
+        self.assertIn("Domain already exists", response.json()["error"])
+
+    def test_signup_with_different_session_id_but_same_email(self):
+        """Test that signup with different session ID but same email cleans up properly."""
+        # Initial signup
+        domain_name = "session_domain"
+        phone_number = "5553333333"
+        email = "session@example.com"
+
+        signup_url = reverse("professional_user-list")
+        data = {
+            "user_signup_info": {
+                "username": email,
+                "password": "temp12345",
+                "email": email,
+                "first_name": "Session",
+                "last_name": "User",
+                "domain_name": domain_name,
+                "visible_phone_number": phone_number,
+                "continue_url": "http://example.com/continue",
+            },
+            "make_new_domain": True,
+            "user_domain": {
+                "name": domain_name,
+                "visible_phone_number": phone_number,
+                "internal_phone_number": "5556667777",
+                "display_name": "Session Domain",
+                "country": "USA",
+                "state": "CA",
+                "city": "SessionCity",
+                "address1": "123 Session St",
+                "zipcode": "12345",
+            },
+        }
+
+        # First signup attempt
+        response = self.client.post(signup_url, data, format="json")
+        self.assertEqual(response.status_code, 201)
+
+        # Get the created objects
+        initial_domain = UserDomain.objects.get(name=domain_name)
+        initial_pro_user = ProfessionalUser.objects.get(user__email=email)
+
+        # Reset the session
+        self.client.cookies.clear()
+
+        # Verify initial checkout session was created
+        initial_session = PendingProStripeCheckoutSession.objects.get(
+            email=email,
+            domain_id=initial_domain.id,
+            professional_user_id=initial_pro_user.id,
+        )
+        self.assertIsNotNone(initial_session)
+
+        # Now do another signup attempt with the same data - this should
+        # not work and handle the previously created pending records
+        response = self.client.post(signup_url, data, format="json")
+        self.assertEqual(response.status_code, 400)
+
 
 class ProfessionalInvitationTests(TestCase):
     def setUp(self) -> None:
diff --git a/tests/sync/test_rest_api.py b/tests/sync/test_rest_api.py
index 89f33ed94..d3c287535 100644
--- a/tests/sync/test_rest_api.py
+++ b/tests/sync/test_rest_api.py
@@ -1359,7 +1359,7 @@ def test_duplicate_domain_creation(self):
 
         # Check error message in response
         response_data = response.json()
-        self.assertIn("user_domain", response_data)
+        self.assertIn("Domain", response_data["error"])
 
         # Verify no new domain was created with the same name
         domains_with_same_name = UserDomain.objects.filter(


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.


<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**fighthealthinsurance/common_view_logic.py:1898**
* The variable 'email' is referenced but not defined in this context. Consider retrieving the email from the metadata (e.g. email = metadata.get('email')) to avoid a NameError.
```
logger.info(f"User {email} already has active domains, not creating LostStripeSession")
```
**fhi_users/auth/rest_auth_views.py:1073**
* Passing the domain_id as a string when the field expects an integer could lead to a type mismatch. Consider removing the string conversion to pass user_domain.id directly.
```
domain_id=str(user_domain.id),
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T18:01:19Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>fighthealthinsurance/rest_mixins.py (1)</summary><blockquote>

`27-34`: **Improve exception handling by preserving the original exception context.**

The new error handling around deserialization and validation is good, but for better debugging, consider preserving the original exception context.


```diff
try:
    request_serializer = self.deserialize(data=request.data)
    request_serializer.is_valid(raise_exception=True)
    return self.perform_create(request, request_serializer)
except Exception as e:
    logger.opt(exception=True).error(f"Error during request serialization: {e}")
-    raise ValidationError("Invalid data provided")
+    raise ValidationError("Invalid data provided") from e
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

34-34: Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling

(B904)

</details>

</details>

</blockquote></details>
<details>
<summary>fhi_users/auth/rest_auth_views.py (1)</summary><blockquote>

`951-953`: **Remove unused `existing_domain` assignment.**  
The variable is never referenced after assignment, leading to a minor code smell.  


Apply this diff to eliminate the unused variable:

```diff
-                existing_domain = UserDomain.find_by_name(name=domain_name).get()
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

953-953: Local variable `existing_domain` is assigned to but never used

Remove assignment to unused variable `existing_domain`

(F841)

</details>

</details>

</blockquote></details>
<details>
<summary>tests/async/test_rest_auth_views.py (1)</summary><blockquote>

`885-963`: **Well-structured test for signup retry scenario**

This test effectively verifies that users can press the back button during Stripe checkout and retry the signup process without causing duplicate user or domain records. The checkout session tracking is correctly tested.

However, there are three unused variables:

```diff
- domain_id = domain.id
- user_id = user.id
- pro_user_id = professional_user.id
```

These variables are assigned but never used in the test. Consider removing them or using them in your assertions.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

932-932: Local variable `domain_id` is assigned to but never used

Remove assignment to unused variable `domain_id`

(F841)

---

933-933: Local variable `user_id` is assigned to but never used

Remove assignment to unused variable `user_id`

(F841)

---

934-934: Local variable `pro_user_id` is assigned to but never used

Remove assignment to unused variable `pro_user_id`

(F841)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between daa9644bc7b0b209cdfeec436d1001b333e19674 and 6765ef10fc32b6af04d2bb902fe1fdce2ade8dfd.

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `fhi_users/auth/rest_auth_views.py` (6 hunks)
* `fhi_users/auth/rest_serializers.py` (1 hunks)
* `fhi_users/migrations/0040_pendingprostripecheckoutsession.py` (1 hunks)
* `fhi_users/models.py` (1 hunks)
* `fighthealthinsurance/common_view_logic.py` (3 hunks)
* `fighthealthinsurance/ml/ml_models.py` (1 hunks)
* `fighthealthinsurance/rest_mixins.py` (2 hunks)
* `tests/async/test_rest_auth_views.py` (2 hunks)
* `tests/sync/test_rest_api.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (3)</summary>

<details>
<summary>fighthealthinsurance/common_view_logic.py (1)</summary><blockquote>

<details>
<summary>fhi_users/models.py (2)</summary>

* `UserDomain` (44-132)
* `ProfessionalUser` (188-216)

</details>

</blockquote></details>
<details>
<summary>fhi_users/auth/rest_auth_views.py (1)</summary><blockquote>

<details>
<summary>fhi_users/models.py (4)</summary>

* `PendingProStripeCheckoutSession` (285-323)
* `ProfessionalDomainRelation` (219-229)
* `UserDomain` (44-132)
* `find_by_name` (102-107)

</details>

</blockquote></details>
<details>
<summary>tests/async/test_rest_auth_views.py (1)</summary><blockquote>

<details>
<summary>fhi_users/models.py (6)</summary>

* `PendingProStripeCheckoutSession` (285-323)
* `UserDomain` (44-132)
* `ProfessionalUser` (188-216)
* `save` (87-91)
* `save` (261-267)
* `save` (276-282)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>fighthealthinsurance/rest_mixins.py</summary>

34-34: Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling

(B904)

</details>
<details>
<summary>fhi_users/auth/rest_auth_views.py</summary>

953-953: Local variable `existing_domain` is assigned to but never used

Remove assignment to unused variable `existing_domain`

(F841)

</details>
<details>
<summary>tests/async/test_rest_auth_views.py</summary>

932-932: Local variable `domain_id` is assigned to but never used

Remove assignment to unused variable `domain_id`

(F841)

---

933-933: Local variable `user_id` is assigned to but never used

Remove assignment to unused variable `user_id`

(F841)

---

934-934: Local variable `pro_user_id` is assigned to but never used

Remove assignment to unused variable `pro_user_id`

(F841)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (7)</summary>

* GitHub Check: types
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)

</details>

<details>
<summary>🔇 Additional comments (21)</summary><blockquote>

<details>
<summary>tests/sync/test_rest_api.py (1)</summary>

`1362-1362`: **LGTM! This assertion correctly reflects the updated API error response structure.**

The change to check for "Domain" within the error dictionary aligns with the improved error handling for duplicate domain creation.

</details>
<details>
<summary>fhi_users/auth/rest_serializers.py (1)</summary>

`157-159`: **LGTM! The optional fields enable smoother re-creation of user data.**

Making these fields optional and allowing blank values supports the back button functionality from Stripe during signup, allowing users to re-submit their information without requiring these fields to be populated.

</details>
<details>
<summary>fighthealthinsurance/ml/ml_models.py (1)</summary>

`279-302`: **LGTM! The formatting changes improve code consistency.**

These changes are purely formatting improvements that make the code more compact and consistent, with no functional changes.

</details>
<details>
<summary>fhi_users/models.py (1)</summary>

`283-323`: **Robust addition for tracking Stripe sessions.**  
This new model cleanly captures and organizes essential information (session IDs, email, phone, etc.) for handling professional user signups and back-button scenarios. The indexing on key fields (email, stripe_session_id, django_session_id) will facilitate efficient lookups.

</details>
<details>
<summary>fighthealthinsurance/common_view_logic.py (3)</summary>

`1809-1819`: **Preventing redundant processing for active subscriptions is correct.**  
By returning early if the domain is already active, you avoid wasted efforts re-handling expired sessions that no longer need updates. This is a prudent check.

---

`1820-1823`: **Graceful handling for missing domains.**  
Catching `UserDomain.DoesNotExist` and logging a message helps debug issues when a domain is recreated or removed, preventing silent failures.

---

`1880-1905`: **Skipping lost session creation for already-active users.**  
This logic correctly skips re-creating “lost” Stripe sessions if the professional is already active with a valid domain, preventing duplications.

</details>
<details>
<summary>fhi_users/migrations/0040_pendingprostripecheckoutsession.py (1)</summary>

`1-65`: **New migration looks consistent with the model definition.**  
All fields, including foreign keys and indexes, match the new `PendingProStripeCheckoutSession` model. Running this migration in sync with the model usage should work smoothly.

</details>
<details>
<summary>fhi_users/auth/rest_auth_views.py (9)</summary>

`40-40`: **Import addition.**  
Importing `PendingProStripeCheckoutSession` here aligns well with its usage for handling re-entrancy and back-button signups.

---

`844-852`: **Ensuring session creation if missing.**  
Your logic verifies or creates a session before capturing checkout details, helping maintain continuity for multi-step signups.

---

`861-869`: **Querying existing checkout sessions.**  
Filtering by email, session ID, and phone ensures that returning users are correctly matched to potential prior signups, mitigating duplicate entries.

---

`871-923`: **Cleanup of previous signups.**  
Your approach to remove stale users, professional domain relations, and domain references if the user attempts another sign-up session ensures a clean new start, preventing partial or conflicting records.

---

`924-924`: **Debugging clarity.**  
The debug log "No existing checkout" provides visibility into signups that are truly fresh.

---

`960-966`: **Validation for uniqueness of phone number.**  
Rejecting signups when the phone is already in use avoids domain collisions, aligning with your uniqueness constraints.

---

`975-976`: **Mandatory domain info check.**  
This ensures the user domain fields are supplied when creating a new domain, preventing partially configured signups.

---

`985-1013`: **Consistent domain and phone matching.**  
By verifying that `domain_name` and `visible_phone_number` match the user domain data, you prevent mismatched domain references during signup.

---

`1069-1076`: **Storing Stripe checkout info for re-entrancy.**  
Saving the checkout session details in `PendingProStripeCheckoutSession` forms the crux of double-submit/back-button protection, allowing full recovery post Stripe session.

</details>
<details>
<summary>tests/async/test_rest_auth_views.py (4)</summary>

`21-21`: **LGTM: Import for tracking Stripe checkout sessions added**

This import correctly adds the `PendingProStripeCheckoutSession` model that's essential for the new tests which verify the Stripe checkout flow.

---

`964-1011`: **LGTM: Comprehensive test for domain activation constraints**

This test correctly verifies that a domain can't be reused for signup once it's been activated, which is an important constraint to maintain data integrity. The manual activation of the domain with Stripe information properly simulates a completed checkout.

---

`1012-1059`: **LGTM: Good validation of domain uniqueness across users**

This test effectively ensures that domain names remain unique across different users, preventing potential conflicts or security issues where multiple accounts could claim the same domain. The deep copy of the request data structure is properly handled.

---

`1060-1116`: **Test seems incomplete - missing assertion or verification**

While the test structure is good, it ends with an assertion that the second signup attempt should fail (line 1115), but there's no verification of what specific error should be returned or what the expected behavior is regarding the existing pending records.


Consider adding assertions to verify:
1. The specific error message returned
2. Whether the existing domain and user records are properly maintained
3. How the checkout sessions are handled in this scenario

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T18:04:16Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>fhi_users/auth/rest_serializers.py (1)</summary><blockquote>

`158-161`: **Consider unifying phone number validation.**

Currently, `visible_phone_number` is allowed to be blank without any additional validation. To avoid inconsistent formats across the codebase, you may want to reuse or adapt the phone number sanitization/validation logic seen in `UserSignupSerializer`.

</blockquote></details>
<details>
<summary>fhi_users/auth/rest_auth_views.py (2)</summary><blockquote>

`875-924`: **Comprehensive cleanup of existing user/domain on signup retry.**

Your rollback approach effectively removes partial signups before recreating them. This is beneficial to ensure consistent data; however, this block is relatively long. If repetitive cleanup logic is needed elsewhere, consider factoring it out into a reusable utility function.

---

`952-953`: **Remove unused local variable.**

According to static analysis, `existing_domain` is never used after assignment. Removing it prevents confusion and keeps the code clean.

```diff
- existing_domain = UserDomain.find_by_name(name=domain_name).get()
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

953-953: Local variable `existing_domain` is assigned to but never used

Remove assignment to unused variable `existing_domain`

(F841)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 187dc84fcec876eee5d1b50a5443b3a15f5b956d and 5e7dc180d0f5a7d9ba283221de7807e755166684.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `fhi_users/auth/rest_auth_views.py` (8 hunks)
* `fhi_users/auth/rest_serializers.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>fhi_users/auth/rest_auth_views.py (1)</summary><blockquote>

<details>
<summary>fhi_users/models.py (2)</summary>

* `PendingProStripeCheckoutSession` (285-323)
* `UserDomain` (44-132)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>fhi_users/auth/rest_auth_views.py</summary>

953-953: Local variable `existing_domain` is assigned to but never used

Remove assignment to unused variable `existing_domain`

(F841)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (8)</summary>

* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: types
* GitHub Check: types
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>fhi_users/auth/rest_auth_views.py (3)</summary>

`40-40`: **Import looks good.**

The `PendingProStripeCheckoutSession` import here facilitates your new checkout session tracking logic.

---

`306-308`: **No issues with the updated error message.**

This new error message clarifies the unique email constraint within the practice/domain.

---

`863-871`: **Check concurrency edge cases for existing session lookups.**

Filtering by `email`, `django_session_id`, and `visible_phone_number`, and then selecting only the latest session is a solid approach. However, in high-concurrency scenarios, multiple sessions might be created in parallel. Consider whether a unique constraint or additional locking is necessary to avoid potential duplicates.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T18:46:19Z
user: login: nyghtowl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T23:32:48Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T23:32:54Z
user: login: holdenk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T03:32:16Z
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.


<details>
<summary>Comments suppressed due to low confidence (4)</summary>

**tests/sync/test_rest_api.py:1362**
* Ensure that the error message check aligns with the actual error structure and wording provided by the updated sign-up flow.
```
self.assertIn("Domain", response_data["error"])
```
**fighthealthinsurance/common_view_logic.py:1903**
* The variable 'email' is used in the logging statement but is not defined in this scope. Consider retrieving the email from the metadata or defining it appropriately.
```
logger.opt(exception=True).warning(f"Error checking for active domains for user {email}: {e}")
```
**fhi_users/auth/rest_auth_views.py:1073**
* Passing the domain ID as a string may cause issues with the foreign key assignment; use the integer value directly (e.g., domain_id=user_domain.id) or assign the domain object.
```
domain_id=str(user_domain.id),
```
**fhi_users/auth/rest_auth_views.py:1351**
* [nitpick] Consider standardizing duplicate user error messages for consistency; the error message here differs from the one returned earlier in the flow.
```
return Response(common_serializers.ErrorSerializer({"error": "A user with this email already exists"}).data,
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T04:33:20Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>fhi_users/auth/rest_auth_views.py (1)</summary><blockquote>

`953-953`: **Remove unused variable assignment**

The variable `existing_domain` is assigned but never used in this block.

```diff
- existing_domain = UserDomain.find_by_name(name=domain_name).get()
+ UserDomain.find_by_name(name=domain_name).get()  # Domain exists
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

953-953: Local variable `existing_domain` is assigned to but never used

Remove assignment to unused variable `existing_domain`

(F841)

</details>

</details>

</blockquote></details>
<details>
<summary>tests/async/test_rest_auth_views.py (1)</summary><blockquote>

`912-990`: **Comprehensive test for signup retry functionality**

This test thoroughly validates the behavior when a user presses back from Stripe checkout and retries signup with the same email and domain.


Remove unused variables on lines 959-961:

```diff
- domain_id = domain.id
- user_id = user.id
- pro_user_id = professional_user.id
```

These variables are assigned but never used in the test.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

959-959: Local variable `domain_id` is assigned to but never used

Remove assignment to unused variable `domain_id`

(F841)

---

960-960: Local variable `user_id` is assigned to but never used

Remove assignment to unused variable `user_id`

(F841)

---

961-961: Local variable `pro_user_id` is assigned to but never used

Remove assignment to unused variable `pro_user_id`

(F841)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5e7dc180d0f5a7d9ba283221de7807e755166684 and afcece495547a65eb36f14122c5950533f72baf5.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `fhi_users/auth/rest_auth_views.py` (8 hunks)
* `tests/async/test_rest_auth_views.py` (3 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>fhi_users/auth/rest_auth_views.py (1)</summary><blockquote>

<details>
<summary>fhi_users/models.py (2)</summary>

* `PendingProStripeCheckoutSession` (285-323)
* `UserDomain` (44-132)

</details>

</blockquote></details>
<details>
<summary>tests/async/test_rest_auth_views.py (1)</summary><blockquote>

<details>
<summary>fhi_users/models.py (3)</summary>

* `PendingProStripeCheckoutSession` (285-323)
* `UserDomain` (44-132)
* `ProfessionalUser` (188-216)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>fhi_users/auth/rest_auth_views.py</summary>

953-953: Local variable `existing_domain` is assigned to but never used

Remove assignment to unused variable `existing_domain`

(F841)

</details>
<details>
<summary>tests/async/test_rest_auth_views.py</summary>

959-959: Local variable `domain_id` is assigned to but never used

Remove assignment to unused variable `domain_id`

(F841)

---

960-960: Local variable `user_id` is assigned to but never used

Remove assignment to unused variable `user_id`

(F841)

---

961-961: Local variable `pro_user_id` is assigned to but never used

Remove assignment to unused variable `pro_user_id`

(F841)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (8)</summary>

* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: types
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: types
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)

</details>

<details>
<summary>🔇 Additional comments (12)</summary><blockquote>

<details>
<summary>fhi_users/auth/rest_auth_views.py (8)</summary>

`40-40`: **Import of PendingProStripeCheckoutSession model supports the new Stripe checkout session tracking**

This import enables the implementation of the feature to handle users pressing back from Stripe checkout and retrying signup.

---

`307-309`: **Error message improvement for better user experience**

The error message now specifically mentions that the user exists "in this practice/domain" which provides more context to the user than the previous generic message.

---

`846-855`: **Proper session management for Stripe checkout tracking**

Good implementation to ensure the session is created if it doesn't exist and logging debug information appropriately.

---

`862-924`: **Robust implementation for handling signup retries**

This logic successfully handles the case when a user presses back from Stripe checkout and attempts to sign up again with the same email and domain, cleaning up stale data before recreating.

---

`975-1016`: **Good validation for domain consistency**

This code enforces consistency between domain names and phone numbers provided in different parts of the signup process, preventing data inconsistencies.

---

`1047-1053`: **Skip payment flow for users joining existing domains**

Appropriate bypass of the billing information process when a user is joining an existing domain rather than creating a new one.

---

`1068-1076`: **Proper tracking of checkout session for recovery**

Good implementation to store checkout session information, which supports the ability to recover if a user goes back from Stripe checkout.

---

`1335-1354`: **Improved error handling for patient user creation**

Added proper error handling with specific error messages for integrity errors when creating patient users.

</details>
<details>
<summary>tests/async/test_rest_auth_views.py (4)</summary>

`163-189`: **Test for proper name handling in patient user creation**

Good test case that verifies patient users are created with the correct first and last name.

---

`991-1038`: **Good test for domain activation edge case**

This test properly verifies that attempting to retry signup with an already active domain fails with the correct error message.

---

`1039-1086`: **Test for domain reuse protection**

Good test that ensures different users cannot reuse the same domain name, preventing potential confusion or security issues.

---

`1087-1143`: **Test for session handling with the same email**

Validates that signup attempts with different session IDs but the same email are handled appropriately, preventing duplicate accounts.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T04:35:33Z
user: login: nyghtowl
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T20:15:52Z

Review comments:
diff_hunk: @@ -1236,16 +1332,26 @@ def perform_create(self, request: Request, serializer) -> Response:
         if "patient_phone_number" in validated_data:
             patient_phone_number = validated_data.pop("patient_phone_number")
         domain_id = auth_utils.get_domain_id_from_request(request)
-        user = create_user(
-            email=validated_data["email"],
-            raw_username=validated_data["username"],
-            first_name=validated_data.get("firstname", ""),
-            last_name=validated_data.get("lastname", ""),
-            domain_name=domain_name,
-            phone_number=provider_phone_number,
-            password=validated_data["password"],
-            domain_id=domain_id,
-        )
+        try:
+            user = create_user(
+                email=validated_data["email"],
+                raw_username=validated_data["username"],
+                first_name=validated_data.get("firstname", ""),
+                last_name=validated_data.get("lastname", ""),
+                domain_name=domain_name,
+                phone_number=provider_phone_number,
+                password=validated_data["password"],
+                domain_id=domain_id,
+            )
path: fhi_users/auth/rest_auth_views.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix mismatch between serializer field names and parameters.**

The serializer defines `first_name` and `last_name`, but here code uses `"firstname"` and `"lastname"`. This discrepancy means the user's name fields will never be populated correctly.


Apply this diff to resolve the naming mismatch:

```diff
- first_name=validated_data.get("firstname", ""),
- last_name=validated_data.get("lastname", ""),
+ first_name=validated_data.get("first_name", ""),
+ last_name=validated_data.get("last_name", ""),
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
            user = create_user(
                email=validated_data["email"],
                raw_username=validated_data["username"],
                first_name=validated_data.get("first_name", ""),
                last_name=validated_data.get("last_name", ""),
                domain_name=domain_name,
                phone_number=provider_phone_number,
                password=validated_data["password"],
                domain_id=domain_id,
            )
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->

✅ Addressed in commits 568f1c8 to afcece4

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-12T18:46:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1236,16 +1332,26 @@ def perform_create(self, request: Request, serializer) -> Response:
         if "patient_phone_number" in validated_data:
             patient_phone_number = validated_data.pop("patient_phone_number")
         domain_id = auth_utils.get_domain_id_from_request(request)
-        user = create_user(
-            email=validated_data["email"],
-            raw_username=validated_data["username"],
-            first_name=validated_data.get("firstname", ""),
-            last_name=validated_data.get("lastname", ""),
-            domain_name=domain_name,
-            phone_number=provider_phone_number,
-            password=validated_data["password"],
-            domain_id=domain_id,
-        )
+        try:
+            user = create_user(
+                email=validated_data["email"],
+                raw_username=validated_data["username"],
+                first_name=validated_data.get("firstname", ""),
+                last_name=validated_data.get("lastname", ""),
+                domain_name=domain_name,
+                phone_number=provider_phone_number,
+                password=validated_data["password"],
+                domain_id=domain_id,
+            )
path: fhi_users/auth/rest_auth_views.py
user: login: nyghtowl
body: @holdenk this is one that you'd want to fix
created_at: 2025-04-12T23:29:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1236,16 +1332,26 @@ def perform_create(self, request: Request, serializer) -> Response:
         if "patient_phone_number" in validated_data:
             patient_phone_number = validated_data.pop("patient_phone_number")
         domain_id = auth_utils.get_domain_id_from_request(request)
-        user = create_user(
-            email=validated_data["email"],
-            raw_username=validated_data["username"],
-            first_name=validated_data.get("firstname", ""),
-            last_name=validated_data.get("lastname", ""),
-            domain_name=domain_name,
-            phone_number=provider_phone_number,
-            password=validated_data["password"],
-            domain_id=domain_id,
-        )
+        try:
+            user = create_user(
+                email=validated_data["email"],
+                raw_username=validated_data["username"],
+                first_name=validated_data.get("firstname", ""),
+                last_name=validated_data.get("lastname", ""),
+                domain_name=domain_name,
+                phone_number=provider_phone_number,
+                password=validated_data["password"],
+                domain_id=domain_id,
+            )
path: fhi_users/auth/rest_auth_views.py
user: login: coderabbitai[bot]
body: > Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

<!-- This is an auto-generated mute comment by CodeRabbit -->
created_at: 2025-04-12T23:32:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1236,16 +1332,26 @@ def perform_create(self, request: Request, serializer) -> Response:
         if "patient_phone_number" in validated_data:
             patient_phone_number = validated_data.pop("patient_phone_number")
         domain_id = auth_utils.get_domain_id_from_request(request)
-        user = create_user(
-            email=validated_data["email"],
-            raw_username=validated_data["username"],
-            first_name=validated_data.get("firstname", ""),
-            last_name=validated_data.get("lastname", ""),
-            domain_name=domain_name,
-            phone_number=provider_phone_number,
-            password=validated_data["password"],
-            domain_id=domain_id,
-        )
+        try:
+            user = create_user(
+                email=validated_data["email"],
+                raw_username=validated_data["username"],
+                first_name=validated_data.get("firstname", ""),
+                last_name=validated_data.get("lastname", ""),
+                domain_name=domain_name,
+                phone_number=provider_phone_number,
+                password=validated_data["password"],
+                domain_id=domain_id,
+            )
path: fhi_users/auth/rest_auth_views.py
user: login: holdenk
body: Good catch, let me make sure we add a test for this too.
created_at: 2025-04-13T03:32:16Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-12T18:01:03Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
Several files have been updated to enhance the professional user signup process. The changes add robust logic for handling existing Stripe checkout sessions and domain validations, clean up stale user data, and prevent duplicate domain creation. A new model and its migration support tracking Stripe sessions. In addition, serializers, logging, and error handling have been improved, and new tests have been introduced to cover various signup scenarios. Minor formatting adjustments were also applied in utility and ML modules.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `fhi_users/auth/rest_auth_views.py` | Enhanced `perform_create` in `ProfessionalUserViewSet` by adding logic to check for existing checkout sessions, clean up stale user and domain data, validate domain status, bypass billing for non-new domains, and store session info in the pending checkout model. |
| `fhi_users/auth/rest_serializers.py` | Updated `UserDomainSerializer` by adding optional `name` and `visible_phone_number` fields; removed an extra blank line for formatting. |
| `fhi_users/migrations/0040_…checkoutsession.py`, `fhi_users/models.py` | Introduced new model `PendingProStripeCheckoutSession` with fields for tracking Stripe session details, including unique session IDs, timestamps, metadata, and foreign keys to user domain and professional user models. |
| `fighthealthinsurance/common_view_logic.py` | Improved error handling and control flow in the `handle_checkout_session_expired` and `generate_appeals` methods with additional domain status checks and refined logging statements. |
| `fighthealthinsurance/ml/ml_models.py` | Reformatted the `get_system_prompts` method by removing unnecessary line breaks and consolidating code for consistency. |
| `fighthealthinsurance/rest_mixins.py` | Added error handling in the `create` method by wrapping deserialization and validation in a try-except block with logging via `loguru`. |
| `tests/async/test_rest_auth_views.py` | Added new test methods covering various signup scenarios: retrying with the same email/domain, handling active domains, different email usage on the same domain, and different session IDs with the same email. |
| `tests/sync/test_rest_api.py` | Modified the duplicate domain creation test to assert an error response key change from `"user_domain"` to `"Domain"` within the nested error structure. |

## Sequence Diagram(s)
```mermaid
sequenceDiagram
    participant U as User
    participant PV as ProfessionalUserViewSet
    participant DM as Domain Manager
    participant PS as PendingProStripeCheckoutSession

    U->>PV: Send sign-up request with email and domain info
    PV->>PV: Check for existing checkout session & domain status
    alt Existing session found
        PV->>DM: Verify if domain is inactive or active
        alt Domain inactive
            PV->>PS: Store checkout session details
            PV->>U: Return next URL for signup completion
        else Domain active
            PV->>U: Return error (active domain)
        end
    else No existing session
        PV->>DM: Validate and create new domain if necessary
        PV->>PS: Create new pending checkout session
        PV->>U: Return next URL for signup completion
    end
```

## Poem
> I'm just a rabbit, with ears so keen,  
> Hopping through code in a vibrant scene.  
> Domains and sessions, aligned just right,  
> Cleaning up data with all my might.  
> In the land of pull requests, I cheer with glee,  
> For new changes that set our signup flow free!  
> 🐰✨

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡💬 Agentic Chat (Pro Plan, General Availability)</summary>
> 
> - We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=fighthealthinsurance/fighthealthinsurance&utm_content=377):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: sentry[bot]
created_at: 2025-04-17T01:29:53Z
body: ## Suspect Issues
This pull request was deployed and Sentry observed the following issues:

- ‼️ **ValidationError: {"user_signup_info":{"visible_phone_number":["Phone number can only contain digits, 'X', and hyphens, and must be at least 7 characters long."]}}** `/v0/auth/rest/router/professional_user/` [View Issue](https://fight-health-insurance.sentry.io/issues/6547068633/?referrer=github-pr-bot)
- ‼️ **Appeal.MultipleObjectsReturned: get() returned more than one Appeal -- it returned 2!** `/ziggy/rest/denials/` [View Issue](https://fight-health-insurance.sentry.io/issues/6558869789/?referrer=github-pr-bot)

<sub>Did you find this useful? React with a 👍 or 👎</sub>
author_association: NONE