feat(prospecting): add complete prospecting module for lead discovery and scoring
Some checks failed
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>
This commit is contained in:
82
app/modules/prospecting/models/prospect.py
Normal file
82
app/modules/prospecting/models/prospect.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# 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}"
|
||||
Reference in New Issue
Block a user