Repository: cisagov/manage.get.gov

Issue:
Reduce email notifications for some status changes
### Issue description

As a domain requestor
I want to receive notifications of status changes ONLY when those changes initially occur
So that I do not get excessive emails which are not meaningful to me

Existing behavior: A notification email is sent to the requestor upon each occurrence of a status change. (Submitted, withdrawn, approved, rejected)


### Acceptance criteria

- [x] No notification is sent when the request moves from "Action Needed" to "Submitted" (requester re-submits with changes)
- [x] No notification is sent when the request moves from "In Review" back to "Submitted" (analyst returned it to the queue)
- [x] #1582

### Additional context

Consider the following statuses, listed horizontally, with the possible transitions listed vertically below each.
![Image](https://github.com/cisagov/manage.get.gov/assets/62573986/0853ad49-e9a2-42a8-9eb8-eef8c70dadf7)

For those statuses that have the corresponding email icon, a notification email should be sent when the request INITIALLY enters that status. Subsequent status changes may take place which might return the request to a previously held status. When this happens, a second email should NOT be sent to the requestor as this adds no valuable information and may even be confusing to the user.
 
For example, a request may be `submitted` by the requestor, which triggers a notification email confirming the submission. Later, the request may be moved to `In Review` (which does not use a notification). If this request is moved back to `submitted` status, this does NOT trigger another notification email, as it has already been in that status.

### Links to other issues

Related to:  #1582 

_No response_


Discussion:

Resolving patch:
diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py
--- a/src/registrar/models/domain_application.py
+++ b/src/registrar/models/domain_application.py
@@ -654,11 +654,15 @@ def submit(self):
         self.submission_date = timezone.now().date()
         self.save()
 
-        self._send_status_update_email(
-            "submission confirmation",
-            "emails/submission_confirmation.txt",
-            "emails/submission_confirmation_subject.txt",
-        )
+        # Limit email notifications to transitions from Started and Withdrawn
+        limited_statuses = [self.ApplicationStatus.STARTED, self.ApplicationStatus.WITHDRAWN]
+
+        if self.status in limited_statuses:
+            self._send_status_update_email(
+                "submission confirmation",
+                "emails/submission_confirmation.txt",
+                "emails/submission_confirmation_subject.txt",
+            )
 
     @transition(
         field="status",
@@ -764,6 +768,7 @@ def approve(self, send_email=True):
     )
     def withdraw(self):
         """Withdraw an application that has been submitted."""
+
         self._send_status_update_email(
             "withdraw",
             "emails/domain_request_withdrawn.txt",


Test patch:
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -306,6 +306,7 @@ def test_form_choices_when_ineligible(self):
         )
 
 
+@boto3_mocking.patching
 class TestDomainApplicationAdmin(MockEppLib):
     def setUp(self):
         super().setUp()
@@ -411,29 +412,27 @@ def test_short_org_name_in_applications_list(self):
         # Now let's make sure the long description does not exist
         self.assertNotContains(response, "Federal: an agency of the U.S. government")
 
-    @boto3_mocking.patching
-    def test_save_model_sends_submitted_email(self):
-        # make sure there is no user with this email
-        EMAIL = "mayor@igorville.gov"
-        User.objects.filter(email=EMAIL).delete()
+    def transition_state_and_send_email(self, application, status):
+        """Helper method for the email test cases."""
 
         with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
             with less_console_noise():
-                # Create a sample application
-                application = completed_application()
-
                 # Create a mock request
                 request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
 
                 # Modify the application's property
-                application.status = DomainApplication.ApplicationStatus.SUBMITTED
+                application.status = status
 
                 # Use the model admin's save_model method
                 self.admin.save_model(request, application, form=None, change=True)
 
+    def assert_email_is_accurate(self, expected_string, email_index, email_address):
+        """Helper method for the email test cases.
+        email_index is the index of the email in mock_client."""
+
         # Access the arguments passed to send_email
         call_args = self.mock_client.EMAILS_SENT
-        kwargs = call_args[0]["kwargs"]
+        kwargs = call_args[email_index]["kwargs"]
 
         # Retrieve the email details from the arguments
         from_email = kwargs.get("FromEmailAddress")
@@ -442,52 +441,137 @@ def test_save_model_sends_submitted_email(self):
         email_body = email_content["Simple"]["Body"]["Text"]["Data"]
 
         # Assert or perform other checks on the email details
-        expected_string = "We received your .gov domain request."
         self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
-        self.assertEqual(to_email, EMAIL)
+        self.assertEqual(to_email, email_address)
         self.assertIn(expected_string, email_body)
 
+    def test_save_model_sends_submitted_email(self):
+        """When transitioning to submitted from started or withdrawn on a domain request,
+        an email is sent out.
+
+        When transitioning to submitted from dns needed or in review on a domain request,
+        no email is sent out."""
+
+        # Ensure there is no user with this email
+        EMAIL = "mayor@igorville.gov"
+        User.objects.filter(email=EMAIL).delete()
+
+        # Create a sample application
+        application = completed_application()
+
+        # Test Submitted Status from started
+        self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
+        self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL)
         self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
 
