Files
orion/app/modules/prospecting/models/prospect.py
Samir Boulahtit 972ee1e5d0 feat(prospecting): add ProspectSecurityAudit model (Phase 1 foundation)
- New model: ProspectSecurityAudit with score, grade, findings_json,
  severity counts, has_https, has_valid_ssl, missing_headers, exposed
  files, technologies, scan_error
- Add last_security_audit_at timestamp to Prospect model
- Add security_audit 1:1 relationship on Prospect

Part of Phase 1: Security Audit in Enrichment Pipeline. Service,
constants, migration, endpoints, and frontend to follow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 22:23:38 +02:00

85 lines
3.2 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)
last_security_audit_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")
security_audit = relationship("ProspectSecurityAudit", 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}"