Repository: cisagov/manage.get.gov

Issue:
Domain switched to Unknown from Dns Needed which is not allowed in our transitions
### Current Behavior

A user had a new domain that properly switch to dns needed from unknown, however when they were going to add nameservers for the first time, the domain somehow switched _back_ to unknown and created an additional contact at the same time. 
Order of events that occurred:
1. A user's domain request was approved and the domain information/domain objects were created just as expected, with domain set to unknown. 
1. The user logged in to their account and the domain correctly switched to dns_need
1. something occurred (this needs investigating at this point) but a new public contact for administrator was created. For additional info, this public contact did not use default values, instead it used the password that is set _only_ when updating a contact (yet it made a brand new record in public contact with the same registry id).
1. the domain switched from dns_need back to unknown.  (unknown if something happened between these steps). Note, this is supposed to be blocked by django fsm.
1. user recieved a 500 error because to contacts existed for admin

### Expected Behavior

The domain should have stayed in dns needed and no additional contact should have been made.


### Steps to Reproduce

unknown how to reproduce this

### AC 

- [x] investigate the logs for what happened this day and try to trouble shoot why a second contact was made and why the state switched (if unable to determine, mark it as such on this ticket by adding a comment with what you learned). Time box troubleshooting/looking at logs to no more than a few hours (see Additional context).

- [x] don't allow a second public contact to be created with the same registry id, (suggestion: log if this is being attempted). You could make registry registry_id on Public contact unique, but first double check that there are no other duplicates that exists on stable to cause an issue.

- [x] don't throw a 500 if there are two public contacts, instead remove the contact that was formed in error


### Environment

_No response_

### Additional Context

for ac 1: please see [slack thread ](https://cisa-corp.slack.com/archives/C066ZBWFLRW/p1712253262431449)which links to the audit logs entries. from there you should be able to find the timestamps to look for in kibana. Note that kibana shows you your local timezone but audit logs are in utc. Also worth following the code that would have been triggered at this time to see where this issue could have come up. Time box this to **no more than a few hours**, as this could really have been caused by guinicorn's multiple workers. One worker could have been completing the save on the first contact when the other one was made, making this a race condition that should be protected against in this ticket
for ac 3:  right now we throw a 500 error if there are 2 contacts for public contacts. This is being done on line 881 of _get_or_create_public_contact in models/domain. Instead of throwing a 500 error, remove the public contact that doesnt match the one from the registry. if both match delete the oldest

### Issue Links

_No response_


Discussion:

Resolving patch:
diff --git a/src/registrar/migrations/0093_alter_publiccontact_unique_together.py b/src/registrar/migrations/0093_alter_publiccontact_unique_together.py
new file mode 100644
--- /dev/null
+++ b/src/registrar/migrations/0093_alter_publiccontact_unique_together.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.10 on 2024-05-08 17:35
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("registrar", "0092_rename_updated_federal_agency_domaininformation_federal_agency_and_more"),
+    ]
+
+    operations = [
+        migrations.AlterUniqueTogether(
+            name="publiccontact",
+            unique_together={("contact_type", "registry_id", "domain")},
+        ),
+    ]
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -769,10 +769,7 @@ def administrative_contact(self) -> PublicContact | None:
     @administrative_contact.setter  # type: ignore
     def administrative_contact(self, contact: PublicContact):
         logger.info("making admin contact")
-        if contact.contact_type != contact.ContactTypeChoices.ADMINISTRATIVE:
-            raise ValueError("Cannot set a registrant contact with a different contact type")
-        self._make_contact_in_registry(contact=contact)
-        self._update_domain_with_contact(contact, rem=False)
+        self._set_singleton_contact(contact=contact, expectedType=contact.ContactTypeChoices.ADMINISTRATIVE)
 
     def _update_epp_contact(self, contact: PublicContact):
         """Sends UpdateContact to update the actual contact object,
@@ -849,11 +846,9 @@ def _set_singleton_contact(self, contact: PublicContact, expectedType: str):  #
 
         # get publicContact objects that have the matching
         # domain and type but a different id
-        # like in highlander we there can only be one
-        hasOtherContact = (
-            PublicContact.objects.exclude(registry_id=contact.registry_id)
-            .filter(domain=self, contact_type=contact.contact_type)
-            .exists()
+        # like in highlander where there can only be one
+        duplicate_contacts = PublicContact.objects.exclude(registry_id=contact.registry_id).filter(
+            domain=self, contact_type=contact.contact_type
         )
 
         # if no record exists with this contact type
@@ -871,14 +866,10 @@ def _set_singleton_contact(self, contact: PublicContact, expectedType: str):  #
         logger.info("_set_singleton_contact()-> contact has been added to the registry")
 
         # if has conflicting contacts in our db remove them
-        if hasOtherContact:
+        if duplicate_contacts.exists():
             logger.info("_set_singleton_contact()-> updating domain, removing old contact")
 
-            existing_contact = (
-                PublicContact.objects.exclude(registry_id=contact.registry_id)
-                .filter(domain=self, contact_type=contact.contact_type)
-                .get()
-            )
+            existing_contact = duplicate_contacts.get()
 
             if isRegistrant:
                 # send update domain only for registant contacts
@@ -1216,24 +1207,28 @@ def generic_contact_getter(self, contact_type_choice: PublicContact.ContactTypeC
 
     def get_default_security_contact(self):
         """Gets the default security contact."""
+        logger.info("get_default_security_contact() -> Adding default security contact")
         contact = PublicContact.get_default_security()
         contact.domain = self
         return contact
 
     def get_default_administrative_contact(self):
         """Gets the default administrative contact."""
+        logger.info("get_default_security_contact() -> Adding administrative security contact")
         contact = PublicContact.get_default_administrative()
         contact.domain = self
         return contact
 
     def get_default_technical_contact(self):
         """Gets the default technical contact."""
+        logger.info("get_default_security_contact() -> Adding technical security contact")
         contact = PublicContact.get_default_technical()
         contact.domain = self
         return contact
 
     def get_default_registrant_contact(self):
         """Gets the default registrant contact."""
+        logger.info("get_default_security_contact() -> Adding default registrant contact")
         contact = PublicContact.get_default_registrant()
         contact.domain = self
         return contact
@@ -1247,6 +1242,7 @@ def get_contact_in_keys(self, contacts, contact_type):
         Returns:
             PublicContact | None
         """