-    @boto3_mocking.patching
+        # Test Withdrawn Status
+        self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN)
+        self.assert_email_is_accurate(
+            "Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL
+        )
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
+
+        # Test Submitted Status Again (from withdrawn)
+        self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
+
+        # Move it to IN_REVIEW
+        self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.IN_REVIEW)
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
+
+        # Test Submitted Status Again from in IN_REVIEW, no new email should be sent
+        self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
+
+        # Move it to IN_REVIEW
+        self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.IN_REVIEW)
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
+
+        # Move it to ACTION_NEEDED
+        self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.ACTION_NEEDED)
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
+
+        # Test Submitted Status Again from in ACTION_NEEDED, no new email should be sent
+        self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
+
     def test_save_model_sends_approved_email(self):
-        # make sure there is no user with this email
+        """When transitioning to approved on a domain request,
+        an email is sent out every time."""
+
+        # Ensure there is no user with this email
         EMAIL = "mayor@igorville.gov"
         User.objects.filter(email=EMAIL).delete()
 
-        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
-            with less_console_noise():
-                # Create a sample application
-                application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
+        # Create a sample application
+        application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
 
-                # Create a mock request
-                request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
+        # Test Submitted Status
+        self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
+        self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 0, EMAIL)
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
 
-                # Modify the application's property
-                application.status = DomainApplication.ApplicationStatus.APPROVED
+        # Test Withdrawn Status
+        self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED)
+        self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, EMAIL)
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
 
-                # Use the model admin's save_model method
-                self.admin.save_model(request, application, form=None, change=True)
+        # Test Submitted Status Again (No new email should be sent)
+        self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
 
-        # Access the arguments passed to send_email
-        call_args = self.mock_client.EMAILS_SENT
-        kwargs = call_args[0]["kwargs"]
+    def test_save_model_sends_rejected_email(self):
+        """When transitioning to rejected on a domain request,
+        an email is sent out every time."""
 
-        # Retrieve the email details from the arguments
-        from_email = kwargs.get("FromEmailAddress")
-        to_email = kwargs["Destination"]["ToAddresses"][0]
-        email_content = kwargs["Content"]
-        email_body = email_content["Simple"]["Body"]["Text"]["Data"]
+        # Ensure there is no user with this email
+        EMAIL = "mayor@igorville.gov"
+        User.objects.filter(email=EMAIL).delete()
 
-        # Assert or perform other checks on the email details
-        expected_string = "Congratulations! Your .gov domain request has been approved."
-        self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
-        self.assertEqual(to_email, EMAIL)
-        self.assertIn(expected_string, email_body)
+        # Create a sample application
+        application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
+
+        # Test Submitted Status
+        self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED)
+        self.assert_email_is_accurate("Your .gov domain request has been rejected.", 0, EMAIL)
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
+
+        # Test Withdrawn Status
+        self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
+        self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
+
+        # Test Submitted Status Again (No new email should be sent)
+        self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED)
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
+
+    def test_save_model_sends_withdrawn_email(self):
+        """When transitioning to withdrawn on a domain request,
+        an email is sent out every time."""
+
+        # Ensure there is no user with this email
+        EMAIL = "mayor@igorville.gov"
+        User.objects.filter(email=EMAIL).delete()
+
+        # Create a sample application
+        application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
 
+        # Test Submitted Status
+        self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN)
+        self.assert_email_is_accurate(
+            "Your .gov domain request has been withdrawn and will not be reviewed by our team.", 0, EMAIL
+        )
         self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
 
-    @boto3_mocking.patching
+        # Test Withdrawn Status
+        self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
+        self.assert_email_is_accurate("We received your .gov domain request.", 1, EMAIL)
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
+
+        # Test Submitted Status Again (No new email should be sent)
+        self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN)
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
+
     def test_save_model_sets_approved_domain(self):
         # make sure there is no user with this email
         EMAIL = "mayor@igorville.gov"
@@ -510,45 +594,6 @@ def test_save_model_sets_approved_domain(self):
         # Test that approved domain exists and equals requested domain
         self.assertEqual(application.requested_domain.name, application.approved_domain.name)
 
