"""یافتن شماره‌های یک مشتری از همهٔ روش‌های ذخیره‌سازی. شماره‌ها در این پروژه به چند شکل ذخیره می‌شوند: * رکوردهای مدل ``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"" 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 []