some improve

This commit is contained in:
javad committed 2026-10-27 11:49:09 +03:30
1 parent fa73a734a0
commit 2e5d0bcdd3
17 files changed
+1512 -226

No files matched your search

+80 -2
View File
@@ -368,6 +368,25 @@ class AppointmentLifetimeSimulationTests(SimulationBase):
make_action(self.customer, 'nasb', 'finish', action_dt=NOW, finish_dt=NOW)
self.assertNotIn(self.customer.pk, manual_holds_map())
def test_same_microsecond_visit_still_kills_appointment(self):
"""ثبت بازدید در همان لحظهٔ هماهنگی هم قرار را باطل می‌کند.
``createdDT`` روی SQLite/ویندوز دقت کافی ندارد و دو رکورد پشت‌سرهم می‌توانند
مقدار یکسان بگیرند؛ مقایسه با ``id`` به‌عنوان مرز تساوی، این حالت را هم
درست تشخیص می‌دهد (قبلاً تست به زمان اجرا وابسته و ناپایدار بود).
"""
from admin_panel.models import Action
self._hold_customer()
visit = make_action(self.customer, 'bazdid', 'finish',
action_dt=NOW, finish_dt=NOW)
# هر دو رکورد همان createdDT را می‌گیرند (بازسازی شرط مسابقه).
stamp = Action.objects.get(
customer=self.customer, actionType='hamahangi',
).createdDT
Action.objects.filter(pk=visit.pk).update(createdDT=stamp)
self.assertNotIn(self.customer.pk, manual_holds_map())
class FollowupDaySimulationTests(SimulationBase):
"""فهرست «نیازمند پیگیری امروز»: کارِ باقی‌مانده، نه آرشیو."""
@@ -397,6 +416,45 @@ class FollowupDaySimulationTests(SimulationBase):
make_action(self.customer, 'bazdid', 'finish', action_dt=NOW, finish_dt=NOW)
self.assertIn(self.customer.pk, followups_on_date(yesterday))
# --- «نصب» هم مثل بازدید فهرست کار را می‌بندد ---
# قاعدهٔ کسب‌وکار: نصب و بازدید هر دو چرخه را از نو شروع می‌کنند
# (``_CYCLE_RESET_TYPES``) و قرار دستی را باطل می‌کنند؛ پس فهرست «نیازمند
# پیگیری» هم باید با هر دو بسته شود.
def test_install_done_today_clears_today_row(self):
"""«نصبِ انجام‌شدهٔ امروز» هم ردیف امروز را می‌بندد (مثل بازدید)."""
make_action(self.customer, 'hamahangi', 'finish',
action_dt=NOW, finish_dt=NOW)
self.assertIn(self.customer.pk, followups_on_date(timezone.localdate()))
make_action(self.customer, 'nasb', 'finish', action_dt=NOW, finish_dt=NOW)
self.assertNotIn(self.customer.pk, followups_on_date(timezone.localdate()))
ids, _ = self.stale_ids('today')
self.assertNotIn(self.customer.pk, ids)
def test_scheduled_install_hides_today_row(self):
"""«نصب» با وضعیت waite و تاریخ **آینده** ⇒ ردیف امروز پنهان می‌شود."""
make_action(self.customer, 'hamahangi', 'finish',
action_dt=NOW, finish_dt=NOW)
self.assertIn(self.customer.pk, followups_on_date(timezone.localdate()))
make_action(self.customer, 'nasb', 'waite', action_dt=at(7))
self.assertNotIn(self.customer.pk, followups_on_date(timezone.localdate()))
ids, _ = self.stale_ids('today')
self.assertNotIn(self.customer.pk, ids)
def test_old_pending_install_does_not_hide(self):
"""نصبِ «در انتظار» با تاریخ گذشته قرار فعال نیست ⇒ ردیف می‌ماند."""
make_action(self.customer, 'hamahangi', 'finish',
action_dt=NOW, finish_dt=NOW)
make_action(self.customer, 'nasb', 'waite', action_dt=at(-30))
self.assertIn(self.customer.pk, followups_on_date(timezone.localdate()))
def test_install_yesterday_does_not_clear_today_row(self):
"""نصبِ دیروز، موعدِ امروز را نمی‌بندد (اگر موعد تازه‌ای گذاشته شده)."""
make_action(self.customer, 'hamahangi', 'finish',
action_dt=NOW, finish_dt=NOW)
make_action(self.customer, 'nasb', 'finish',
action_dt=at(-1), finish_dt=at(-1))
self.assertIn(self.customer.pk, followups_on_date(timezone.localdate()))
class TaskListBoundaryTests(SimulationBase):
"""«لیست وظایف امروز»: مرزهای روز به وقت محلی و aware.
@@ -744,8 +802,9 @@ class MissedViewSimulationTests(SimulationBase):
"""رفتار فهرست ``?view=today&missed=1`` پس از رفع باگ.
قاعدهٔ کسب‌وکار: این فهرست فقط «پیگیری‌های واقعاً باقی‌مانده» را نشان می‌دهد.
هر مشتری که (الف) بازدیدش از موعد به بعد انجام شده، یا (ب) قرار دستیِ معتبر
دارد، یا (ج) بازدیدِ در انتظار از آن روز دارد، از فهرست خارج می‌شود.
هر مشتری که (الف) بازدید یا نصبش از موعد به بعد انجام شده، یا (ب) قرار
دستیِ معتبر دارد، یا (ج) بازدید/نصبِ در انتظار از آن روز دارد، از فهرست
خارج می‌شود.
"""
URL = '/panel/entezar/?view=today&missed=1'
@@ -807,6 +866,25 @@ class MissedViewSimulationTests(SimulationBase):
ids, _ = self.missed()
self.assertNotIn(self.customer.pk, ids)
def test_pending_install_removes_from_missed(self):
"""«نصبِ» در انتظار با تاریخ آینده هم مشتری را از missed خارج می‌کند."""
make_action(self.customer, 'hamahangi', 'finish',
action_dt=at(-3), finish_dt=at(-3))
make_action(self.customer, 'nasb', 'waite', action_dt=at(5))
ids, _ = self.missed()
self.assertNotIn(self.customer.pk, ids)
def test_finished_install_after_coordination_is_not_listed(self):
"""هماهنگی دیروز + نصب انجام‌شدهٔ امروز ⇒ نباید در missed بیاید."""
make_action(self.customer, 'hamahangi', 'finish',
action_dt=at(-1, -2), finish_dt=at(-1))
make_action(self.customer, 'nasb', 'finish',
action_dt=NOW, finish_dt=NOW)
ids, response = self.missed()
self.assertNotIn(self.customer.pk, ids)
self.assertNotIn(self.customer.pk, overdue_followups())
self.assertEqual(response.context['counts']['followup_overdue'], 0)
def test_old_pending_visit_does_not_remove(self):
"""بازدیدِ در انتظارِ قدیمی‌تر از موعد، قرار فعال نیست ⇒ در missed می‌ماند."""
make_action(self.customer, 'hamahangi', 'finish',
+416 -4
View File
@@ -2177,14 +2177,14 @@ class EventTriggerTests(PanelTestCase):
mark_primary(customer, '09120000000')
return customer
# ---------- قرارداد: دقیقاً ۷ نوع ----------
# ---------- قرارداد: دقیقاً ۸ نوع ----------
def test_exactly_seven_sms_types(self):
self.assertEqual(len(SMSType.choices), 7)
def test_exactly_eight_sms_types(self):
self.assertEqual(len(SMSType.choices), 8)
self.assertEqual(
{code for code, _label in SMSType.choices},
{'welcome', 'survey', 'cheque_registration', 'visit',
'visit_created', 'payment_thanks', 'nesiye_due'},
'visit_created', 'install_created', 'payment_thanks', 'nesiye_due'},
)
def test_removed_types_have_no_template(self):
@@ -2198,6 +2198,15 @@ class EventTriggerTests(PanelTestCase):
seeded = set(SMSTemplate.objects.values_list('sms_type', flat=True))
self.assertEqual(seeded, {code for code, _label in SMSType.choices})
def test_install_template_is_seeded_with_auto_send_off(self):
"""قالب «ثبت نصب» فعال است ولی «ارسال خودکار» آن خاموش (نیازمند تأیید)."""
template = SMSTemplate.objects.get(sms_type=SMSType.INSTALL_CREATED)
self.assertTrue(template.is_enabled)
self.assertFalse(template.auto_send)
self.assertEqual(template.offset_days, 0)
self.assertIn('{name}', template.text)
self.assertIn('{date}', template.text)
# ---------- نظرسنجی + تشکر از پرداخت نقدی روی فاکتور ----------
def _post_factor(self, customer, payments):
@@ -2284,6 +2293,244 @@ class EventTriggerTests(PanelTestCase):
SMSQueue.objects.filter(sms_type=SMSType.VISIT_CREATED).exists(),
)
# ---------- تبدیل نوع اقدام در «ویرایش» ----------
# باگ گزارش‌شده: تریگر «بازدید ثبت شد» فقط در ویوی ایجاد صدا زده می‌شد، پس
# تبدیل یک اقدام «هماهنگی» به «بازدید» هیچ پیامکی نمی‌فرستاد.
def _visit_sms(self, customer):
"""همهٔ ردیف‌های «ثبت بازدید» (هر وضعیتی) — برای شمارش کل."""
return SMSQueue.objects.filter(customer=customer,
sms_type=SMSType.VISIT_CREATED)
def _pending_visit_sms(self, customer):
"""فقط ردیف‌هایی که worker ممکن است بفرستد (در انتظار/تأییدشده)."""
return self._visit_sms(customer).filter(
status__in=(SMSQueue.Status.PENDING, SMSQueue.Status.APPROVED),
)
def _edit_action(self, customer, action_type, *, is_done=True):
"""ویرایش اولین اقدام مشتری با نوع جدید (از مسیر واقعی ویو)."""
from admin_panel.models import Action
action = Action.objects.filter(customer=customer).order_by('id').first()
url = reverse('admin_panel:customerEditAction',
kwargs={'c_id': customer.id, 'action_id': action.id})
payload = {
'actionType': action_type,
'dateTimeAnjam': '1700000000000',
'desc': 'ویرایش', 'userID': self.user.id,
'FinishDateTime': '1700000000000',
}
if is_done:
payload['is_done'] = 'on'
response = self.client.post(
url, payload,
HTTP_REFERER=reverse('admin_panel:customerAction',
kwargs={'c_id': customer.id}),
)
action.refresh_from_db()
return response, action
def test_editing_coordination_to_bazdid_enqueues_visit_created(self):
"""هماهنگی → بازدید در ویرایش ⇒ پیامک «ثبت بازدید» باید ساخته شود."""
customer = self._customer_with_phone()
self._post_action(customer, 'hamahangi')
self.assertEqual(self._visit_sms(customer).count(), 0)
response, action = self._edit_action(customer, 'bazdid')
self.assertEqual(response.status_code, 302)
self.assertEqual(action.actionType, 'bazdid')
self.assertEqual(self._visit_sms(customer).count(), 1)
# کلید یکتاسازی همان رکورد اقدام است (پس ویرایش دوباره تکرار نمی‌کند).
self.assertEqual(self._visit_sms(customer).first().dedup_key,
'visit_created:%d' % action.id)
def test_editing_to_bazdid_twice_does_not_duplicate_sms(self):
"""ویرایش دوبارهٔ همان رکورد نباید پیامک دوم بسازد (dedup روی action)."""
customer = self._customer_with_phone()
self._post_action(customer, 'hamahangi')
self._edit_action(customer, 'bazdid')
# برگشت به هماهنگی و تبدیل دوباره به بازدید: پیامک معلق لغو شده، ولی
# کلید دفتر dedup خورده است ⇒ پیامک تازه ساخته نمی‌شود.
self._edit_action(customer, 'hamahangi')
self._edit_action(customer, 'bazdid')
self.assertEqual(self._visit_sms(customer).count(), 1)
def test_editing_bazdid_to_other_type_cancels_pending_sms(self):
"""بازدید → هماهنگی در ویرایش ⇒ پیامک معلق «ثبت بازدید» لغو می‌شود."""
customer = self._customer_with_phone()
self._post_action(customer, 'bazdid')
self.assertEqual(self._pending_visit_sms(customer).count(), 1)
self._edit_action(customer, 'hamahangi')
# ردیف حذف نمی‌شود (سابقه می‌ماند) ولی دیگر ارسال‌شدنی نیست.
self.assertEqual(self._pending_visit_sms(customer).count(), 0)
self.assertEqual(
self._visit_sms(customer).first().status, SMSQueue.Status.CANCELLED,
)
def test_editing_other_fields_of_bazdid_sends_no_new_sms(self):
"""ویرایش عادی یک اقدام بازدید (بدون تغییر نوع) پیامک تازه نمی‌فرستد."""
customer = self._customer_with_phone()
self._post_action(customer, 'bazdid')
self.assertEqual(self._visit_sms(customer).count(), 1)
self._edit_action(customer, 'bazdid')
self.assertEqual(self._visit_sms(customer).count(), 1)
def test_editing_bazdid_keeps_other_actions_pending_sms(self):
"""لغو پیامک معلق، فقط رکورد ویرایش‌شده را هدف می‌گیرد (نه بقیهٔ بازدیدها)."""
from admin_panel.models import Action
from admin_panel.sms.triggers import notify_visit_scheduled
customer = self._customer_with_phone()
self._post_action(customer, 'bazdid') # رکورد ۱ + پیامک
first = Action.objects.filter(customer=customer).order_by('id').first()
second = Action.objects.create(
customer=customer, actionType='bazdid', status='finish',
actionDateTime=timezone.now(), finishDateTime=timezone.now(),
)
notify_visit_scheduled(customer, second, created_by=self.user) # پیامک رکورد ۲
self.assertEqual(self._pending_visit_sms(customer).count(), 2)
# رکورد دوم را از بازدید به هماهنگی تغییر می‌دهیم ⇒ فقط پیامک رکورد ۲ لغو شود.
url = reverse('admin_panel:customerEditAction',
kwargs={'c_id': customer.id, 'action_id': second.id})
self.client.post(url, {
'actionType': 'hamahangi', 'dateTimeAnjam': '1700000000000',
'FinishDateTime': '1700000000000', 'desc': '', 'userID': self.user.id,
'is_done': 'on',
}, HTTP_REFERER=reverse('admin_panel:customerAction',
kwargs={'c_id': customer.id}))
keys = set(self._pending_visit_sms(customer).values_list('dedup_key', flat=True))
self.assertEqual(keys, {'visit_created:%d' % first.id})
# ---------- ثبت اقدام از نوع «نصب» ----------
def _install_sms(self, customer):
"""همهٔ ردیف‌های «ثبت نصب» (هر وضعیتی)."""
return SMSQueue.objects.filter(customer=customer,
sms_type=SMSType.INSTALL_CREATED)
def _pending_install_sms(self, customer):
return self._install_sms(customer).filter(
status__in=(SMSQueue.Status.PENDING, SMSQueue.Status.APPROVED),
)
def test_nasb_action_enqueues_install_created(self):
"""اقدام «نصب» ⇒ پیامک «ثبت نصب» از قالب install_created."""
customer = self._customer_with_phone()
response = self._post_action(customer, 'nasb')
self.assertIn(response.status_code, (200, 302))
item = self._install_sms(customer)
self.assertEqual(item.count(), 1)
self.assertIn('نصب', item.first().message)
# auto_send قالب خاموش است ⇒ ردیف «در انتظار تأیید» می‌ماند، نه ارسال فوری.
self.assertEqual(item.first().status, SMSQueue.Status.PENDING)
# پیامک بازدید ساخته نمی‌شود (دو نوع مستقل).
self.assertEqual(self._pending_visit_sms(customer).count(), 0)
def test_nasb_action_sms_has_its_own_dedup_key(self):
"""کلید یکتاسازی با پیشوند نوع خودش است (با visit_created قاطی نمی‌شود)."""
from admin_panel.models import Action
customer = self._customer_with_phone()
self._post_action(customer, 'nasb')
action = Action.objects.filter(customer=customer).order_by('id').first()
self.assertEqual(self._install_sms(customer).first().dedup_key,
'install_created:%d' % action.id)
def test_non_nasb_action_has_no_install_created(self):
customer = self._customer_with_phone()
self._post_action(customer, 'bazdid')
self.assertFalse(self._install_sms(customer).exists())
def test_editing_to_nasb_enqueues_install_created(self):
"""هماهنگی → نصب در ویرایش ⇒ پیامک «ثبت نصب» ساخته می‌شود."""
customer = self._customer_with_phone()
self._post_action(customer, 'hamahangi')
self.assertEqual(self._install_sms(customer).count(), 0)
response, action = self._edit_action(customer, 'nasb')
self.assertEqual(response.status_code, 302)
self.assertEqual(action.actionType, 'nasb')
self.assertEqual(self._pending_install_sms(customer).count(), 1)
def test_editing_nasb_to_other_type_cancels_pending_install_sms(self):
"""نصب → هماهنگی در ویرایش ⇒ پیامک معلق «ثبت نصب» لغو می‌شود."""
customer = self._customer_with_phone()
self._post_action(customer, 'nasb')
self.assertEqual(self._pending_install_sms(customer).count(), 1)
self._edit_action(customer, 'hamahangi')
self.assertEqual(self._pending_install_sms(customer).count(), 0)
self.assertEqual(
self._install_sms(customer).first().status, SMSQueue.Status.CANCELLED,
)
def test_editing_bazdid_to_nasb_switches_sms_type(self):
"""بازدید → نصب ⇒ پیامک بازدید لغو و پیامک نصب ساخته می‌شود (نه هر دو)."""
customer = self._customer_with_phone()
self._post_action(customer, 'bazdid')
self.assertEqual(self._pending_visit_sms(customer).count(), 1)
self._edit_action(customer, 'nasb')
self.assertEqual(self._pending_visit_sms(customer).count(), 0)
self.assertEqual(self._pending_install_sms(customer).count(), 1)
def test_install_template_can_be_disabled_from_templates_page(self):
"""تنظیمات «ثبت نصب» از صفحهٔ قالب‌ها: غیرفعال‌کردن ⇒ پیامکی ساخته نمی‌شود."""
template = SMSTemplate.objects.get(sms_type=SMSType.INSTALL_CREATED)
response = self.client.post(
reverse('admin_panel:sms_template_edit', kwargs={'t_id': template.id}),
{'text': template.text, 'title': template.title},
)
self.assertEqual(response.status_code, 302)
template.refresh_from_db()
self.assertFalse(template.is_enabled)
customer = self._customer_with_phone()
self._post_action(customer, 'nasb')
self.assertFalse(self._install_sms(customer).exists())
def test_install_template_text_is_editable_from_templates_page(self):
"""ویرایش متن قالب از پنل ⇒ همان متن در پیامک ساخته‌شده می‌آید."""
template = SMSTemplate.objects.get(sms_type=SMSType.INSTALL_CREATED)
response = self.client.post(
reverse('admin_panel:sms_template_edit', kwargs={'t_id': template.id}),
{'text': '{name} عزیز، نصب دستگاه انجام شد ({date}).',
'title': 'ثبت نصب', 'is_enabled': 'on'},
)
self.assertEqual(response.status_code, 302)
customer = self._customer_with_phone()
self._post_action(customer, 'nasb')
message = self._install_sms(customer).first().message
self.assertIn('نصب دستگاه انجام شد', message)
self.assertIn(customer.name_family, message)
def test_install_sms_disabled_per_customer_is_not_created(self):
"""opt-out مشتری برای نوع install_created رعایت می‌شود."""
from admin_panel.models import CustomerSMSSetting
customer = self._customer_with_phone()
CustomerSMSSetting.objects.create(
customer=customer, sms_type=SMSType.INSTALL_CREATED, is_enabled=False,
)
self._post_action(customer, 'nasb')
self.assertFalse(self._install_sms(customer).exists())
def test_install_template_can_be_enabled_for_auto_send(self):
"""اگر مدیر «ارسال خودکار» را روشن کند، ردیف مستقیماً تأییدشده ساخته می‌شود."""
SMSTemplate.objects.filter(sms_type=SMSType.INSTALL_CREATED).update(
auto_send=True,
)
customer = self._customer_with_phone()
self._post_action(customer, 'nasb')
self.assertEqual(
self._install_sms(customer).first().status, SMSQueue.Status.APPROVED,
)
# ---------- تشکر از پرداخت (وصول چک، نسیه، سند دستی) ----------
def test_pass_check_enqueues_payment_thanks(self):
@@ -2372,6 +2619,85 @@ class EventTriggerTests(PanelTestCase):
)
class InstallTemplateMigrationTests(TestCase):
"""مهاجرت ۰۰۸۱: قالب‌های موجود دست‌نخورده می‌مانند، «ثبت نصب» بدون ارسال خودکار.
توابع مهاجرت مستقیم و با همان ``apps`` واقعی صدا زده می‌شوند (بدون اجرای
schema)، تا در تراکنش تست انجام و بازگردانی شود.
"""
MODULE = 'admin_panel.migrations.0081_sms_type_install_created'
FIELDS = ('sms_type', 'title', 'text', 'is_enabled', 'auto_send',
'offset_days', 'repeat_interval_days', 'repeat_interval_months')
class _Apps:
"""جایگزین ``schema_editor.apps``: نگاشت به مدل‌های واقعی پروژه."""
@staticmethod
def get_model(app_label, model_name):
from django.apps import apps as real_apps
return real_apps.get_model(app_label, model_name)
def _migration(self):
from importlib import import_module
return import_module(self.MODULE)
def _snapshot(self):
return {row['sms_type']: row
for row in SMSTemplate.objects.values(*self.FIELDS)}
def _seed(self):
self._migration().seed_install_template(self._Apps, None)
def test_seed_leaves_existing_templates_untouched(self):
SMSTemplate.objects.filter(sms_type=SMSType.INSTALL_CREATED).delete()
# مقادیر غیرپیش‌فرض؛ اگر مهاجرت هر فیلدی را بازنویسی کند دیده می‌شود.
(SMSTemplate.objects.filter(sms_type=SMSType.VISIT).update(
title='عنوان دستی', text='متن دستی {name}', is_enabled=False,
auto_send=True, offset_days=3, repeat_interval_months=6,
))
before = self._snapshot()
self._seed()
after = self._snapshot()
for sms_type, row in before.items():
self.assertEqual(after[sms_type], row, msg=sms_type)
self.assertIn(SMSType.INSTALL_CREATED, after)
def test_seeded_install_template_has_auto_send_off(self):
SMSTemplate.objects.filter(sms_type=SMSType.INSTALL_CREATED).delete()
self._seed()
template = SMSTemplate.objects.get(sms_type=SMSType.INSTALL_CREATED)
self.assertTrue(template.is_enabled, msg='باید در پنل قالب‌ها دیده شود')
self.assertFalse(template.auto_send, msg='نباید بدون تأیید مدیر ارسال شود')
self.assertEqual(template.offset_days, 0)
self.assertIsNone(template.repeat_interval_days)
self.assertIsNone(template.repeat_interval_months)
def test_seed_is_idempotent_for_an_existing_install_template(self):
"""اگر قالب از قبل وجود داشته باشد، متن/تنظیماتش بازنویسی نمی‌شود."""
template = SMSTemplate.objects.get(sms_type=SMSType.INSTALL_CREATED)
SMSTemplate.objects.filter(pk=template.pk).update(
text='متن ویرایش‌شدهٔ مدیر {name}', auto_send=True, offset_days=5,
)
before = self._snapshot()
self._seed()
self.assertEqual(self._snapshot(), before)
def test_reverse_only_removes_the_new_type(self):
before = self._snapshot()
self._migration().drop_install_template(self._Apps, None)
after = self._snapshot()
self.assertNotIn(SMSType.INSTALL_CREATED, after)
self.assertEqual(set(after), set(before) - {SMSType.INSTALL_CREATED})
for sms_type, row in after.items():
self.assertEqual(row, before[sms_type], msg=sms_type)
class CustomerSmsHistoryTests(PanelTestCase):
"""تب «سوابق پیامک» در صفحهٔ مشتری: متن دقیق، تاریخ شمسی، وضعیت تحویل، صفحه‌بندی."""
@@ -2468,6 +2794,92 @@ class CustomerSmsHistoryTests(PanelTestCase):
html = self.client.get(self._url()).content.decode('utf-8')
self.assertIn('پیامکی برای این مشتری ثبت نشده است', html)
# ---------- انتقال تنظیمات پیامک از صفحهٔ فاکتور به این صفحه ----------
def test_factor_page_no_longer_contains_sms_settings_box(self):
"""باکس «پیامک» از صفحهٔ ایجاد فاکتور حذف شده (تنظیمات به تب پیامک رفت)."""
html = self.client.get(
reverse('admin_panel:customerNew', kwargs={'c_id': self.customer.id}),
).content.decode('utf-8')
self.assertNotIn(
reverse('admin_panel:sms_send_custom', kwargs={'c_id': self.customer.id}),
html,
)
self.assertNotIn(
reverse('admin_panel:sms_customer_toggle', kwargs={'c_id': self.customer.id}),
html,
)
def test_sms_page_holds_all_settings_controls(self):
"""همهٔ کنترل‌های تنظیمات روی صفحهٔ پیامک هستند: نوع‌ها، شمارهٔ اصلی، ارسال."""
html = self.client.get(self._url()).content.decode('utf-8')
self.assertIn(
reverse('admin_panel:sms_customer_toggle', kwargs={'c_id': self.customer.id}),
html,
)
self.assertIn(
reverse('admin_panel:sms_primary_toggle', kwargs={'c_id': self.customer.id}),
html,
)
self.assertIn(
reverse('admin_panel:sms_send_custom', kwargs={'c_id': self.customer.id}),
html,
)
# یک ردیف برای هر نوع پیامک (۸ نوع) و یک کلید for each.
self.assertEqual(html.count('name="sms_type"'), len(SMSType.choices))
self.assertEqual(html.count('role="switch"'), len(SMSType.choices))
def test_settings_live_in_modals_not_in_history_flow(self):
"""تنظیمات داخل مودال است تا فهرست سوابق شلوغ نشود."""
html = self.client.get(self._url()).content.decode('utf-8')
self.assertIn('id="smsSettingsModal"', html)
self.assertIn('id="smsCustomModal"', html)
self.assertIn('js-csmsx-open', html)
def test_stats_are_computed_for_the_header(self):
self._item(status=SMSQueue.Status.SENT)
self._item(status=SMSQueue.Status.PENDING)
self._item(status=SMSQueue.Status.FAILED)
response = self.client.get(self._url())
self.assertEqual(response.context['stat_sent'], 1)
self.assertEqual(response.context['stat_pending'], 1)
self.assertEqual(response.context['stat_failed'], 1)
self.assertEqual(response.context['total_count'], 3)
def test_toggle_redirects_back_to_sms_page_with_open_settings(self):
"""پس از تغییر تنظیم، کاربر در همان صفحه/مودال می‌ماند (نه صفحهٔ مشتری)."""
response = self.client.post(
reverse('admin_panel:sms_customer_toggle', kwargs={'c_id': self.customer.id}),
{'sms_type': SMSType.WELCOME, 'is_enabled': 'false'},
)
self.assertEqual(response.status_code, 302)
self.assertTrue(response.url.startswith(self._url()), response.url)
self.assertIn('open=settings', response.url)
self.assertIn('notice=customer_sms_saved', response.url)
def test_custom_send_redirects_back_to_sms_page(self):
mark_primary(self.customer, '09120000000')
response = self.client.post(
reverse('admin_panel:sms_send_custom', kwargs={'c_id': self.customer.id}),
{'text': 'سلام {name}'},
)
self.assertEqual(response.status_code, 302)
self.assertTrue(response.url.startswith(self._url()), response.url)
self.assertIn('notice=custom_queued', response.url)
def test_primary_toggle_redirects_back_to_sms_page(self):
response = self.client.post(
reverse('admin_panel:sms_primary_toggle', kwargs={'c_id': self.customer.id}),
{'phone': '09120000000', 'is_primary': 'true'},
)
self.assertEqual(response.status_code, 302)
self.assertTrue(response.url.startswith(self._url()), response.url)
def test_open_settings_query_keeps_modal_available(self):
response = self.client.get(self._url() + '?open=settings')
self.assertEqual(response.status_code, 200)
self.assertIn('id="smsSettingsModal"', response.content.decode('utf-8'))
class SmsPartsTests(TestCase):
"""محاسبهٔ «تعداد پیامکِ محاسبه‌شده» (parts) بر اساس کدگذاری GSM-7 / UCS-2."""