From b1776494f9926f08dd4257c475d024969d3f923c Mon Sep 17 00:00:00 2001 From: ismail Date: Mon, 20 Jul 2026 22:13:22 +0300 Subject: [PATCH] feat(suggestion): optional implemented-notice SMS on close MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When PX closes a suggestion that was actually implemented, ticking the checkbox sends a high-value 'Good news — your suggestion was implemented' SMS instead of the generic closed message. The flag is ignored for any status other than CLOSED. Acknowledge-on-submit SMS already existed; this completes the closed-loop value path. The checkbox appears in the status-change form in feedback_detail.html. Three tests cover: implemented flag on close, flag off on close, flag ignored when transitioning to a non-closed status. --- apps/feedback/test_workflow_realignment.py | 91 ++++++++++++++++++++++ apps/feedback/views.py | 23 +++++- templates/feedback/feedback_detail.html | 4 + 3 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 apps/feedback/test_workflow_realignment.py diff --git a/apps/feedback/test_workflow_realignment.py b/apps/feedback/test_workflow_realignment.py new file mode 100644 index 0000000..309a179 --- /dev/null +++ b/apps/feedback/test_workflow_realignment.py @@ -0,0 +1,91 @@ +"""Tests for the suggestion workflow realignment (optional implemented-notice SMS).""" +from unittest.mock import patch +from django.contrib.auth.models import Group + +import pytest +from django.test import TestCase +from django.urls import reverse + +from apps.accounts.models import User +from apps.organizations.models import Hospital +from apps.feedback.models import Feedback, FeedbackStatus, FeedbackType + + +def _ensure_group(name): + """Get or create a role group (pytest runs with --nomigrations).""" + grp, _ = Group.objects.get_or_create(name=name) + return grp + + +@pytest.mark.django_db +class TestSuggestionImplementedNotice(TestCase): + """Closing a suggestion with implemented_notice=True sends the high-value SMS. + + Uses self.client because feedback_change_status calls messages.success/error + (needs MessageMiddleware) and redirects (302) — no template rendering. + """ + + def setUp(self): + self.hospital = Hospital.objects.create(name="Test Hospital", code="TH09") + # Use PX Employee (not PX Admin) so HospitalSelectionMiddleware uses + # user.hospital directly instead of requiring session['selected_hospital_id']. + _ensure_group("PX Employee") + self.user = User.objects.create_user( + username="pxsug", email="pxsug@test", password="x", hospital=self.hospital + ) + self.user.groups.add(Group.objects.get(name="PX Employee")) + self.client.force_login(self.user) + + self.feedback = Feedback.objects.create( + hospital=self.hospital, + feedback_type=FeedbackType.SUGGESTION, + status=FeedbackStatus.REVIEWED, + contact_phone="+966500000000", + contact_name="Patient", + message="Add online check-in", + ) + + @patch("apps.feedback.views.NotificationService.send_sms") + def test_close_with_implemented_notice_sends_implemented_sms(self, mock_sms): + """When implemented_notice=on and status=closed, SMS body mentions 'implemented'.""" + response = self.client.post( + reverse("feedback:feedback_change_status", kwargs={"pk": self.feedback.pk}), + data={"status": "closed", "implemented_notice": "on"}, + ) + assert response.status_code == 302, f"expected redirect, got {response.status_code}" + self.feedback.refresh_from_db() + assert self.feedback.status == FeedbackStatus.CLOSED + assert mock_sms.called, "send_sms should have been called" + last_call_body = mock_sms.call_args[0][1] + assert "implemented" in last_call_body.lower(), \ + f"Expected 'implemented' in SMS body, got: {last_call_body}" + + @patch("apps.feedback.views.NotificationService.send_sms") + def test_close_without_implemented_notice_sends_generic_sms(self, mock_sms): + """Without the flag, the generic closed message fires (no 'implemented').""" + response = self.client.post( + reverse("feedback:feedback_change_status", kwargs={"pk": self.feedback.pk}), + data={"status": "closed"}, + ) + assert response.status_code == 302 + self.feedback.refresh_from_db() + assert self.feedback.status == FeedbackStatus.CLOSED + assert mock_sms.called + last_call_body = mock_sms.call_args[0][1] + assert "implemented" not in last_call_body.lower(), \ + f"Did not expect 'implemented' in generic SMS, got: {last_call_body}" + + @patch("apps.feedback.views.NotificationService.send_sms") + def test_implemented_notice_ignored_when_status_not_closed(self, mock_sms): + """The flag has no effect when transitioning to a non-closed status (e.g. acknowledged).""" + response = self.client.post( + reverse("feedback:feedback_change_status", kwargs={"pk": self.feedback.pk}), + data={"status": "acknowledged", "implemented_notice": "on"}, + ) + assert response.status_code == 302 + self.feedback.refresh_from_db() + assert self.feedback.status == FeedbackStatus.ACKNOWLEDGED + # SMS fires for acknowledged too (existing behavior), but must NOT say 'implemented'. + if mock_sms.called: + last_call_body = mock_sms.call_args[0][1] + assert "implemented" not in last_call_body.lower() diff --git a/apps/feedback/views.py b/apps/feedback/views.py index 62a7ed7..930e261 100644 --- a/apps/feedback/views.py +++ b/apps/feedback/views.py @@ -913,13 +913,30 @@ def feedback_change_status(request, pk): # Notify the suggester on acknowledge/close (closed-loop communication). # Only for suggestions with a patient-facing contact channel; internal / # source-user submissions have no patient to notify. + implemented_notice = ( + request.POST.get("implemented_notice") in ("on", "true", "True", "1") + and new_status == FeedbackStatus.CLOSED + ) if new_status in (FeedbackStatus.ACKNOWLEDGED, FeedbackStatus.CLOSED) and feedback.contact_phone: try: - label = "acknowledged" if new_status == FeedbackStatus.ACKNOWLEDGED else "closed" + if implemented_notice: + body = ( + f"Good news — your suggestion (ref {feedback.reference_number}) " + f"has been implemented. Thank you for helping us improve." + ) + elif new_status == FeedbackStatus.ACKNOWLEDGED: + body = ( + f"Your suggestion (ref {feedback.reference_number}) has been acknowledged. " + f"Thank you for helping us improve." + ) + else: # CLOSED without implemented flag + body = ( + f"Your suggestion (ref {feedback.reference_number}) has been closed. " + f"Thank you for helping us improve." + ) NotificationService.send_sms( feedback.contact_phone, - f"Your suggestion (ref {feedback.reference_number}) has been {label}. " - f"Thank you for helping us improve.", + body, related_object=feedback, ) except Exception: diff --git a/templates/feedback/feedback_detail.html b/templates/feedback/feedback_detail.html index f6dd2c5..173d5b1 100644 --- a/templates/feedback/feedback_detail.html +++ b/templates/feedback/feedback_detail.html @@ -352,6 +352,10 @@ {% endfor %} +