+        logger.info(f"get_contact_in_keys() -> Grabbing a {contact_type} contact from cache")
         # Registrant doesn't exist as an array, and is of
         # a special data type, so we need to handle that.
         if contact_type == PublicContact.ContactTypeChoices.REGISTRANT:
@@ -1309,6 +1305,7 @@ def _get_or_create_domain(self):
                     logger.error(e.code)
                     raise e
                 if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST and self.state == Domain.State.UNKNOWN:
+                    logger.info("_get_or_create_domain() -> Switching to dns_needed from unknown")
                     # avoid infinite loop
                     already_tried_to_create = True
                     self.dns_needed_from_unknown()
@@ -1319,6 +1316,7 @@ def _get_or_create_domain(self):
                     raise e
 
     def addRegistrant(self):
+        """Adds a default registrant contact"""
         registrant = PublicContact.get_default_registrant()
         registrant.domain = self
         registrant.save()  # calls the registrant_contact.setter
@@ -1346,6 +1344,8 @@ def dns_needed_from_unknown(self):
         self.addAllDefaults()
 
     def addAllDefaults(self):
+        """Adds default security, technical, and administrative contacts"""
+        logger.info("addAllDefaults() -> Adding default security, technical, and administrative contacts")
         security_contact = self.get_default_security_contact()
         security_contact.save()
 
@@ -1360,7 +1360,7 @@ def place_client_hold(self, ignoreEPP=False):
         """place a clienthold on a domain (no longer should resolve)
         ignoreEPP (boolean) - set to true to by-pass EPP (used for transition domains)
         """
-        # TODO - ensure all requirements for client hold are made here
+
         # (check prohibited statuses)
         logger.info("clientHold()-> inside clientHold")
 
@@ -1368,7 +1368,6 @@ def place_client_hold(self, ignoreEPP=False):
         # include this ignoreEPP flag
         if not ignoreEPP:
             self._place_client_hold()
-        # TODO -on the client hold ticket any additional error handling here
 
     @transition(field="state", source=[State.READY, State.ON_HOLD], target=State.READY)
     def revert_client_hold(self, ignoreEPP=False):
@@ -1570,6 +1569,7 @@ def _fetch_contacts(self, contact_data):
 
     def _get_or_create_contact(self, contact: PublicContact):
         """Try to fetch info about a contact. Create it if it does not exist."""
+        logger.info("_get_or_create_contact() -> Fetching contact info")
         try:
             return self._request_contact_info(contact)
         except RegistryError as e:
@@ -1962,10 +1962,23 @@ def _get_or_create_public_contact(self, public_contact: PublicContact):
             domain=self,
         )
 
-        # Raise an error if we find duplicates.
-        # This should not occur
+        # If we find duplicates, log it and delete the oldest ones.
         if db_contact.count() > 1:
-            raise Exception(f"Multiple contacts found for {public_contact.contact_type}")
+            logger.warning("_get_or_create_public_contact() -> Duplicate contacts found. Deleting duplicate.")
+
+            newest_duplicate = db_contact.order_by("-created_at").first()
+
+            duplicates_to_delete = db_contact.exclude(id=newest_duplicate.id)  # type: ignore
+
+            # Delete all duplicates
+            duplicates_to_delete.delete()
+
+            # Do a second filter to grab the latest content
+            db_contact = PublicContact.objects.filter(
+                registry_id=public_contact.registry_id,
+                contact_type=public_contact.contact_type,
+                domain=self,
+            )
 
         # Save to DB if it doesn't exist already.
         if db_contact.count() == 0:
@@ -1977,16 +1990,14 @@ def _get_or_create_public_contact(self, public_contact: PublicContact):
 
         existing_contact = db_contact.get()
 
-        # Does the item we're grabbing match
-        # what we have in our DB?
+        # Does the item we're grabbing match what we have in our DB?
         if existing_contact.email != public_contact.email or existing_contact.registry_id != public_contact.registry_id:
             existing_contact.delete()
             public_contact.save()
             logger.warning("Requested PublicContact is out of sync " "with DB.")
             return public_contact
-        # If it already exists, we can
-        # assume that the DB instance was updated
-        # during set, so we should just use that.
+
+        # If it already exists, we can assume that the DB instance was updated during set, so we should just use that.
         return existing_contact
 
     def _registrant_to_public_contact(self, registry_id: str):
diff --git a/src/registrar/models/public_contact.py b/src/registrar/models/public_contact.py
--- a/src/registrar/models/public_contact.py
+++ b/src/registrar/models/public_contact.py
@@ -19,6 +19,16 @@ def get_id():
 class PublicContact(TimeStampedModel):
     """Contact information intended to be published in WHOIS."""
 
+    class Meta:
+        """Contains meta info about this class"""
+
+        # Creates a composite primary key with these fields.
+        # We can share the same registry id, but only if the contact type is
+        # different or if the domain is different.
+        # For instance - we don't desire to have two admin contacts with the same id
+        # on the same domain.
+        unique_together = [("contact_type", "registry_id", "domain")]
+
     class ContactTypeChoices(models.TextChoices):
         """These are the types of contacts accepted by the registry."""
 


Test patch: