Coverage for src / idx_api / models / lead.py: 91%
33 statements
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-28 11:16 -0700
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-28 11:16 -0700
1"""Lead model for contact form submissions."""
3from datetime import datetime
4from typing import TYPE_CHECKING, Optional
6from sqlalchemy import DateTime, ForeignKey, String, Text
7from sqlalchemy.orm import Mapped, mapped_column, relationship
9from idx_api.models.base import Base, TimestampMixin
11if TYPE_CHECKING:
12 from idx_api.models.agent import Agent
13 from idx_api.models.brokerage import Brokerage
16class Lead(Base, TimestampMixin):
17 """Contact form lead from potential clients.
19 Unlike TourRequest (which is property-specific), Lead captures
20 general inquiries from the contact form - buying interest,
21 selling interest, valuations, general questions, etc.
22 """
24 __tablename__ = "leads"
26 id: Mapped[int] = mapped_column(primary_key=True)
28 # Contact information
29 first_name: Mapped[str] = mapped_column(String(100), nullable=False)
30 last_name: Mapped[str] = mapped_column(String(100), nullable=False)
31 email: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
32 phone: Mapped[Optional[str]] = mapped_column(String(20))
34 # Inquiry details
35 subject: Mapped[str] = mapped_column(
36 String(50), nullable=False, index=True
37 ) # buying, selling, valuation, general, other
38 message: Mapped[str] = mapped_column(Text, nullable=False)
40 # Source tracking
41 source_domain: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
42 source_page: Mapped[Optional[str]] = mapped_column(String(255)) # Which page they submitted from
43 utm_source: Mapped[Optional[str]] = mapped_column(String(100))
44 utm_medium: Mapped[Optional[str]] = mapped_column(String(100))
45 utm_campaign: Mapped[Optional[str]] = mapped_column(String(100))
47 # Assignment (auto-assigned based on domain -> brokerage)
48 brokerage_id: Mapped[int] = mapped_column(ForeignKey("brokerages.id"), nullable=False, index=True)
49 assigned_agent_id: Mapped[Optional[int]] = mapped_column(ForeignKey("agents.id"), index=True)
51 # Status tracking
52 status: Mapped[str] = mapped_column(
53 String(20), nullable=False, default="new", index=True
54 ) # new, contacted, qualified, converted, lost
56 # Follow-up tracking
57 last_contacted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
58 notes: Mapped[Optional[str]] = mapped_column(Text)
60 # Relationships
61 brokerage: Mapped["Brokerage"] = relationship(
62 back_populates="leads", foreign_keys=[brokerage_id]
63 )
64 assigned_agent: Mapped[Optional["Agent"]] = relationship(
65 back_populates="leads", foreign_keys=[assigned_agent_id]
66 )
68 @property
69 def full_name(self) -> str:
70 """Get lead's full name."""
71 return f"{self.first_name} {self.last_name}"
73 @property
74 def subject_display(self) -> str:
75 """Get human-readable subject."""
76 subjects = {
77 "buying": "Looking to Buy",
78 "selling": "Want to Sell",
79 "valuation": "Property Valuation",
80 "general": "General Inquiry",
81 "other": "Other",
82 }
83 return subjects.get(self.subject, self.subject)
85 def __repr__(self) -> str:
86 return f"<Lead(id={self.id}, email='{self.email}', subject='{self.subject}', status='{self.status}')>"