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

+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."""