Some checks failed
Migrates scanning pipeline from marketing-.lu-domains app into Orion module. Supports digital (domain scan) and offline (manual capture) lead channels with enrichment, scoring, campaign management, and interaction tracking. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
83 lines
3.0 KiB
Python
83 lines
3.0 KiB
Python
# app/modules/prospecting/models/prospect.py
|
|
"""
|
|
Prospect model - core entity for lead discovery.
|
|
|
|
Supports two channels:
|
|
- digital: discovered via domain scanning (.lu domains)
|
|
- offline: manually captured (street encounters, networking)
|
|
"""
|
|
|
|
import enum
|
|
|
|
from sqlalchemy import Boolean, Column, DateTime, Enum, Float, Integer, String, Text
|
|
from sqlalchemy.orm import relationship
|
|
|
|
from app.core.database import Base
|
|
from models.database.base import TimestampMixin
|
|
|
|
|
|
class ProspectChannel(str, enum.Enum):
|
|
DIGITAL = "digital"
|
|
OFFLINE = "offline"
|
|
|
|
|
|
class ProspectStatus(str, enum.Enum):
|
|
PENDING = "pending"
|
|
ACTIVE = "active"
|
|
INACTIVE = "inactive"
|
|
PARKED = "parked"
|
|
ERROR = "error"
|
|
CONTACTED = "contacted"
|
|
CONVERTED = "converted"
|
|
|
|
|
|
class Prospect(Base, TimestampMixin):
|
|
"""Represents a business prospect (potential client)."""
|
|
|
|
__tablename__ = "prospects"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
channel = Column(Enum(ProspectChannel), nullable=False, default=ProspectChannel.DIGITAL)
|
|
business_name = Column(String(255), nullable=True)
|
|
domain_name = Column(String(255), nullable=True, unique=True, index=True)
|
|
status = Column(Enum(ProspectStatus), nullable=False, default=ProspectStatus.PENDING)
|
|
source = Column(String(100), nullable=True)
|
|
|
|
# Website status (digital channel)
|
|
has_website = Column(Boolean, nullable=True)
|
|
uses_https = Column(Boolean, nullable=True)
|
|
http_status_code = Column(Integer, nullable=True)
|
|
redirect_url = Column(Text, nullable=True)
|
|
|
|
# Location (offline channel)
|
|
address = Column(String(500), nullable=True)
|
|
city = Column(String(100), nullable=True)
|
|
postal_code = Column(String(10), nullable=True)
|
|
country = Column(String(2), nullable=False, default="LU")
|
|
|
|
# Notes and metadata
|
|
notes = Column(Text, nullable=True)
|
|
tags = Column(Text, nullable=True) # JSON string of tags
|
|
|
|
# Capture info
|
|
captured_by_user_id = Column(Integer, nullable=True)
|
|
location_lat = Column(Float, nullable=True)
|
|
location_lng = Column(Float, nullable=True)
|
|
|
|
# Scan timestamps
|
|
last_http_check_at = Column(DateTime, nullable=True)
|
|
last_tech_scan_at = Column(DateTime, nullable=True)
|
|
last_perf_scan_at = Column(DateTime, nullable=True)
|
|
last_contact_scrape_at = Column(DateTime, nullable=True)
|
|
|
|
# Relationships
|
|
tech_profile = relationship("ProspectTechProfile", back_populates="prospect", uselist=False, cascade="all, delete-orphan")
|
|
performance_profile = relationship("ProspectPerformanceProfile", back_populates="prospect", uselist=False, cascade="all, delete-orphan")
|
|
score = relationship("ProspectScore", back_populates="prospect", uselist=False, cascade="all, delete-orphan")
|
|
contacts = relationship("ProspectContact", back_populates="prospect", cascade="all, delete-orphan")
|
|
interactions = relationship("ProspectInteraction", back_populates="prospect", cascade="all, delete-orphan")
|
|
|
|
@property
|
|
def display_name(self) -> str:
|
|
return self.business_name or self.domain_name or f"Prospect #{self.id}"
|