-    @boto3_mocking.patching
-    def test_save_model_sends_rejected_email(self):
-        # make sure there is no user with this email
-        EMAIL = "mayor@igorville.gov"
-        User.objects.filter(email=EMAIL).delete()
-
-        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
-            with less_console_noise():
-                # Create a sample application
-                application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
-
-                # Create a mock request
-                request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
-
-                # Modify the application's property
-                application.status = DomainApplication.ApplicationStatus.REJECTED
-
-                # Use the model admin's save_model method
-                self.admin.save_model(request, application, form=None, change=True)
-
-        # Access the arguments passed to send_email
-        call_args = self.mock_client.EMAILS_SENT
-        kwargs = call_args[0]["kwargs"]
-
-        # Retrieve the email details from the arguments
-        from_email = kwargs.get("FromEmailAddress")
-        to_email = kwargs["Destination"]["ToAddresses"][0]
-        email_content = kwargs["Content"]
-        email_body = email_content["Simple"]["Body"]["Text"]["Data"]
-
-        # Assert or perform other checks on the email details
-        expected_string = "Your .gov domain request has been rejected."
-        self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
-        self.assertEqual(to_email, EMAIL)
-        self.assertIn(expected_string, email_body)
-
-        self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
-
-    @boto3_mocking.patching
     def test_save_model_sets_restricted_status_on_user(self):
         # make sure there is no user with this email
         EMAIL = "mayor@igorville.gov"
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py
--- a/src/registrar/tests/test_models.py
+++ b/src/registrar/tests/test_models.py
@@ -161,33 +161,63 @@ def test_status_fsm_submit_succeed(self):
                     application.submit()
             self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED)
 
-    def test_submit_sends_email(self):
-        """Create an application and submit it and see if email was sent."""
-        with less_console_noise():
-            user, _ = User.objects.get_or_create(username="testy")
-            contact = Contact.objects.create(email="test@test.gov")
-            domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
-            application = DomainApplication.objects.create(
-                creator=user,
-                requested_domain=domain,
-                submitter=contact,
-            )
-            application.save()
+    def check_email_sent(self, application, msg, action, expected_count):
+        """Check if an email was sent after performing an action."""
 
+        with self.subTest(msg=msg, action=action):
             with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
-                application.submit()
-
-            # check to see if an email was sent
-            self.assertGreater(
-                len(
-                    [
-                        email
-                        for email in MockSESClient.EMAILS_SENT
-                        if "test@test.gov" in email["kwargs"]["Destination"]["ToAddresses"]
-                    ]
-                ),
-                0,
-            )
+                with less_console_noise():
+                    # Perform the specified action
+                    action_method = getattr(application, action)
+                    action_method()
+
+            # Check if an email was sent
+            sent_emails = [
+                email
+                for email in MockSESClient.EMAILS_SENT
+                if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"]
+            ]
+            self.assertEqual(len(sent_emails), expected_count)
+
+    def test_submit_from_started_sends_email(self):
+        msg = "Create an application and submit it and see if email was sent."
+        application = completed_application()
+        self.check_email_sent(application, msg, "submit", 1)
+
+    def test_submit_from_withdrawn_sends_email(self):
+        msg = "Create a withdrawn application and submit it and see if email was sent."
+        application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
+        self.check_email_sent(application, msg, "submit", 1)
+
+    def test_submit_from_action_needed_does_not_send_email(self):
+        msg = "Create an application with ACTION_NEEDED status and submit it, check if email was not sent."
+        application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
+        self.check_email_sent(application, msg, "submit", 0)
+
+    def test_submit_from_in_review_does_not_send_email(self):
+        msg = "Create a withdrawn application and submit it and see if email was sent."
+        application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
+        self.check_email_sent(application, msg, "submit", 0)
+
+    def test_approve_sends_email(self):
+        msg = "Create an application and approve it and see if email was sent."
+        application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
+        self.check_email_sent(application, msg, "approve", 1)
+
+    def test_withdraw_sends_email(self):
+        msg = "Create an application and withdraw it and see if email was sent."
+        application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
+        self.check_email_sent(application, msg, "withdraw", 1)
+
+    def test_reject_sends_email(self):
+        msg = "Create an application and reject it and see if email was sent."
+        application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
+        self.check_email_sent(application, msg, "reject", 1)
+
+    def test_reject_with_prejudice_does_not_send_email(self):
+        msg = "Create an application and reject it with prejudice and see if email was sent."
+        application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
+        self.check_email_sent(application, msg, "reject_with_prejudice", 0)
 
     def test_submit_transition_allowed(self):
         """