import logging from typing import Any logger = logging.getLogger(__name__) class LeadHandler: """ Handles all Leads CRUD and scoring logic for MarketingService. Physically decoupled for Phase 4.6.47. """ def __init__(self, supabase_client: Any): self.supabase_client = supabase_client def execute_query(self, query_func, error_msg: str) -> tuple[bool, Any]: """Helper to maintain parity with BaseRepository-like execution.""" try: res = query_func() return True, res except Exception as e: logger.error(f"{error_msg}: {e}") return False, {"error": str(e)} async def list_leads(self, user_id: str | None = None, role: str | None = None) -> list[dict]: def _query(): q = self.supabase_client.table("leads").select("*") if role == "sales" and user_id: q = q.or_(f"assigned_sales_id.eq.{user_id},assigned_sales_id.is.null") return q.order("created_at", desc=True).execute() success, res = self.execute_query(_query, "Failed to fetch leads") return res.data if success and hasattr(res, "data") and res.data is not None else [] async def create_lead(self, lead_data: dict, creator_id: str | None = None) -> tuple[bool, dict]: if "created_from_user_id" in lead_data: del lead_data["created_from_user_id"] if not lead_data.get("enrichment_score") or lead_data.get("enrichment_score") == 0: lead_data["enrichment_score"] = await self.calculate_lead_score(lead_data.get("job_title")) source_url = lead_data.get("source_job_url") if source_url: def _check_existing(): return self.supabase_client.table("leads").select("id").eq("source_job_url", source_url).execute() _, existing = self.execute_query(_check_existing, "Check existing lead") if hasattr(existing, "data") and existing.data: if lead_data.get("pitch_content"): self.supabase_client.table("leads").update({"pitch_content": lead_data["pitch_content"]}).eq( "id", existing.data[0]["id"] ).execute() return True, {"lead": existing.data[0]} def _insert(): return self.supabase_client.table("leads").insert(lead_data).execute() success, res = self.execute_query(_insert, "Failed to create lead") if success and hasattr(res, "data") and res.data: return True, {"lead": res.data[0]} return False, res async def update_lead(self, lead_id: str, update_data: dict) -> tuple[bool, dict]: def _query(): return self.supabase_client.table("leads").update(update_data).eq("id", lead_id).execute() success, res = self.execute_query(_query, f"Failed to update lead {lead_id}") if success and hasattr(res, "data") and res.data: lead_data = res.data[0] if lead_data.get("status") == "LOST": try: from ..librarian_service import LibrarianService librarian = LibrarianService() # The lead data will contain company_name and job_title. We extract lost_reason. reason = update_data.get("lost_reason", "No reason provided") company = lead_data.get("company_name", "Unknown Company") job = lead_data.get("job_title", "Unknown Job") import asyncio # Run archiving as a background task to prevent blocking the UI asyncio.create_task( librarian.archive_failure_case( content=f"Lead marked as LOST. Need: {lead_data.get('identified_need')}", reason=reason, company=company, job_title=job, metadata={"lead_id": lead_id}, ) ) except Exception as e: logger.error(f"Failed to archive failure case for lead {lead_id}: {e}") return True, {"lead": lead_data} return False, res async def promote_to_vendor( self, lead_id: str, vendor_name: str, email: str | None, notes: str | None, owner_id: str ) -> tuple[bool, dict]: try: vendor_data = { "name": vendor_name, "contact_email": email, "description": notes or "Promoted", "status": "active", "owner_id": owner_id, } vendor_res = self.supabase_client.table("vendors").insert(vendor_data).execute() new_vendor_id = vendor_res.data[0]["id"] self.supabase_client.table("leads").update({"status": "converted"}).eq("id", lead_id).execute() self.supabase_client.table("visit_logs").update({"customer_id": new_vendor_id}).eq( "lead_id", lead_id ).execute() return True, {"vendor": vendor_res.data[0]} except Exception as e: return False, {"error": str(e)} async def calculate_lead_score(self, job_title: str | None) -> int: try: from ..settings_service import SettingsService settings = SettingsService(self.supabase_client) w_strat = int(settings.get_setting("SCORE_STRATEGIC") or "95") w_tech = int(settings.get_setting("SCORE_TECHNICAL") or "85") w_ops = int(settings.get_setting("SCORE_OPERATIONAL") or "70") w_base = int(settings.get_setting("SCORE_BASE") or "40") title = str(job_title or "").upper() if any(kw in title for kw in ["DIRECTOR", "VP", "HEAD", "CHIEF", "ARCHITECT", "FOUNDER"]): return w_strat if any(kw in title for kw in ["AI", "ML", "MACHINE LEARNING", "DATA", "PYTHON", "ENGINEER"]): return w_tech if any(kw in title for kw in ["MANAGER", "LEAD", "SENIOR", "TEAM LEAD"]): return w_ops return w_base except Exception as e: logger.warning(f"Lead Scoring Failed: {e}. Falling back to 40.") return 40 async def reset_leads(self) -> bool: try: self.supabase_client.table("leads").delete().neq("id", "00000000-0000-0000-0000-000000000000").execute() return True except Exception as e: logger.error(f"Failed to reset leads: {e}") return False