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

1"""Lead model for contact form submissions.""" 

2 

3from datetime import datetime 

4from typing import TYPE_CHECKING, Optional 

5 

6from sqlalchemy import DateTime, ForeignKey, String, Text 

7from sqlalchemy.orm import Mapped, mapped_column, relationship 

8 

9from idx_api.models.base import Base, TimestampMixin 

10 

11if TYPE_CHECKING: 

12 from idx_api.models.agent import Agent 

13 from idx_api.models.brokerage import Brokerage 

14 

15 

16class Lead(Base, TimestampMixin): 

17 """Contact form lead from potential clients. 

18 

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 """ 

23 

24 __tablename__ = "leads" 

25 

26 id: Mapped[int] = mapped_column(primary_key=True) 

27 

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)) 

33 

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) 

39 

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)) 

46 

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) 

50 

51 # Status tracking 

52 status: Mapped[str] = mapped_column( 

53 String(20), nullable=False, default="new", index=True 

54 ) # new, contacted, qualified, converted, lost 

55 

56 # Follow-up tracking 

57 last_contacted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) 

58 notes: Mapped[Optional[str]] = mapped_column(Text) 

59 

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 ) 

67 

68 @property 

69 def full_name(self) -> str: 

70 """Get lead's full name.""" 

71 return f"{self.first_name} {self.last_name}" 

72 

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) 

84 

85 def __repr__(self) -> str: 

86 return f"<Lead(id={self.id}, email='{self.email}', subject='{self.subject}', status='{self.status}')>"