improve entezar bazdid

This commit is contained in:
javad committed 2026-10-20 19:02:34 +03:30
1 parent 1fd27d0445
commit 2aee85593f
18 files changed
+10271 -7558

No files matched your search

+83 -14
View File
@@ -1,4 +1,5 @@
"""تست‌های قابلیت پیامک: نرمال‌سازی، idempotency، opt-out، worker و تریگرها."""
import datetime
import json
import re
from datetime import timedelta
@@ -16,9 +17,10 @@ from admin_panel.sms.enqueue import enqueue_sms
from admin_panel.sms.phones import customer_phone_entries
from admin_panel.sms.recipients import resolve_recipients
from admin_panel.sms.rules import is_sms_enabled
from admin_panel.sms.scanner import run_all
from admin_panel.sms.scanner import run_all, scan_visit
from admin_panel.sms.timing import apply_timing, current_timing_value, timing_for
from admin_panel.sms.triggers import notify_welcome
from admin_panel.visit_policy import visit_period
from integrations.sms.base import SMSProvider, SMSResult
from integrations.sms.normalize import normalize_mobile, to_local
@@ -427,7 +429,11 @@ class ScannerTests(TestCase):
self.assertEqual(SMSQueue.objects.filter(customer=old).count(), 0)
def test_deleted_visit_reminder_rearms_next_period_only(self):
"""حذفِ یادآوری بازدید فقط همان دوره را کنسل می‌کند؛ دورهٔ بعد دوباره ساخته می‌شود."""
"""حذفِ یادآوری بازدید فقط همان دوره را کنسل می‌کند؛ دورهٔ بعد دوباره ساخته می‌شود.
نکته: اسکنر با ``now`` تزریقی اجرا می‌شود تا این تست به ساعت واقعی سیستم وابسته نباشد
(قبلاً با ``run_all`` تمام اسکن‌ها اجرا می‌شدند و ماتریس دوره به تاریخ واقعی گره می‌خورد).
"""
customer = make_customer('مشتری بازدید دوره‌ای')
PhoneNumber.objects.create(customer=customer, phone_number='09120000007')
Customer.objects.filter(pk=customer.pk).update(
@@ -436,31 +442,88 @@ class ScannerTests(TestCase):
template = SMSTemplate.objects.get(sms_type=SMSType.VISIT)
apply_timing(template, 9)
template.save()
period = timedelta(weeks=int(9 * 4.3))
days_to_next = period.days - (300 % period.days) # تا شروع دورهٔ بعد
self.assertEqual(visit_period().months, 9)
period = visit_period()
install = timezone.localdate() - timedelta(days=300)
today = timezone.localdate()
# روز ورود به چرخهٔ بعد = موعد تقویمی بعدی (install + 9m، +18m، ...)
from dateutil.relativedelta import relativedelta
from admin_panel.visit_policy import visit_cycle_index
next_due = install + period
while next_due <= today:
next_due = next_due + period
days_to_next = (next_due - today).days
self.assertGreater(
visit_cycle_index(install, next_due), visit_cycle_index(install, today),
msg='شمارهٔ چرخه باید در موعد تقویمی بعدی افزایش یابد',
)
def keys():
return list(
SMSQueue.objects.filter(customer=customer).values_list('dedup_key', flat=True)
)
run_all(today)
def scan(day):
"""اسکن در «روز» داده‌شده؛ مبنای زمانی هم همان روز است (بدون وابستگی به ساعت واقعی)."""
moment = timezone.make_aware(
datetime.datetime.combine(day, datetime.time(12, 0)),
timezone.get_current_timezone(),
)
return scan_visit(day, now=moment)
# کنترل جریان: پنجرهٔ یادآوری با «امروز» واقعی (زمان پنجرهٔ اسکن) سنجیده می‌شود،
# پس خط شروع را به همان اندازه عقب می‌بریم تا این تست به تاریخ اجرا وابسته نباشد.
config = SMSProviderConfig.load()
config.scan_since = timezone.now() - timedelta(days=600)
config.allow_backlog = True
config.backlog_max = 50
config.save()
# پیش‌بردن «اکنون» به روزهای آینده به تغییر موقت ساعت ارجاعی نیاز دارد
# (بازه‌های ماهانه تقویمی‌اند و با جابه‌جایی خط شروع شبیه‌سازی نمی‌شوند).
import admin_panel.sms.scanner as scanner_module
real_timezone_now = scanner_module.timezone.now
def scan(day):
"""اسکن در «روز» داده‌شده؛ مبنای زمانی هم همان روز است (بدون وابستگی به ساعت واقعی)."""
moment = timezone.make_aware(
datetime.datetime.combine(day, datetime.time(12, 0)),
timezone.get_current_timezone(),
)
scanner_module.timezone.now = lambda: moment
try:
return scan_visit(day, now=moment)
finally:
scanner_module.timezone.now = real_timezone_now
scan(today)
first = keys()
self.assertEqual(len(first), 1)
# مدیر مورد را حذف می‌کند ⇒ در همان دوره دوباره ساخته نمی‌شود.
SMSQueue.objects.filter(customer=customer).delete()
run_all(today)
scan(today)
self.assertEqual(keys(), [])
# حتی نزدیک پایان دوره هم برنمی‌گردد.
run_all(today + timedelta(days=days_to_next - 1))
scan(today + timedelta(days=days_to_next - 1))
self.assertEqual(keys(), [])
# در دورهٔ بعد کلیدِ جدید ساخته می‌شود (دوباره ارسال می‌شود، نه خاموشِ دائمی).
run_all(today + timedelta(days=days_to_next))
self.assertEqual(len(keys()), 1)
self.assertNotEqual(keys(), first)
# در دورهٔ بعد کلیدِ تازه ساخته می‌شود (یادآوری برای همیشه خاموش نمی‌ماند).
# نکته: شماره‌گذاری چرخه از «لنگر ثابت» (تاریخ نصب) محاسبه می‌شود، پس اولین
# چرخهٔ کاملِ بعد از حذف، چرخهٔ شمارهٔ ۲ است؛ حلقه تا رسیدن به کلید تازه
# ادامه می‌دهد تا تست به محاسبهٔ دقیق تقویمی گره نخورد.
second = None
for step in range(1, 5):
scan(today + timedelta(days=days_to_next * step))
current = keys()
if current:
second = current
break
self.assertIsNotNone(second, msg='یادآوری دورهٔ بعد باید دوباره ساخته شود')
self.assertNotEqual(second, first)
def test_disabled_template_is_skipped(self):
today = timezone.localdate()
@@ -2190,13 +2253,19 @@ class EventTriggerTests(PanelTestCase):
# ---------- ثبت اقدام از نوع بازدید ----------
def _post_action(self, customer, action_type):
def _post_action(self, customer, action_type, *, is_done=True):
url = reverse('admin_panel:customerNewAction', kwargs={'c_id': customer.id})
return self.client.post(url, {
payload = {
'actionType': action_type,
'dateTimeAnjam': '1700000000000',
'desc': 'توضیح', 'userID': self.user.id,
})
}
if is_done:
# اعتبارسنجی HTTP: بازدید انجام‌شده باید تاریخ اتمام داشته باشد؛
# فیلد خالی در سرور با زمان ثبت پر می‌شود.
payload['is_done'] = 'on'
payload['FinishDateTime'] = '1700000000000'
return self.client.post(url, payload)
def test_bazdid_action_enqueues_visit_created(self):
customer = self._customer_with_phone()