Files
khatibi/admin_panel/sms/phones.py
T
2026-10-10 14:21:04 +03:30

164 lines
6.8 KiB
Python

"""یافتن شماره‌های یک مشتری از همهٔ روش‌های ذخیره‌سازی.
شماره‌ها در این پروژه به چند شکل ذخیره می‌شوند:
* رکوردهای مدل ``PhoneNumber`` (روش جدید؛ عنوان + شماره)
* فیلدهای قدیمی ``Customer.phone1`` / ``phone2`` / ``phone3``
* بدون صفر ابتدایی (``9133722815`` به‌جای ``09133722815``) و بعضاً مقدارهای
بی‌معنی مثل ``'0'`` یا شمارهٔ ثابت.
این ماژول تنها مرجع خواندن شماره‌هاست: هر جای دیگر (پنل، اسکنر، worker) باید از
همین توابع استفاده کند تا رفتار یکسان باشد.
«شمارهٔ اصلی» به‌صورت **مقدارِ خودِ شماره** (نرمال‌شده) در ``Customer.sms_primary_phones``
ذخیره می‌شود، نه با کلید رکورد. دلیل: دادهٔ مشتریان دائماً تغییر می‌کند (افزودن، ویرایش،
حذف) و کلید رکورد در بازسازی‌ها از بین می‌رود؛ ولی شماره یک مقدار پایدار است. اگر شماره‌ای
از مشتری حذف یا ویرایش شود، انتخاب اصلی خودبه‌خود بی‌اثر می‌شود و سیستم به شمارهٔ اول
برمی‌گردد.
"""
from integrations.sms.normalize import normalize_mobile
class PhoneEntry:
"""یک شمارهٔ مشتری، مستقل از اینکه کجا ذخیره شده است."""
__slots__ = ("raw", "display", "normalized", "title", "source", "is_primary")
def __init__(self, raw, title, source, is_primary=False):
self.raw = raw
self.title = title
self.source = source
self.normalized = normalize_mobile(raw)
self.display = to_display(raw)
self.is_primary = is_primary
@property
def is_mobile(self):
return bool(self.normalized)
def as_dict(self):
return {
"raw": self.raw,
"display": self.display,
"normalized": self.normalized,
"title": self.title,
"source": self.source,
"is_primary": self.is_primary,
"is_mobile": self.is_mobile,
}
def __repr__(self): # pragma: no cover - کمک‌کنندهٔ دیباگ
return f"<PhoneEntry {self.display!r} primary={self.is_primary} source={self.source}>"
def to_display(raw):
"""شماره را برای نمایش با صفر ابتدایی برمی‌گرداند (بدون دست‌بردن در مقدار ذخیره‌شده)."""
text = "" if raw is None else str(raw).strip()
if not text:
return ""
normalized = normalize_mobile(text)
if normalized:
return "0" + normalized[3:] # +989xxxxxxxxx → 09xxxxxxxxx
return text
def _iter_sources(customer):
"""(title, raw, source) از همهٔ منابع، بدون تکرار — ابتدا رکوردها، سپس فیلدهای قدیمی."""
from ..models import PhoneNumber
seen = set()
rows = PhoneNumber.objects.filter(customer=customer).order_by("id")
for row in rows:
raw = (row.phone_number or "").strip()
if not raw:
continue
key = normalize_mobile(raw) or raw
if key in seen:
continue
seen.add(key)
yield (row.title or "تلفن", raw, "phone_number")
legacy = (
("تلفن 1", getattr(customer, "phone1", "")),
("تلفن 2", getattr(customer, "phone2", "")),
("تلفن 3", getattr(customer, "phone3", "")),
)
for title, raw in legacy:
raw = (raw or "").strip()
if not raw:
continue
key = normalize_mobile(raw) or raw
if key in seen:
continue
seen.add(key)
yield (title, raw, "legacy")
def stored_primary_normalized(customer):
"""مجموعهٔ شماره‌های نرمال‌شده‌ای که مدیر به‌عنوان «اصلی» انتخاب کرده است."""
raw = (getattr(customer, "sms_primary_phones", "") or "").strip()
if not raw:
return set()
return {part.strip() for part in raw.split(",") if part.strip()}
def normalize_primary_values(values):
"""فهرست شماره‌های خام → رشتهٔ نرمال‌شدهٔ یکتا برای ذخیره در ``sms_primary_phones``.
شماره‌های نامعتبر (ثابت، ناقص، ``'0'``) نادیده گرفته می‌شوند؛ پس علامت‌زدن یک شمارهٔ
غیرموبایل به‌عنوان «اصلی» بی‌اثر می‌ماند و سیستم به شمارهٔ اولِ معتبر برمی‌گردد.
"""
unique = []
for raw in values:
number = normalize_mobile(raw)
if number and number not in unique:
unique.append(number)
return ",".join(unique)
def save_primary_normalized(customer, numbers, *, update_fields=("sms_primary_phones",)):
"""ذخیرهٔ فهرست شماره‌های اصلی (نرمال‌شده، بدون تکرار، به ترتیب ورودی)."""
customer.sms_primary_phones = normalize_primary_values(numbers)
customer.save(update_fields=list(update_fields))
return stored_primary_normalized(customer)
def customer_phone_entries(customer):
"""همهٔ شماره‌های مشتری از هر منبعی که ذخیره شده باشند (بدون تکرار)."""
if customer is None:
return []
primary = stored_primary_normalized(customer)
return [
PhoneEntry(raw, title, source, is_primary=(entry_norm in primary) if entry_norm else False)
for title, raw, source in _iter_sources(customer)
for entry_norm in [normalize_mobile(raw)]
]
def usable_entries(customer):
"""فقط شماره‌هایی که موبایل معتبرند و واقعاً قابل ارسال پیامک هستند."""
return [entry for entry in customer_phone_entries(customer) if entry.is_mobile]
def primary_phones(customer):
"""شماره‌های خام گیرندهٔ پیامک: انتخاب‌شده‌های مدیر، وگرنه شمارهٔ اولِ قابل استفاده."""
entries = usable_entries(customer)
if not entries:
return []
chosen = [entry for entry in entries if entry.is_primary]
if chosen:
return [entry.raw for entry in chosen]
return [entries[0].raw]
def resolved_recipients(customer):
"""گیرندگان نهاییِ نرمال‌شده (``+989xxxxxxxxx``) و یکتا."""
seen = []
for entry in usable_entries(customer):
if entry.is_primary and entry.normalized not in seen:
seen.append(entry.normalized)
if seen:
return seen
entries = usable_entries(customer)
return [entries[0].normalized] if entries else []