sms
This commit is contained in:
1 parent
250cdbab7f
commit
5c6069c3ef
87 files changed
+29209
-3697
No files matched your search
@@ -0,0 +1,163 @@
|
||||
"""یافتن شمارههای یک مشتری از همهٔ روشهای ذخیرهسازی.
|
||||
|
||||
شمارهها در این پروژه به چند شکل ذخیره میشوند:
|
||||
|
||||
* رکوردهای مدل ``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 []
|
||||
Reference in new issue
Block a user