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:
parent
93f4fc6342
commit
b1776494f9
91
apps/feedback/test_workflow_realignment.py
Normal file
91
apps/feedback/test_workflow_realignment.py
Normal 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()
|
||||||
@ -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:
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user