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).
|
||||
# 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:
|
||||
|
||||
@ -352,6 +352,10 @@
|
||||
{% endfor %}
|
||||
</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>
|
||||
<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">
|
||||
<i data-lucide="refresh-cw" class="w-3 h-3 inline-block me-1"></i>{% trans "Update" %}
|
||||
</button>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user