some improve for sms delivery

This commit is contained in:
javad committed 2026-09-16 10:40:21 +03:30
1 parent 1d3cff0b63
commit ffc1ec0dcf
10 files changed
+134 -11

No files matched your search

+15
View File
@@ -18,6 +18,21 @@ class TicketConversationPage(BasePage):
assert "reply_result=reply" in self.page.url
expect(self.page.locator("[data-conversation]")).to_contain_text(body)
def open_composer(self):
launch = self.page.locator("[data-composer-toggle]")
expect(launch).to_be_visible()
launch.click()
expect(self.page.locator("[data-composer-shell]:not(.is-collapsed)")).to_be_visible()
def expect_latest_message_above_composer(self):
latest = self.page.locator("[data-message]").last
expect(latest).to_be_visible()
latest.scroll_into_view_if_needed()
message_box = latest.bounding_box()
composer_box = self.page.locator("[data-composer-shell]").bounding_box()
assert message_box and composer_box
assert message_box["y"] + message_box["height"] <= composer_box["y"] + 1
@property
def chat_guidance(self):
return self.page.locator(".chat-coach-floating")
+30
View File
@@ -3,6 +3,7 @@ from django.urls import reverse
from apps.accounts.models import User
from apps.customers.models import CustomerProfile
from apps.messaging.models import Message
from apps.staff.models import StaffProfile
from apps.tickets.models import TicketCategory
from apps.tickets.services import create_ticket
@@ -76,3 +77,32 @@ def test_staff_sees_chat_guidance_after_two_rapid_replies(live_server, browser_p
conversation.send_reply("پاسخ سوم کارشناس")
conversation.expect_guidance_visible()
conversation.dismiss_guidance()
def test_customer_mobile_composer_never_covers_latest_message(live_server, browser_page):
category = TicketCategory.objects.create(title="مکاتبات موبایل", slug="e2e-customer-mobile-composer")
customer = User.objects.create_user(
username="e2e-mobile-customer",
mobile="09120009021",
password=PASSWORD,
account_type=User.AccountType.CUSTOMER,
)
CustomerProfile.objects.create(user=customer, first_name="مشتری", last_name="موبایل")
staff = User.objects.create_user(
username="e2e-mobile-staff",
mobile="09120009022",
account_type=User.AccountType.STAFF,
)
StaffProfile.objects.create(user=staff, display_name="کارشناس موبایل")
ticket = create_ticket(customer=customer, category=category, subject="کنترل composer مشتری", body="پیام آغازین")
for index in range(24):
Message.objects.create(ticket=ticket, author=staff, body=f"پاسخ طولانی کارشناس شماره {index + 1}")
with browser_page() as page:
page.set_viewport_size({"width": 390, "height": 844})
AuthenticationPage(page, live_server.url).login_customer(customer, PASSWORD)
conversation = TicketConversationPage(page, live_server.url)
conversation.goto(reverse("tickets:detail", args=[ticket.public_id]))
conversation.expect_latest_message_above_composer()
conversation.open_composer()
conversation.expect_latest_message_above_composer()
+7
View File
@@ -9,11 +9,13 @@ 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.messaging.models import Message
from apps.notifications.models import NotificationJob, SMSTemplate
from apps.staff.models import OrganizationalUnit, StaffProfile
from apps.tickets.models import TicketCategory
from apps.tickets.services import create_ticket, dispatch_group_tickets
from tests.e2e.pages.auth import AuthenticationPage
from tests.e2e.pages.tickets import TicketConversationPage
pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
@@ -153,6 +155,8 @@ def test_dispatch_report_presents_delivery_and_engagement_professionally(live_se
delivery_description="0098SMS [کد 5]: پیامک به گوشی گیرنده رسیده است.",
delivery_check_attempts=2,delivery_checked_at=timezone.now(),delivery_next_check_at=None,
)
for index in range(24):
Message.objects.create(ticket=ticket,author=staff,body=f"مکاتبه طولانی شماره {index + 1}")
links=[TicketMagicLink.issue_with_record(ticket,created_by=staff)[1] for _ in range(4)]
links[0].view_count=2; links[0].used_at=timezone.now(); links[0].save(update_fields=["view_count","used_at"])
@@ -190,6 +194,8 @@ def test_dispatch_report_presents_delivery_and_engagement_professionally(live_se
assert box["x"] >= 0 and box["x"] + box["width"] <= 390
assert box["y"] >= 0 and box["y"] + box["height"] <= 844
assert page.evaluate("document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1")
conversation=TicketConversationPage(page,live_server.url)
conversation.expect_latest_message_above_composer()
launch.click()
close_composer = page.get_by_role("button", name="بستن", exact=True)
expect(close_composer).to_be_visible()
@@ -197,5 +203,6 @@ def test_dispatch_report_presents_delivery_and_engagement_professionally(live_se
assert close_box is not None
assert close_box["x"] >= 0 and close_box["x"] + close_box["width"] <= 390
assert close_box["y"] >= 0 and close_box["y"] + close_box["height"] <= 844
conversation.expect_latest_message_above_composer()
dismiss_context.click()
expect(page.locator(".ticket-dispatch-context")).to_have_count(0)
+31 -1
View File
@@ -8,6 +8,7 @@ from django.utils import timezone
from django.core.files.uploadedfile import SimpleUploadedFile
from apps.attachments.models import Attachment, AttachmentPolicy
from apps.accounts.models import User, normalize_mobile
from apps.messaging.models import MessageReadReceipt
from apps.notifications.models import NotificationJob
from apps.staff.models import OrganizationalUnit, StaffProfile
from apps.tickets.models import Ticket, TicketCategory
@@ -499,7 +500,11 @@ def test_due_unread_staff_reply_is_sent_once(category,customer,monkeypatch):
from apps.integrations.sms.base import SMSResult
from apps.notifications.management.commands import process_notifications
a=make_staff("a"); ticket=create_ticket(customer=customer,category=category,subject="پاسخ خوانده نشده",selected_expert=a,body="متن")
reply_ticket(ticket_id=ticket.pk,user=a,body="پاسخ کارشناس")
message=reply_ticket(ticket_id=ticket.pk,user=a,body="پاسخ کارشناس")
observer=make_staff("observer")
other_customer=User.objects.create_user(username="other-sms-customer",mobile="09123334444",account_type="CUSTOMER")
MessageReadReceipt.objects.create(message=message,user=observer)
MessageReadReceipt.objects.create(message=message,user=other_customer)
job=NotificationJob.objects.get(ticket=ticket,event_type="STAFF_REPLY")
NotificationJob.objects.filter(pk=job.pk).update(next_attempt_at=timezone.now()-timedelta(seconds=1))
ticket.last_activity_at=timezone.now()-timedelta(hours=1); ticket.save(update_fields=["last_activity_at"])
@@ -514,6 +519,31 @@ def test_due_unread_staff_reply_is_sent_once(category,customer,monkeypatch):
assert len(sent)==1
@pytest.mark.django_db
def test_due_staff_reply_is_cancelled_only_when_ticket_customer_has_seen_it(category,customer,monkeypatch):
from django.core.management import call_command
from apps.notifications.management.commands import process_notifications
staff=make_staff("a")
ticket=create_ticket(customer=customer,category=category,subject="پاسخ دیده شده",selected_expert=staff,body="متن")
message=reply_ticket(ticket_id=ticket.pk,user=staff,body="پاسخ کارشناس")
MessageReadReceipt.objects.create(message=message,user=customer)
job=NotificationJob.objects.get(ticket=ticket,event_type="STAFF_REPLY")
NotificationJob.objects.filter(pk=job.pk).update(next_attempt_at=timezone.now()-timedelta(seconds=1))
ticket.last_activity_at=timezone.now()-timedelta(hours=1)
ticket.save(update_fields=["last_activity_at"])
class Provider:
def send_message(self,mobile,message):
raise AssertionError("پیام دیده‌شده توسط خود مشتری نباید به درگاه ارسال شود")
monkeypatch.setattr(process_notifications,"get_sms_provider",lambda:Provider())
call_command("process_notifications",once=True,job_id=job.pk)
job.refresh_from_db()
assert job.status==NotificationJob.Status.CANCELLED
assert job.last_error=="customer_read"
@pytest.mark.django_db
def test_every_failed_sms_gets_exactly_three_retries_three_minutes_apart(category,customer,monkeypatch,settings):
from django.core.management import call_command
+41 -1
View File
@@ -252,6 +252,42 @@ def test_staff_reply_feedback_and_new_reply_action_remain_available(client):
assert 'composer staff-composer is-collapsed' in content
assert 'accept="image/*"' in content and "data-gallery-input" in content and "data-document-input" in content
@pytest.mark.django_db
def test_staff_ticket_sms_status_is_compact_expandable_and_time_precise(client):
from apps.core.templatetags.jalali import jalali_datetime
from apps.notifications.models import NotificationJob
category=TicketCategory.objects.create(title="رهگیری پیامک",slug="sms-timeline")
customer=User.objects.create_user(username="sms-timeline-customer",mobile="09120000031",account_type="CUSTOMER")
expert=User.objects.create_user(username="sms-timeline-expert",mobile="09120000032",account_type="STAFF")
StaffProfile.objects.create(user=expert,display_name="کارشناس رهگیری")
expert.user_permissions.add(Permission.objects.get(content_type__app_label="tickets",codename="view_ticket"))
ticket=create_ticket(customer=customer,category=category,subject="نمایش دقیق وضعیت پیامک",selected_expert=expert,body="شرح")
job=NotificationJob.objects.get(ticket=ticket,event_type="CUSTOMER_TICKET_CREATED")
sent_at=timezone.now()-timedelta(minutes=4)
delivered_at=timezone.now()-timedelta(minutes=1)
NotificationJob.objects.filter(pk=job.pk).update(
status=NotificationJob.Status.SENT,
sent_at=sent_at,
provider_message_id="123456789",
delivery_status=NotificationJob.DeliveryStatus.DELIVERED,
delivery_status_code="5",
delivery_description="پیامک به گوشی گیرنده رسیده است.",
delivery_checked_at=delivered_at,
delivery_next_check_at=None,
)
client.force_login(expert)
content=client.get(reverse("staff_tickets:detail",args=[ticket.public_id])).content.decode()
assert 'class="sms-delivery-item"' in content
assert 'aria-label="نمایش جزئیات زمان‌بندی پیامک"' in content
for label in ("ثبت درخواست ارسال","پذیرش توسط درگاه پیامک","تأیید رسیدن به گوشی"):
assert label in content
assert jalali_datetime(sent_at) in content
assert jalali_datetime(delivered_at) in content
assert "123456789" in content
@pytest.mark.django_db
def test_staff_sidebar_hides_admin_link_and_shows_developer_credit(client):
expert=User.objects.create_superuser(username="sidebar-admin",mobile="09384999655",password="test-pass",account_type="STAFF")
@@ -392,7 +428,7 @@ def test_ticket_polling_is_versioned_incremental_and_permission_safe(client):
def test_ticket_polling_javascript_keeps_background_sync_and_recovers_reliably():
script=Path("static/js/conversation.js").read_text(encoding="utf-8")
for feature in ("visibilitychange","document.visibilityState==='visible'","AbortController","pollRunning","nextPollAt","pollHeartbeat","setInterval(startPolling,1000)","cache:'no-store'","const restartPolling=()=>{stopPolling();startPolling()}","window.addEventListener('pageshow',restartPolling)","window.addEventListener('online',restartPolling)","data-scroll-to-submit","scrollIntoView","submit-attention"):
for feature in ("visibilitychange","document.visibilityState==='visible'","AbortController","pollRunning","nextPollAt","pollHeartbeat","setInterval(startPolling,1000)","cache:'no-store'","const restartPolling=()=>{stopPolling();startPolling()}","window.addEventListener('pageshow',restartPolling)","window.addEventListener('online',restartPolling)","data-scroll-to-submit","scrollIntoView","submit-attention","keepLatestVisible","requestAnimationFrame"):
assert feature in script
assert "pollUrl&&navigator.onLine" not in script
assert "window.addEventListener('offline',stopPolling)" not in script
@@ -403,6 +439,10 @@ def test_ticket_polling_javascript_keeps_background_sync_and_recovers_reliably()
assert "scroller.scrollTo({top,behavior:'smooth'})" in script
assert "data-message-id" in Path("templates/tickets/_messages.html").read_text(encoding="utf-8")
assert "acknowledgeVisibleMessages" in script and "document.visibilityState!=='visible'" in script
source_css=Path("static/src/tailwind.css").read_text(encoding="utf-8")
assert ".staff-ticket-workspace .composer.is-collapsed{position:fixed" not in source_css
assert ".staff-ticket-workspace .composer:not(.is-collapsed){position:fixed" not in source_css
assert "max-height:min(45dvh,28rem)" in source_css
for menu_feature in ("data-message-menu","data-message-menu-close","closeMessageMenu","event.key==='Escape'","!menu.contains(event.target)"):
assert menu_feature in script or menu_feature in Path("templates/tickets/_messages.html").read_text(encoding="utf-8")