fix button force anoncecall
This commit is contained in:
1 parent
9af91941d9
commit
4525ec3e6a
9 files changed
+99
-48
No files matched your search
@@ -85,7 +85,7 @@ def _deliver(job):
|
||||
job.last_error = ""
|
||||
|
||||
|
||||
def process_due_announcements():
|
||||
def process_due_announcements(*, job_id=None):
|
||||
now = timezone.now()
|
||||
# A crashed worker can safely retry with the same provider idempotency key.
|
||||
AnnouncementJob.objects.filter(
|
||||
@@ -93,13 +93,15 @@ def process_due_announcements():
|
||||
next_attempt_at__lt=now - timedelta(minutes=10),
|
||||
).update(status=AnnouncementJob.Status.PENDING, next_attempt_at=now)
|
||||
with transaction.atomic():
|
||||
jobs = list(
|
||||
queryset = (
|
||||
AnnouncementJob.objects.select_for_update(skip_locked=True)
|
||||
.filter(status=AnnouncementJob.Status.PENDING, next_attempt_at__lte=now)
|
||||
.select_related("message__ticket__customer", "ticket__customer")
|
||||
.annotate(priority=Case(When(source=AnnouncementJob.Source.MANUAL, then=Value(0)), default=Value(1), output_field=IntegerField()))
|
||||
.order_by("priority", "next_attempt_at", "pk")[:20]
|
||||
)
|
||||
if job_id is not None:
|
||||
queryset = queryset.filter(pk=job_id)
|
||||
jobs = list(queryset.order_by("priority", "next_attempt_at", "pk")[:20])
|
||||
AnnouncementJob.objects.filter(pk__in=[job.pk for job in jobs]).update(
|
||||
status=AnnouncementJob.Status.PROCESSING, next_attempt_at=now,
|
||||
)
|
||||
|
||||
@@ -251,9 +251,19 @@ def _process_delivery_checks(options):
|
||||
NotificationJob.objects.filter(pk__in=[job.pk for job in jobs]).update(
|
||||
delivery_next_check_at=now + timedelta(minutes=10),
|
||||
)
|
||||
provider = get_sms_provider()
|
||||
if not jobs:
|
||||
return
|
||||
try:
|
||||
provider = get_sms_provider()
|
||||
except Exception:
|
||||
logger.exception("SMS delivery provider initialization failed")
|
||||
return
|
||||
for job in jobs:
|
||||
result = provider.get_delivery_status(job.provider_message_id)
|
||||
try:
|
||||
result = provider.get_delivery_status(job.provider_message_id)
|
||||
except Exception:
|
||||
logger.exception("SMS delivery check failed for job %s", job.pk)
|
||||
continue
|
||||
if result is None:
|
||||
job.delivery_status = ""
|
||||
job.delivery_next_check_at = None
|
||||
|
||||
+13
-2
@@ -472,8 +472,19 @@ def staff_announcement_now(request, public_id):
|
||||
elif not ticket.customer.is_active or not ticket.customer.local_mobile:
|
||||
messages.error(request,"شماره همراه مخاطب برای تماس در دسترس نیست.")
|
||||
else:
|
||||
_,created=schedule_immediate_announcement(ticket,request.user)
|
||||
messages.success(request,"درخواست اطلاعرسانی تلفنی فوری در صف قرار گرفت." if created else "درخواست اطلاعرسانی تلفنی این تیکت در دقیقهٔ گذشته ثبت شده است.")
|
||||
job,created=schedule_immediate_announcement(ticket,request.user)
|
||||
if created:
|
||||
from apps.notifications.announcement_worker import process_due_announcements
|
||||
process_due_announcements(job_id=job.pk)
|
||||
job.refresh_from_db()
|
||||
if job.status == AnnouncementJob.Status.SENT:
|
||||
messages.success(request,"درخواست تماس توسط سرویس پذیرفته شد.")
|
||||
elif job.attempts:
|
||||
messages.warning(request,"سرویس تماس پاسخ نداد؛ تلاش مجدد در صف قرار گرفت.")
|
||||
else:
|
||||
messages.info(request,"درخواست تماس در حال پردازش است.")
|
||||
else:
|
||||
messages.info(request,"درخواست اطلاعرسانی تلفنی این تیکت در دقیقهٔ گذشته ثبت شده است.")
|
||||
return redirect("staff_tickets:detail",public_id=public_id)
|
||||
@staff_required
|
||||
@require_POST
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
/* Staff organizational operations — intentionally scoped to avoid regressions. */
|
||||
.immediate-announcement-card{display:grid;gap:.7rem;border:1px solid #c9e4df;background:#f8fcfb}
|
||||
.immediate-announcement-head{display:flex;align-items:center;gap:.65rem}
|
||||
.immediate-announcement-head h3{margin:0;color:#164b47;font-size:.86rem;line-height:1.6}
|
||||
.immediate-announcement-icon{display:grid;place-items:center;width:2rem;height:2rem;flex:none;border-radius:.65rem;background:#e2f3ef;color:#0f766e;font-size:1.05rem}
|
||||
.immediate-announcement-card p{margin:0;color:#45605f;font-size:.73rem;line-height:1.85}
|
||||
.immediate-announcement-card{display:grid;gap:.3rem;padding:.65rem;border:1px solid #c9e4df;background:#f8fcfb}
|
||||
.immediate-announcement-card form{margin:0}
|
||||
.immediate-announcement-button{display:flex;align-items:center;justify-content:center;gap:.5rem;width:100%;min-height:2.75rem;border:1px solid #0f766e;border-radius:.7rem;background:#0f766e;color:#fff;font-size:.76rem;font-weight:800;cursor:pointer;transition:background .15s ease,box-shadow .15s ease}
|
||||
.immediate-announcement-button{display:flex;align-items:center;justify-content:center;gap:.4rem;width:100%;min-height:2.25rem;border:1px solid #0f766e;border-radius:.55rem;background:#0f766e;color:#fff;font-size:.73rem;font-weight:800;cursor:pointer;transition:background .15s ease,box-shadow .15s ease}
|
||||
.immediate-announcement-button:hover:not(:disabled){background:#0b5f59;box-shadow:0 5px 15px -10px #0b5f59}
|
||||
.immediate-announcement-button:focus-visible{outline:2px solid #0f766e;outline-offset:3px}
|
||||
.immediate-announcement-button:disabled{border-color:#aebfc0;background:#aebfc0;cursor:not-allowed}
|
||||
.immediate-announcement-card small{color:#607574;font-size:.65rem;line-height:1.7}
|
||||
.immediate-announcement-card small{color:#607574;font-size:.62rem;line-height:1.45;text-align:center}
|
||||
.operations-page{display:grid;gap:1rem;min-width:0}
|
||||
.operations-page .staff-page-head{margin-bottom:0}
|
||||
.operations-page .ticket-stream,.operations-page .filters{border:1px solid #d9e4ee;background:#fff;box-shadow:0 18px 45px -38px rgba(8,55,107,.55)}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
{% if can_request_announcement %}
|
||||
<section class="info-card immediate-announcement-card" aria-label="اطلاعرسانی تلفنی فوری">
|
||||
<div class="immediate-announcement-head"><span class="immediate-announcement-icon" aria-hidden="true">☎</span><h3>اطلاعرسانی تلفنی فوری</h3></div>
|
||||
<p>این دکمه درخواست تماس با مخاطب و پخش یک پیام اطلاعرسانی از پیش ضبطشده را ثبت میکند.</p>
|
||||
<form method="post" action="{% url 'staff_tickets:announcement_now' ticket.public_id %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="immediate-announcement-button" {% if not announcement_ready %}disabled{% endif %}>
|
||||
<span aria-hidden="true">☎</span> ارسال اطلاعرسانی تلفنی فوری
|
||||
<span aria-hidden="true">☎</span> اطلاعرسانی تلفنی فوری
|
||||
</button>
|
||||
</form>
|
||||
<small>{% if announcement_ready %}درخواست در صف تماس ثبت میشود؛ نتیجه را در وضعیت اطلاعرسانیهای تلفنی ببینید.{% else %}شماره مخاطب یا تنظیمات سرویس تماس در دسترس نیست.{% endif %}</small>
|
||||
<small>{% if announcement_ready %}پخش پیام صوتی از پیش ضبطشده برای مخاطب{% else %}شماره مخاطب یا تنظیمات تماس در دسترس نیست.{% endif %}</small>
|
||||
</section>
|
||||
{% endif %}
|
||||
@@ -17,8 +17,8 @@
|
||||
<aside class="workspace-side actions-side" id="ticket-actions" data-workspace-panel>
|
||||
<button type="button" class="panel-close" data-close-panel>×</button>
|
||||
<section class="info-card"><p class="panel-index">وضعیت تیکت</p><div class="info-list"><div class="info-row"><span>وضعیت</span><b>{{ ticket.get_status_display }}</b></div><div class="info-row"><span>اولویت</span><b>{{ ticket.get_priority_display }}</b></div><div class="info-row"><span>دسته</span><b>{{ ticket.category }}</b></div><div class="info-row"><span>مالک</span><b>{{ ticket.assigned_expert_name }}</b></div>{% if ticket.first_response_due_at %}<div class="info-row"><span>مهلت پاسخ</span><b>{{ ticket.first_response_due_at|jalali_datetime }}</b></div>{% endif %}</div></section>
|
||||
{% include 'tickets/_immediate_announcement.html' %}
|
||||
{% if sms_jobs %}<section class="info-card sms-delivery-card"><div class="sms-delivery-heading"><div><h3>وضعیت پیامکهای مشتری</h3><small>خلاصه ارسال و تحویل</small></div><span aria-hidden="true">ⓘ</span></div><div class="sms-delivery-list">{% for sms_job in sms_jobs %}<details class="sms-delivery-item"><summary><span class="sms-delivery-event">{% if sms_job.event_type == 'STAFF_TICKET_CREATED' %}ثبت تیکت توسط کارشناس{% elif sms_job.event_type == 'CUSTOMER_TICKET_CREATED' %}ثبت تیکت مشتری{% elif sms_job.event_type == 'STAFF_REPLY' %}اعلان پاسخ کارشناس{% else %}پیامک سیستمی{% endif %}</span><b>{% if sms_job.delivery_status %}{{ sms_job.get_delivery_status_display }}{% else %}{{ sms_job.get_status_display }}{% endif %}</b><span class="sms-delivery-info" aria-label="نمایش جزئیات زمانبندی پیامک" title="نمایش جزئیات">i</span></summary><ol class="sms-delivery-timeline"><li class="is-complete"><span></span><div><b>ثبت درخواست ارسال</b><time datetime="{{ sms_job.created_at|date:'c' }}">{{ sms_job.created_at|jalali_datetime }}</time></div></li><li class="{% if sms_job.sent_at %}is-complete{% endif %}"><span></span><div><b>پذیرش توسط درگاه پیامک</b>{% if sms_job.sent_at %}<time datetime="{{ sms_job.sent_at|date:'c' }}">{{ sms_job.sent_at|jalali_datetime }}</time>{% else %}<small>{% if sms_job.status == 'FAILED' %}ناموفق{% elif sms_job.status == 'CANCELLED' %}لغوشده پیش از ارسال{% else %}در انتظار ارسال{% endif %}</small>{% endif %}</div></li><li class="{% if sms_job.delivery_status == 'DELIVERED' %}is-complete{% endif %}"><span></span><div><b>تأیید رسیدن به گوشی</b>{% if sms_job.delivery_status == 'DELIVERED' and sms_job.delivery_checked_at %}<time datetime="{{ sms_job.delivery_checked_at|date:'c' }}">{{ sms_job.delivery_checked_at|jalali_datetime }}</time>{% elif sms_job.delivery_checked_at %}<small>آخرین استعلام: {{ sms_job.delivery_checked_at|jalali_datetime }}</small>{% else %}<small>{% if sms_job.sent_at %}در انتظار نتیجه اپراتور{% else %}هنوز ارسال نشده{% endif %}</small>{% endif %}</div></li></ol>{% if sms_job.delivery_description %}<p class="sms-delivery-description">{{ sms_job.delivery_description }}</p>{% endif %}{% if sms_job.last_error %}<p class="sms-delivery-error">علت دقیق: {{ sms_job.last_error }}</p>{% endif %}{% if sms_job.provider_message_id %}<small class="sms-delivery-tracking" dir="ltr">شناسه رهگیری: {{ sms_job.provider_message_id }}</small>{% endif %}{% if sms_job.sms_retryable %}<form method="post" action="{% url 'staff_tickets:sms_retry' ticket.public_id sms_job.pk %}" class="inline-retry-form">{% csrf_token %}<button type="submit" aria-label="تلاش مجدد برای ارسال این پیامک" title="تلاش مجدد"><span aria-hidden="true">↻</span> تلاش مجدد</button></form>{% endif %}</details>{% endfor %}</div></section>{% endif %}
|
||||
{% include 'tickets/_immediate_announcement.html' %}
|
||||
{% include 'tickets/_announcement_status.html' %}
|
||||
<section class="info-card ticket-operations"><h3>عملیات تیکت</h3>{% if perms.tickets.change_ticket %}<form method="post" action="{% url 'staff_tickets:status' ticket.public_id %}">{% csrf_token %}<label>وضعیت<select name="status">{% for value,label in statuses %}<option value="{{ value }}" {% if ticket.status == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label><button>ثبت وضعیت</button></form>{% endif %}{% if perms.tickets.change_ticket_priority %}<form method="post" action="{% url 'staff_tickets:priority' ticket.public_id %}">{% csrf_token %}<label>اولویت<select name="priority">{% for value,label in priorities %}<option value="{{ value }}" {% if ticket.priority == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></label><button class="secondary">ثبت اولویت</button></form>{% endif %}{% if can_transfer %}<details><summary>انتقال تیکت</summary><form method="post" action="{% url 'staff_tickets:transfer' ticket.public_id %}">{% csrf_token %}{{ transfer_form.as_p }}<button>انتقال</button></form></details>{% endif %}{% if can_share %}<details><summary>اشتراک با کارشناس</summary><form method="post" action="{% url 'staff_tickets:share' ticket.public_id %}">{% csrf_token %}{{ share_form.as_p }}<button>ایجاد دسترسی</button></form>{% for grant in grants %}<div class="grant"><span>{{ grant.user.staff_profile.display_name }}</span><form method="post" action="{% url 'staff_tickets:revoke' ticket.public_id grant.user.public_id %}">{% csrf_token %}<button class="danger-button">لغو</button></form></div>{% endfor %}</details>{% endif %}</section>
|
||||
{% if can_delete %}<section class="info-card danger-zone"><h3>حذف کامل تیکت</h3><p class="muted">تمام پیامها، فایلها و سوابق تیکت برای همیشه حذف میشوند.</p><form method="post" action="{% url 'staff_tickets:delete' ticket.public_id %}" onsubmit="return confirm('آیا از حذف کامل و غیرقابلبازگشت این تیکت مطمئن هستید؟')">{% csrf_token %}<button type="submit" class="danger-button">حذف کامل تیکت</button></form></section>{% endif %}
|
||||
|
||||
@@ -11,12 +11,12 @@ class TicketConversationPage(BasePage):
|
||||
def request_immediate_announcement(self):
|
||||
card = self.page.get_by_role("region", name="اطلاعرسانی تلفنی فوری")
|
||||
expect(card).to_be_visible()
|
||||
expect(card).to_contain_text("پیام اطلاعرسانی از پیش ضبطشده")
|
||||
button = card.get_by_role("button", name="ارسال اطلاعرسانی تلفنی فوری")
|
||||
expect(card).to_contain_text("پیام صوتی از پیش ضبطشده")
|
||||
button = card.get_by_role("button", name="اطلاعرسانی تلفنی فوری")
|
||||
expect(button).to_be_enabled()
|
||||
with self.page.expect_navigation(wait_until="domcontentloaded"):
|
||||
button.click()
|
||||
expect(self.page.get_by_text("درخواست اطلاعرسانی تلفنی فوری در صف قرار گرفت.")).to_be_visible()
|
||||
expect(self.page.get_by_text("درخواست تماس توسط سرویس پذیرفته شد.")).to_be_visible()
|
||||
expect(self.page.get_by_role("region", name="وضعیت اطلاعرسانیهای تلفنی")).to_contain_text("درخواست فوری")
|
||||
|
||||
def send_reply(self, body):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import base64
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from django.contrib.auth.models import Permission
|
||||
@@ -41,7 +42,9 @@ def test_staff_can_request_immediate_announcement_from_ticket_sidebar(live_serve
|
||||
AuthenticationPage(page, live_server.url).login_staff(staff, PASSWORD)
|
||||
conversation = TicketConversationPage(page, live_server.url)
|
||||
conversation.goto(reverse("staff_tickets:detail", args=[ticket.public_id]))
|
||||
conversation.request_immediate_announcement()
|
||||
with patch("apps.integrations.announcements.requests.post", return_value=Mock(content=b"")) as post:
|
||||
conversation.request_immediate_announcement()
|
||||
post.assert_called_once()
|
||||
|
||||
|
||||
def test_staff_operations_pages_are_authorized_and_visually_styled(live_server, browser_page):
|
||||
|
||||
+56
-25
@@ -11,7 +11,7 @@ from apps.accounts.models import User
|
||||
from apps.core.admin import VoipConfigurationAdmin
|
||||
from apps.core.models import VoipConfiguration
|
||||
from apps.messaging.models import MessageReadReceipt
|
||||
from apps.notifications.models import AnnouncementJob
|
||||
from apps.notifications.models import AnnouncementJob, NotificationJob
|
||||
from apps.staff.models import OrganizationalUnit
|
||||
from apps.tickets.models import TicketAccessGrant, TicketCategory
|
||||
from apps.tickets.services import create_ticket, reply_ticket
|
||||
@@ -34,7 +34,7 @@ def announcement_people(db):
|
||||
@pytest.fixture
|
||||
def announcement_data(announcement_catalog, announcement_people):
|
||||
configuration = VoipConfiguration.objects.create(
|
||||
announcement_url="http://192.168.138.215:8000/v1/announcements",
|
||||
announcement_url="http://37.255.224.143:8009/v1/announcements",
|
||||
announcement_id="custom/fereshte1_G711",
|
||||
announcement_delay_minutes=90,
|
||||
)
|
||||
@@ -236,28 +236,26 @@ def test_staff_can_queue_immediate_call_and_see_it_in_ticket(client, announcemen
|
||||
detail = client.get(detail_url).content.decode()
|
||||
assert call_url in detail
|
||||
assert "اطلاعرسانی تلفنی فوری" in detail
|
||||
assert "پیام اطلاعرسانی از پیش ضبطشده" in detail
|
||||
assert "پیام صوتی از پیش ضبطشده" in detail
|
||||
assert detail.index("وضعیت پیامکهای مشتری") < detail.index('aria-label="اطلاعرسانی تلفنی فوری"')
|
||||
assert client.get(call_url).status_code == 405
|
||||
|
||||
response = client.post(call_url, follow=True)
|
||||
assert response.status_code == 200
|
||||
assert "درخواست اطلاعرسانی تلفنی فوری در صف قرار گرفت." in response.content.decode()
|
||||
job = AnnouncementJob.objects.get(ticket=ticket, source=AnnouncementJob.Source.MANUAL)
|
||||
assert job.message is None and job.requested_by == staff
|
||||
assert job.next_attempt_at <= timezone.now()
|
||||
assert "درخواست فوری" in response.content.decode()
|
||||
assert AnnouncementJob.objects.get(pk=automatic.pk).status == AnnouncementJob.Status.PENDING
|
||||
|
||||
provider_response = Mock(content=b"{}")
|
||||
provider_response.json.return_value = {}
|
||||
with patch("apps.integrations.announcements.requests.post", return_value=provider_response) as post:
|
||||
call_command("process_notifications", once=True)
|
||||
job.refresh_from_db()
|
||||
automatic.refresh_from_db()
|
||||
response = client.post(call_url, follow=True)
|
||||
assert response.status_code == 200
|
||||
assert "درخواست تماس توسط سرویس پذیرفته شد." in response.content.decode()
|
||||
job = AnnouncementJob.objects.get(ticket=ticket, source=AnnouncementJob.Source.MANUAL)
|
||||
assert job.message is None and job.requested_by == staff
|
||||
assert job.status == AnnouncementJob.Status.SENT
|
||||
assert automatic.status == AnnouncementJob.Status.PENDING
|
||||
assert "درخواست فوری" in response.content.decode()
|
||||
assert AnnouncementJob.objects.get(pk=automatic.pk).status == AnnouncementJob.Status.PENDING
|
||||
|
||||
post.assert_called_once()
|
||||
assert post.call_args.args == (configuration.announcement_url,)
|
||||
assert post.call_args.kwargs["json"] == {"customer_number": customer.local_mobile, "announcement_id": configuration.announcement_id}
|
||||
assert post.call_args.kwargs["headers"]["Idempotency-Key"] == job.idempotency_key
|
||||
assert post.call_args.kwargs["headers"] == {"Idempotency-Key": job.idempotency_key}
|
||||
|
||||
|
||||
def test_immediate_call_coalesces_repeated_clicks(client, announcement_data):
|
||||
@@ -265,10 +263,12 @@ def test_immediate_call_coalesces_repeated_clicks(client, announcement_data):
|
||||
ticket = create_ticket(customer=customer, category=category, subject="تماس فوری", body="پیام مشتری")
|
||||
url = reverse("staff_tickets:announcement_now", args=[ticket.public_id])
|
||||
client.force_login(staff)
|
||||
assert client.post(url).status_code == 302
|
||||
second = client.post(url, follow=True)
|
||||
with patch("apps.integrations.announcements.requests.post", return_value=Mock(content=b"")) as post:
|
||||
assert client.post(url).status_code == 302
|
||||
second = client.post(url, follow=True)
|
||||
assert "در دقیقهٔ گذشته ثبت شده است" in second.content.decode()
|
||||
assert AnnouncementJob.objects.filter(ticket=ticket, source=AnnouncementJob.Source.MANUAL).count() == 1
|
||||
post.assert_called_once()
|
||||
|
||||
|
||||
def test_immediate_call_requires_staff_reply_access_and_configuration(client, announcement_data):
|
||||
@@ -300,11 +300,14 @@ def test_immediate_call_is_processed_before_older_scheduled_calls(client, announ
|
||||
for message in ticket.ticket_messages.all():
|
||||
due(AnnouncementJob.objects.get(message=message))
|
||||
client.force_login(staff)
|
||||
client.post(reverse("staff_tickets:announcement_now", args=[ticket.public_id]))
|
||||
with patch("apps.integrations.announcements.requests.post", return_value=Mock(content=b"")) as immediate_post:
|
||||
client.post(reverse("staff_tickets:announcement_now", args=[ticket.public_id]))
|
||||
immediate = AnnouncementJob.objects.get(ticket=ticket, source=AnnouncementJob.Source.MANUAL)
|
||||
assert immediate.status == AnnouncementJob.Status.SENT
|
||||
immediate_post.assert_called_once()
|
||||
with patch("apps.integrations.announcements.requests.post", return_value=Mock(content=b"")) as post:
|
||||
call_command("process_notifications", once=True)
|
||||
assert post.call_args_list[0].kwargs["headers"]["Idempotency-Key"] == immediate.idempotency_key
|
||||
assert all(call.kwargs["headers"]["Idempotency-Key"] != immediate.idempotency_key for call in post.call_args_list)
|
||||
|
||||
|
||||
def test_immediate_call_retries_provider_outage_without_breaking_ticket(client, announcement_data):
|
||||
@@ -312,11 +315,10 @@ def test_immediate_call_retries_provider_outage_without_breaking_ticket(client,
|
||||
ticket = create_ticket(customer=customer, category=category, subject="تماس فوری", body="پیام مشتری")
|
||||
url = reverse("staff_tickets:announcement_now", args=[ticket.public_id])
|
||||
client.force_login(staff)
|
||||
client.post(url)
|
||||
job = AnnouncementJob.objects.get(ticket=ticket, source=AnnouncementJob.Source.MANUAL)
|
||||
with patch("apps.integrations.announcements.requests.post", side_effect=requests.ConnectionError("provider secret")) as post:
|
||||
call_command("process_notifications", once=True)
|
||||
job.refresh_from_db()
|
||||
result = client.post(url, follow=True)
|
||||
job = AnnouncementJob.objects.get(ticket=ticket, source=AnnouncementJob.Source.MANUAL)
|
||||
assert "تلاش مجدد در صف قرار گرفت" in result.content.decode()
|
||||
assert job.status == AnnouncementJob.Status.PENDING
|
||||
assert job.last_error == "service_unreachable" and job.attempts == 1
|
||||
assert post.call_args.kwargs["headers"]["Idempotency-Key"] == job.idempotency_key
|
||||
@@ -327,3 +329,32 @@ def test_immediate_call_retries_provider_outage_without_breaking_ticket(client,
|
||||
job.refresh_from_db()
|
||||
assert job.status == AnnouncementJob.Status.SENT
|
||||
assert retry.call_args.kwargs["headers"]["Idempotency-Key"] == job.idempotency_key
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_worker_skips_sms_provider_when_no_delivery_check_is_due():
|
||||
with patch("apps.notifications.management.commands.process_notifications.get_sms_provider", side_effect=RuntimeError("SMS provider unavailable")) as sms_provider:
|
||||
call_command("process_notifications", once=True)
|
||||
sms_provider.assert_not_called()
|
||||
|
||||
|
||||
def test_sms_delivery_provider_error_does_not_stop_worker(announcement_data):
|
||||
category, staff, customer, _ = announcement_data
|
||||
ticket = create_ticket(customer=customer, created_by=staff, category=category, subject="تماس فوری", body="پیام")
|
||||
sms_job = NotificationJob.objects.create(
|
||||
channel=NotificationJob.Channel.SMS,
|
||||
event_type="CUSTOMER_TICKET_CREATED",
|
||||
ticket=ticket,
|
||||
customer=customer,
|
||||
status=NotificationJob.Status.SENT,
|
||||
next_attempt_at=timezone.now(),
|
||||
delivery_status=NotificationJob.DeliveryStatus.PENDING,
|
||||
delivery_next_check_at=timezone.now() - timedelta(seconds=1),
|
||||
provider_message_id="provider-id",
|
||||
dedupe_key="sms-delivery-failure-does-not-stop-worker",
|
||||
)
|
||||
with patch("apps.notifications.management.commands.process_notifications.get_sms_provider", side_effect=RuntimeError("provider unavailable")):
|
||||
call_command("process_notifications", once=True)
|
||||
sms_job.refresh_from_db()
|
||||
assert sms_job.delivery_status == NotificationJob.DeliveryStatus.PENDING
|
||||
assert sms_job.delivery_next_check_at > timezone.now()
|
||||
Reference in new issue
Block a user