feat(suggestion): optional implemented-notice SMS on close

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.
This commit is contained in:
ismail 2026-07-20 22:13:22 +03:00
parent 93f4fc6342
commit b1776494f9
3 changed files with 115 additions and 3 deletions

View File

@ -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()

View File

@ -913,13 +913,30 @@ def feedback_change_status(request, pk):
# Notify the suggester on acknowledge/close (closed-loop communication). # Notify the suggester on acknowledge/close (closed-loop communication).
# Only for suggestions with a patient-facing contact channel; internal / # Only for suggestions with a patient-facing contact channel; internal /
# source-user submissions have no patient to notify. # 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: if new_status in (FeedbackStatus.ACKNOWLEDGED, FeedbackStatus.CLOSED) and feedback.contact_phone:
try: 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( NotificationService.send_sms(
feedback.contact_phone, feedback.contact_phone,
f"Your suggestion (ref {feedback.reference_number}) has been {label}. " body,
f"Thank you for helping us improve.",
related_object=feedback, related_object=feedback,
) )
except Exception: except Exception:

View File

@ -352,6 +352,10 @@
{% endfor %} {% endfor %}
</select> </select>
<textarea name="note" rows="2" placeholder="{% trans 'Add a note...' %}" class="w-full mt-2 px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-navy"></textarea> <textarea name="note" rows="2" placeholder="{% trans 'Add a note...' %}" class="w-full mt-2 px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-navy"></textarea>
<label class="flex items-center gap-2 mt-2 text-xs text-slate-600 cursor-pointer">
<input type="checkbox" name="implemented_notice" class="rounded border-slate-300 text-navy focus:ring-navy">
<span>{% trans "Implemented — notify the patient with the good news" %}</span>
</label>
<button type="submit" class="w-full mt-2 px-4 py-2 bg-navy text-white text-sm font-semibold rounded-lg hover:bg-navy/90 transition"> <button type="submit" class="w-full mt-2 px-4 py-2 bg-navy text-white text-sm font-semibold rounded-lg hover:bg-navy/90 transition">
<i data-lucide="refresh-cw" class="w-3 h-3 inline-block me-1"></i>{% trans "Update" %} <i data-lucide="refresh-cw" class="w-3 h-3 inline-block me-1"></i>{% trans "Update" %}
</button> </button>