mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-18 23:39:34 +00:00
Continuing Phase 3-6 implementation: - AI: conversation_intelligence.py (Arabic dialogue analysis, buying signals) - AI: message_writer.py (Arabic/English multi-channel message generation) - AI: sales_agent.py (autonomous WhatsApp qualification bot) - API: compliance.py (PDPL consent & data rights endpoints) - API: inbox.py (unified multi-channel inbox) - API: proposals.py (CPQ quote management endpoints) - API: sequences.py (multi-channel sequence management) - Services: territory_manager.py (Saudi region-based lead routing) - Seeds: contracting_template.json (Saudi contracting industry template) - Updated: router.py, consent_manager.py, data_rights.py https://claude.ai/code/session_01LsnvBa7HwF5hs99VZbgLGj
282 lines
9.2 KiB
Python
282 lines
9.2 KiB
Python
"""
|
|
Dealix Saudi Territory Manager
|
|
إدارة المناطق وتوزيع العملاء على مندوبي المبيعات تلقائياً
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy import select, func, and_
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.lead import Lead
|
|
from app.models.user import User
|
|
from app.models.deal import Deal
|
|
|
|
logger = logging.getLogger("dealix.territory")
|
|
|
|
SAUDI_REGIONS: dict[str, dict] = {
|
|
"riyadh": {
|
|
"name_ar": "الرياض",
|
|
"name_en": "Riyadh",
|
|
"cities_ar": ["الرياض", "الخرج", "الدرعية", "المجمعة"],
|
|
},
|
|
"jeddah": {
|
|
"name_ar": "جدة",
|
|
"name_en": "Jeddah",
|
|
"cities_ar": ["جدة", "رابغ", "الليث"],
|
|
},
|
|
"eastern": {
|
|
"name_ar": "المنطقة الشرقية",
|
|
"name_en": "Eastern Province",
|
|
"cities_ar": ["الدمام", "الخبر", "الظهران", "الجبيل", "الأحساء", "القطيف"],
|
|
},
|
|
"makkah": {
|
|
"name_ar": "مكة المكرمة",
|
|
"name_en": "Makkah",
|
|
"cities_ar": ["مكة المكرمة", "الطائف"],
|
|
},
|
|
"madinah": {
|
|
"name_ar": "المدينة المنورة",
|
|
"name_en": "Madinah",
|
|
"cities_ar": ["المدينة المنورة", "ينبع"],
|
|
},
|
|
"asir": {
|
|
"name_ar": "عسير",
|
|
"name_en": "Asir",
|
|
"cities_ar": ["أبها", "خميس مشيط", "النماص"],
|
|
},
|
|
"qassim": {
|
|
"name_ar": "القصيم",
|
|
"name_en": "Qassim",
|
|
"cities_ar": ["بريدة", "عنيزة", "الرس"],
|
|
},
|
|
"tabuk": {
|
|
"name_ar": "تبوك",
|
|
"name_en": "Tabuk",
|
|
"cities_ar": ["تبوك", "ضبا", "الوجه"],
|
|
},
|
|
"hail": {
|
|
"name_ar": "حائل",
|
|
"name_en": "Hail",
|
|
"cities_ar": ["حائل", "بقعاء"],
|
|
},
|
|
"jazan": {
|
|
"name_ar": "جازان",
|
|
"name_en": "Jazan",
|
|
"cities_ar": ["جازان", "صبيا", "أبو عريش"],
|
|
},
|
|
"najran": {
|
|
"name_ar": "نجران",
|
|
"name_en": "Najran",
|
|
"cities_ar": ["نجران", "شرورة"],
|
|
},
|
|
"baha": {
|
|
"name_ar": "الباحة",
|
|
"name_en": "Al Baha",
|
|
"cities_ar": ["الباحة", "بلجرشي"],
|
|
},
|
|
"jouf": {
|
|
"name_ar": "الجوف",
|
|
"name_en": "Al Jouf",
|
|
"cities_ar": ["سكاكا", "دومة الجندل"],
|
|
},
|
|
}
|
|
|
|
|
|
class TerritoryAssignment(BaseModel):
|
|
territory_key: str
|
|
rep_ids: list[str] = Field(default_factory=list)
|
|
round_robin_index: int = 0
|
|
|
|
|
|
class TerritoryStats(BaseModel):
|
|
territory_key: str
|
|
name_ar: str
|
|
name_en: str
|
|
total_leads: int = 0
|
|
total_deals: int = 0
|
|
total_value: float = 0.0
|
|
win_rate: float = 0.0
|
|
reps_count: int = 0
|
|
|
|
|
|
class TerritoryManager:
|
|
"""Territory-based lead routing and performance analytics for Saudi regions."""
|
|
|
|
def __init__(self, db: AsyncSession):
|
|
self.db = db
|
|
self._assignments: dict[str, TerritoryAssignment] = {}
|
|
|
|
async def assign_territory(
|
|
self, territory_key: str, rep_ids: list[str],
|
|
) -> dict:
|
|
"""Assign sales reps to a territory."""
|
|
if territory_key not in SAUDI_REGIONS:
|
|
raise ValueError(f"منطقة غير معروفة: {territory_key}")
|
|
|
|
self._assignments[territory_key] = TerritoryAssignment(
|
|
territory_key=territory_key,
|
|
rep_ids=rep_ids,
|
|
round_robin_index=0,
|
|
)
|
|
region = SAUDI_REGIONS[territory_key]
|
|
logger.info(
|
|
"Territory '%s' assigned to %d reps", territory_key, len(rep_ids),
|
|
)
|
|
return {
|
|
"territory": territory_key,
|
|
"name_ar": region["name_ar"],
|
|
"name_en": region["name_en"],
|
|
"reps_assigned": len(rep_ids),
|
|
"rep_ids": rep_ids,
|
|
}
|
|
|
|
async def auto_route_lead(
|
|
self,
|
|
tenant_id: str,
|
|
lead_id: str,
|
|
region_key: Optional[str] = None,
|
|
city_hint: Optional[str] = None,
|
|
) -> dict:
|
|
"""Auto-assign a lead to the next rep in the matching territory via round-robin."""
|
|
territory_key = region_key
|
|
if not territory_key and city_hint:
|
|
territory_key = self._detect_territory(city_hint)
|
|
if not territory_key:
|
|
territory_key = "riyadh"
|
|
|
|
assignment = self._assignments.get(territory_key)
|
|
if not assignment or not assignment.rep_ids:
|
|
logger.warning(
|
|
"No reps assigned to territory '%s', falling back to riyadh",
|
|
territory_key,
|
|
)
|
|
assignment = self._assignments.get("riyadh")
|
|
territory_key = "riyadh"
|
|
|
|
if not assignment or not assignment.rep_ids:
|
|
return {
|
|
"lead_id": lead_id,
|
|
"assigned_to": None,
|
|
"territory": territory_key,
|
|
"error_ar": "لا يوجد مندوبين معينين لهذه المنطقة",
|
|
}
|
|
|
|
rep_id = assignment.rep_ids[assignment.round_robin_index % len(assignment.rep_ids)]
|
|
assignment.round_robin_index += 1
|
|
|
|
import uuid
|
|
result = await self.db.execute(
|
|
select(Lead).where(
|
|
Lead.id == uuid.UUID(lead_id),
|
|
Lead.tenant_id == uuid.UUID(tenant_id),
|
|
)
|
|
)
|
|
lead = result.scalar_one_or_none()
|
|
if lead:
|
|
lead.assigned_to = uuid.UUID(rep_id)
|
|
metadata = dict(lead.extra_metadata or {})
|
|
metadata["territory"] = territory_key
|
|
metadata["auto_routed_at"] = datetime.now(timezone.utc).isoformat()
|
|
lead.extra_metadata = metadata
|
|
await self.db.flush()
|
|
|
|
region = SAUDI_REGIONS.get(territory_key, {})
|
|
logger.info("Lead %s routed to rep %s in %s", lead_id, rep_id, territory_key)
|
|
return {
|
|
"lead_id": lead_id,
|
|
"assigned_to": rep_id,
|
|
"territory": territory_key,
|
|
"territory_name_ar": region.get("name_ar", ""),
|
|
}
|
|
|
|
async def get_territory_stats(
|
|
self, tenant_id: str, territory_key: Optional[str] = None,
|
|
) -> list[TerritoryStats]:
|
|
"""Get performance analytics per territory."""
|
|
import uuid
|
|
|
|
keys = [territory_key] if territory_key else list(SAUDI_REGIONS.keys())
|
|
stats_list: list[TerritoryStats] = []
|
|
|
|
for key in keys:
|
|
region = SAUDI_REGIONS.get(key)
|
|
if not region:
|
|
continue
|
|
|
|
assignment = self._assignments.get(key)
|
|
rep_ids = assignment.rep_ids if assignment else []
|
|
|
|
if not rep_ids:
|
|
stats_list.append(TerritoryStats(
|
|
territory_key=key,
|
|
name_ar=region["name_ar"],
|
|
name_en=region["name_en"],
|
|
))
|
|
continue
|
|
|
|
rep_uuids = [uuid.UUID(r) for r in rep_ids]
|
|
tid = uuid.UUID(tenant_id)
|
|
|
|
lead_count_q = select(func.count()).where(
|
|
Lead.tenant_id == tid,
|
|
Lead.assigned_to.in_(rep_uuids),
|
|
)
|
|
total_leads = (await self.db.execute(lead_count_q)).scalar() or 0
|
|
|
|
deals_q = select(func.count(), func.coalesce(func.sum(Deal.value), 0)).where(
|
|
Deal.tenant_id == tid,
|
|
Deal.assigned_to.in_(rep_uuids),
|
|
)
|
|
row = (await self.db.execute(deals_q)).one_or_none()
|
|
total_deals = row[0] if row else 0
|
|
total_value = float(row[1]) if row else 0.0
|
|
|
|
won_q = select(func.count()).where(
|
|
Deal.tenant_id == tid,
|
|
Deal.assigned_to.in_(rep_uuids),
|
|
Deal.stage == "closed_won",
|
|
)
|
|
won_count = (await self.db.execute(won_q)).scalar() or 0
|
|
win_rate = round((won_count / total_deals) * 100, 1) if total_deals > 0 else 0.0
|
|
|
|
stats_list.append(TerritoryStats(
|
|
territory_key=key,
|
|
name_ar=region["name_ar"],
|
|
name_en=region["name_en"],
|
|
total_leads=total_leads,
|
|
total_deals=total_deals,
|
|
total_value=total_value,
|
|
win_rate=win_rate,
|
|
reps_count=len(rep_ids),
|
|
))
|
|
|
|
return stats_list
|
|
|
|
def list_regions(self) -> list[dict]:
|
|
"""Return all Saudi regions with metadata."""
|
|
return [
|
|
{
|
|
"key": key,
|
|
"name_ar": info["name_ar"],
|
|
"name_en": info["name_en"],
|
|
"cities_ar": info["cities_ar"],
|
|
"reps_assigned": len(self._assignments.get(key, TerritoryAssignment(territory_key=key)).rep_ids),
|
|
}
|
|
for key, info in SAUDI_REGIONS.items()
|
|
]
|
|
|
|
def _detect_territory(self, city_hint: str) -> Optional[str]:
|
|
"""Detect territory from a city name hint (Arabic or English)."""
|
|
hint_lower = city_hint.strip().lower()
|
|
for key, info in SAUDI_REGIONS.items():
|
|
if hint_lower in info["name_en"].lower() or hint_lower == key:
|
|
return key
|
|
for city in info["cities_ar"]:
|
|
if city in city_hint:
|
|
return key
|
|
return None
|