button force anoncecall

This commit is contained in:
javad committed 2026-09-28 20:35:27 +03:30
1 parent c7993aae44
commit 9af91941d9
15 files changed
+294 -23

No files matched your search

+11
View File
@@ -8,6 +8,17 @@ from tests.e2e.pages.base import BasePage
class TicketConversationPage(BasePage):
guidance_text = "لطفاً همه توضیحات و سؤال‌های خود را در یک پیام کامل بنویسید و سپس ارسال کنید."
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(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_role("region", name="وضعیت اطلاع‌رسانی‌های تلفنی")).to_contain_text("درخواست فوری")
def send_reply(self, body):
self.page.locator("[data-composer-toggle]").click()
reply = self.page.locator('form[data-chat-coach] textarea[name="body"]')
+23 -1
View File
@@ -8,7 +8,7 @@ from playwright.sync_api import expect
from apps.accounts.models import TicketMagicLink, User
from apps.customers.models import ContactGroup, CustomerProfile
from apps.core.models import PageTitle, SiteBranding
from apps.core.models import PageTitle, SiteBranding, VoipConfiguration
from apps.messaging.models import Message
from apps.notifications.models import NotificationJob, SMSTemplate
from apps.staff.models import OrganizationalUnit, StaffProfile
@@ -22,6 +22,28 @@ pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
PASSWORD = "E2e-safe-password-2026"
@pytest.fixture
def immediate_announcement_case():
unit = OrganizationalUnit.objects.create(name="واحد تماس فوری", slug="immediate-call-e2e")
category = TicketCategory.objects.create(unit=unit, title="تماس فوری", slug="immediate-call-e2e")
staff = User.objects.create_superuser(username="immediate-call-e2e", password=PASSWORD, account_type=User.AccountType.STAFF)
StaffProfile.objects.create(user=staff, display_name="کارشناس تماس فوری", primary_unit=unit)
customer = User.objects.create_user(username="immediate-call-customer-e2e", mobile="09120009122", account_type=User.AccountType.CUSTOMER)
ticket = create_ticket(customer=customer, created_by=staff, category=category, subject="پیگیری تلفنی فوری", body="پیام نخست")
VoipConfiguration.objects.create(announcement_url="http://127.0.0.1:9000/v1/announcements", announcement_id="custom/test")
return staff, ticket
def test_staff_can_request_immediate_announcement_from_ticket_sidebar(live_server, browser_page, immediate_announcement_case):
staff, ticket = immediate_announcement_case
with browser_page() as page:
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()
def test_staff_operations_pages_are_authorized_and_visually_styled(live_server, browser_page):
branding = SiteBranding.load()
branding.staff_brand_title = "مرکز عملیات E2E"
+106 -1
View File
@@ -4,6 +4,7 @@ from unittest.mock import Mock, patch
import pytest
import requests
from django.core.management import call_command
from django.urls import reverse
from django.utils import timezone
from apps.accounts.models import User
@@ -12,7 +13,7 @@ from apps.core.models import VoipConfiguration
from apps.messaging.models import MessageReadReceipt
from apps.notifications.models import AnnouncementJob
from apps.staff.models import OrganizationalUnit
from apps.tickets.models import TicketCategory
from apps.tickets.models import TicketAccessGrant, TicketCategory
from apps.tickets.services import create_ticket, reply_ticket
@@ -222,3 +223,107 @@ def test_staff_ticket_shows_call_history_without_claiming_connection(client, ann
assert "پذیرش درخواست، به‌تنهایی برقرار شدن" in content
assert configuration.announcement_url not in content
assert configuration.announcement_api_key not in content
def test_staff_can_queue_immediate_call_and_see_it_in_ticket(client, announcement_data):
category, staff, customer, configuration = announcement_data
ticket = create_ticket(customer=customer, created_by=staff, category=category, subject="تماس فوری", body="پیام نخست")
automatic = AnnouncementJob.objects.get(message__ticket=ticket)
MessageReadReceipt.objects.create(message=automatic.message, user=customer)
detail_url = reverse("staff_tickets:detail", args=[ticket.public_id])
call_url = reverse("staff_tickets:announcement_now", args=[ticket.public_id])
client.force_login(staff)
detail = client.get(detail_url).content.decode()
assert call_url in detail
assert "اطلاع‌رسانی تلفنی فوری" in detail
assert "پیام اطلاع‌رسانی از پیش ضبط‌شده" in detail
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()
assert job.status == AnnouncementJob.Status.SENT
assert automatic.status == AnnouncementJob.Status.PENDING
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
def test_immediate_call_coalesces_repeated_clicks(client, announcement_data):
category, staff, customer, _ = 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)
assert "در دقیقهٔ گذشته ثبت شده است" in second.content.decode()
assert AnnouncementJob.objects.filter(ticket=ticket, source=AnnouncementJob.Source.MANUAL).count() == 1
def test_immediate_call_requires_staff_reply_access_and_configuration(client, announcement_data):
category, staff, customer, configuration = announcement_data
ticket = create_ticket(customer=customer, category=category, subject="تماس فوری", body="پیام مشتری")
url = reverse("staff_tickets:announcement_now", args=[ticket.public_id])
outsider = User.objects.create_user(username="announcement-viewer", account_type="STAFF")
TicketAccessGrant.objects.create(ticket=ticket, user=outsider, granted_by=staff, permission_level=TicketAccessGrant.Level.VIEW_ONLY)
client.force_login(outsider)
assert url not in client.get(reverse("staff_tickets:detail", args=[ticket.public_id])).content.decode()
assert client.post(url).status_code == 403
client.force_login(customer)
assert client.post(url).status_code == 403
assert not AnnouncementJob.objects.filter(ticket=ticket).exists()
client.force_login(staff)
configuration.announcement_url = ""
configuration.save()
page = client.get(reverse("staff_tickets:detail", args=[ticket.public_id])).content.decode()
assert "اطلاع‌رسانی تلفنی فوری" in page and "disabled" in page
result = client.post(url, follow=True)
assert "تنظیمات اطلاع‌رسانی تلفنی کامل نیست." in result.content.decode()
assert not AnnouncementJob.objects.filter(ticket=ticket).exists()
def test_immediate_call_is_processed_before_older_scheduled_calls(client, announcement_data):
category, staff, customer, _ = announcement_data
ticket = create_ticket(customer=customer, created_by=staff, category=category, subject="تماس فوری", body="پیام نخست")
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]))
immediate = AnnouncementJob.objects.get(ticket=ticket, source=AnnouncementJob.Source.MANUAL)
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
def test_immediate_call_retries_provider_outage_without_breaking_ticket(client, announcement_data):
category, staff, customer, _ = 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)
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()
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
assert client.get(reverse("staff_tickets:detail", args=[ticket.public_id])).status_code == 200
due(job)
with patch("apps.integrations.announcements.requests.post", return_value=Mock(content=b"")) as retry:
call_command("process_notifications", once=True)
job.refresh_from_db()
assert job.status == AnnouncementJob.Status.SENT
assert retry.call_args.kwargs["headers"]["Idempotency-Key"] == job.idempotency_key