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:
66
app/modules/prospecting/schemas/__init__.py
Normal file
66
app/modules/prospecting/schemas/__init__.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# app/modules/prospecting/schemas/__init__.py
|
||||
from app.modules.prospecting.schemas.campaign import (
|
||||
CampaignPreviewRequest,
|
||||
CampaignPreviewResponse,
|
||||
CampaignSendCreate,
|
||||
CampaignSendListResponse,
|
||||
CampaignSendResponse,
|
||||
CampaignTemplateCreate,
|
||||
CampaignTemplateResponse,
|
||||
CampaignTemplateUpdate,
|
||||
)
|
||||
from app.modules.prospecting.schemas.contact import (
|
||||
ProspectContactCreate,
|
||||
ProspectContactResponse,
|
||||
)
|
||||
from app.modules.prospecting.schemas.interaction import (
|
||||
InteractionCreate,
|
||||
InteractionListResponse,
|
||||
InteractionResponse,
|
||||
)
|
||||
from app.modules.prospecting.schemas.performance_profile import (
|
||||
PerformanceProfileResponse,
|
||||
)
|
||||
from app.modules.prospecting.schemas.prospect import (
|
||||
ProspectCreate,
|
||||
ProspectDeleteResponse,
|
||||
ProspectDetailResponse,
|
||||
ProspectImportResponse,
|
||||
ProspectListResponse,
|
||||
ProspectResponse,
|
||||
ProspectUpdate,
|
||||
)
|
||||
from app.modules.prospecting.schemas.scan_job import (
|
||||
ScanJobListResponse,
|
||||
ScanJobResponse,
|
||||
)
|
||||
from app.modules.prospecting.schemas.score import ProspectScoreResponse
|
||||
from app.modules.prospecting.schemas.tech_profile import TechProfileResponse
|
||||
|
||||
__all__ = [
|
||||
"ProspectCreate",
|
||||
"ProspectUpdate",
|
||||
"ProspectResponse",
|
||||
"ProspectDetailResponse",
|
||||
"ProspectListResponse",
|
||||
"ProspectDeleteResponse",
|
||||
"ProspectImportResponse",
|
||||
"TechProfileResponse",
|
||||
"PerformanceProfileResponse",
|
||||
"ProspectScoreResponse",
|
||||
"ProspectContactCreate",
|
||||
"ProspectContactResponse",
|
||||
"ScanJobResponse",
|
||||
"ScanJobListResponse",
|
||||
"InteractionCreate",
|
||||
"InteractionResponse",
|
||||
"InteractionListResponse",
|
||||
"CampaignTemplateCreate",
|
||||
"CampaignTemplateUpdate",
|
||||
"CampaignTemplateResponse",
|
||||
"CampaignSendCreate",
|
||||
"CampaignPreviewRequest",
|
||||
"CampaignPreviewResponse",
|
||||
"CampaignSendResponse",
|
||||
"CampaignSendListResponse",
|
||||
]
|
||||
103
app/modules/prospecting/schemas/campaign.py
Normal file
103
app/modules/prospecting/schemas/campaign.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# app/modules/prospecting/schemas/campaign.py
|
||||
"""Pydantic schemas for campaign management."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class CampaignTemplateCreate(BaseModel):
|
||||
"""Schema for creating a campaign template."""
|
||||
|
||||
name: str = Field(..., max_length=255)
|
||||
lead_type: str = Field(
|
||||
...,
|
||||
pattern="^(no_website|bad_website|gmail_only|security_issues|performance_issues|outdated_cms|general)$",
|
||||
)
|
||||
channel: str = Field("email", pattern="^(email|letter|phone_script)$")
|
||||
language: str = Field("fr", max_length=5)
|
||||
subject_template: str | None = Field(None, max_length=500)
|
||||
body_template: str = Field(...)
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class CampaignTemplateUpdate(BaseModel):
|
||||
"""Schema for updating a campaign template."""
|
||||
|
||||
name: str | None = Field(None, max_length=255)
|
||||
lead_type: str | None = None
|
||||
channel: str | None = None
|
||||
language: str | None = Field(None, max_length=5)
|
||||
subject_template: str | None = None
|
||||
body_template: str | None = None
|
||||
is_active: bool | None = None
|
||||
|
||||
|
||||
class CampaignTemplateResponse(BaseModel):
|
||||
"""Schema for campaign template response."""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
lead_type: str
|
||||
channel: str
|
||||
language: str
|
||||
subject_template: str | None = None
|
||||
body_template: str
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class CampaignSendCreate(BaseModel):
|
||||
"""Schema for sending a campaign."""
|
||||
|
||||
template_id: int
|
||||
prospect_ids: list[int] = Field(..., min_length=1)
|
||||
|
||||
|
||||
class CampaignPreviewRequest(BaseModel):
|
||||
"""Schema for previewing a rendered campaign."""
|
||||
|
||||
template_id: int
|
||||
prospect_id: int
|
||||
|
||||
|
||||
class CampaignPreviewResponse(BaseModel):
|
||||
"""Schema for campaign preview response."""
|
||||
|
||||
subject: str | None = None
|
||||
body: str
|
||||
|
||||
|
||||
class CampaignSendResponse(BaseModel):
|
||||
"""Schema for campaign send record response."""
|
||||
|
||||
id: int
|
||||
template_id: int | None = None
|
||||
prospect_id: int
|
||||
channel: str
|
||||
rendered_subject: str | None = None
|
||||
rendered_body: str | None = None
|
||||
status: str
|
||||
sent_at: datetime | None = None
|
||||
sent_by_user_id: int | None = None
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class CampaignSendListResponse(BaseModel):
|
||||
"""List of campaign sends."""
|
||||
|
||||
items: list[CampaignSendResponse]
|
||||
total: int
|
||||
|
||||
|
||||
class CampaignTemplateDeleteResponse(BaseModel):
|
||||
"""Response for template deletion."""
|
||||
|
||||
message: str
|
||||
34
app/modules/prospecting/schemas/contact.py
Normal file
34
app/modules/prospecting/schemas/contact.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# app/modules/prospecting/schemas/contact.py
|
||||
"""Pydantic schemas for prospect contacts."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ProspectContactCreate(BaseModel):
|
||||
"""Schema for creating a contact."""
|
||||
|
||||
contact_type: str = Field(..., pattern="^(email|phone|address|social|form)$")
|
||||
value: str = Field(..., max_length=500)
|
||||
label: str | None = Field(None, max_length=100)
|
||||
is_primary: bool = False
|
||||
|
||||
|
||||
class ProspectContactResponse(BaseModel):
|
||||
"""Schema for contact response."""
|
||||
|
||||
id: int
|
||||
prospect_id: int
|
||||
contact_type: str
|
||||
value: str
|
||||
label: str | None = None
|
||||
source_url: str | None = None
|
||||
source_element: str | None = None
|
||||
is_validated: bool = False
|
||||
is_primary: bool = False
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
69
app/modules/prospecting/schemas/enrichment.py
Normal file
69
app/modules/prospecting/schemas/enrichment.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# app/modules/prospecting/schemas/enrichment.py
|
||||
"""Pydantic response schemas for enrichment/scanning endpoints."""
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class HttpCheckResult(BaseModel):
|
||||
"""Response from a single HTTP check."""
|
||||
|
||||
has_website: bool
|
||||
uses_https: bool | None = None
|
||||
http_status_code: int | None = None
|
||||
redirect_url: str | None = None
|
||||
|
||||
|
||||
class HttpCheckBatchItem(BaseModel):
|
||||
"""Single item in a batch HTTP check response."""
|
||||
|
||||
domain: str
|
||||
has_website: bool
|
||||
uses_https: bool | None = None
|
||||
http_status_code: int | None = None
|
||||
redirect_url: str | None = None
|
||||
|
||||
|
||||
class HttpCheckBatchResponse(BaseModel):
|
||||
"""Response from batch HTTP check."""
|
||||
|
||||
processed: int
|
||||
results: list[HttpCheckBatchItem]
|
||||
|
||||
|
||||
class ScanSingleResponse(BaseModel):
|
||||
"""Response from a single scan (tech or performance)."""
|
||||
|
||||
domain: str
|
||||
profile: bool
|
||||
|
||||
|
||||
class ScanBatchResponse(BaseModel):
|
||||
"""Response from a batch scan."""
|
||||
|
||||
processed: int
|
||||
successful: int
|
||||
|
||||
|
||||
class ContactScrapeResponse(BaseModel):
|
||||
"""Response from a single contact scrape."""
|
||||
|
||||
domain: str
|
||||
contacts_found: int
|
||||
|
||||
|
||||
class FullEnrichmentResponse(BaseModel):
|
||||
"""Response from full enrichment pipeline."""
|
||||
|
||||
domain: str
|
||||
has_website: bool | None = None
|
||||
tech_scanned: bool
|
||||
perf_scanned: bool
|
||||
contacts_found: int
|
||||
score: int
|
||||
lead_tier: str
|
||||
|
||||
|
||||
class ScoreComputeBatchResponse(BaseModel):
|
||||
"""Response from batch score computation."""
|
||||
|
||||
scored: int
|
||||
44
app/modules/prospecting/schemas/interaction.py
Normal file
44
app/modules/prospecting/schemas/interaction.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# app/modules/prospecting/schemas/interaction.py
|
||||
"""Pydantic schemas for prospect interactions."""
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class InteractionCreate(BaseModel):
|
||||
"""Schema for creating an interaction."""
|
||||
|
||||
interaction_type: str = Field(
|
||||
..., pattern="^(note|call|email_sent|email_received|meeting|visit|sms|proposal_sent)$"
|
||||
)
|
||||
subject: str | None = Field(None, max_length=255)
|
||||
notes: str | None = None
|
||||
outcome: str | None = Field(None, pattern="^(positive|neutral|negative|no_answer)$")
|
||||
next_action: str | None = Field(None, max_length=255)
|
||||
next_action_date: date | None = None
|
||||
|
||||
|
||||
class InteractionResponse(BaseModel):
|
||||
"""Schema for interaction response."""
|
||||
|
||||
id: int
|
||||
prospect_id: int
|
||||
interaction_type: str
|
||||
subject: str | None = None
|
||||
notes: str | None = None
|
||||
outcome: str | None = None
|
||||
next_action: str | None = None
|
||||
next_action_date: date | None = None
|
||||
created_by_user_id: int
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class InteractionListResponse(BaseModel):
|
||||
"""List of interactions."""
|
||||
|
||||
items: list[InteractionResponse]
|
||||
total: int
|
||||
33
app/modules/prospecting/schemas/performance_profile.py
Normal file
33
app/modules/prospecting/schemas/performance_profile.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# app/modules/prospecting/schemas/performance_profile.py
|
||||
"""Pydantic schemas for performance profile."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PerformanceProfileResponse(BaseModel):
|
||||
"""Schema for performance profile response."""
|
||||
|
||||
id: int
|
||||
prospect_id: int
|
||||
performance_score: int | None = None
|
||||
accessibility_score: int | None = None
|
||||
best_practices_score: int | None = None
|
||||
seo_score: int | None = None
|
||||
first_contentful_paint_ms: int | None = None
|
||||
largest_contentful_paint_ms: int | None = None
|
||||
total_blocking_time_ms: int | None = None
|
||||
cumulative_layout_shift: float | None = None
|
||||
speed_index: int | None = None
|
||||
time_to_interactive_ms: int | None = None
|
||||
is_mobile_friendly: bool | None = None
|
||||
viewport_configured: bool | None = None
|
||||
total_bytes: int | None = None
|
||||
scan_strategy: str | None = None
|
||||
scan_error: str | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
122
app/modules/prospecting/schemas/prospect.py
Normal file
122
app/modules/prospecting/schemas/prospect.py
Normal file
@@ -0,0 +1,122 @@
|
||||
# app/modules/prospecting/schemas/prospect.py
|
||||
"""Pydantic schemas for prospect management."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ProspectCreate(BaseModel):
|
||||
"""Schema for creating a prospect."""
|
||||
|
||||
channel: str = Field("digital", pattern="^(digital|offline)$")
|
||||
business_name: str | None = Field(None, max_length=255)
|
||||
domain_name: str | None = Field(None, max_length=255)
|
||||
source: str | None = Field(None, max_length=100)
|
||||
address: str | None = Field(None, max_length=500)
|
||||
city: str | None = Field(None, max_length=100)
|
||||
postal_code: str | None = Field(None, max_length=10)
|
||||
country: str = Field("LU", max_length=2)
|
||||
notes: str | None = None
|
||||
tags: list[str] | None = None
|
||||
location_lat: float | None = None
|
||||
location_lng: float | None = None
|
||||
contacts: list["ProspectContactCreate"] | None = None
|
||||
|
||||
|
||||
class ProspectUpdate(BaseModel):
|
||||
"""Schema for updating a prospect."""
|
||||
|
||||
business_name: str | None = Field(None, max_length=255)
|
||||
status: str | None = None
|
||||
source: str | None = Field(None, max_length=100)
|
||||
address: str | None = Field(None, max_length=500)
|
||||
city: str | None = Field(None, max_length=100)
|
||||
postal_code: str | None = Field(None, max_length=10)
|
||||
notes: str | None = None
|
||||
tags: list[str] | None = None
|
||||
|
||||
|
||||
class ProspectResponse(BaseModel):
|
||||
"""Schema for prospect response."""
|
||||
|
||||
id: int
|
||||
channel: str
|
||||
business_name: str | None = None
|
||||
domain_name: str | None = None
|
||||
status: str
|
||||
source: str | None = None
|
||||
has_website: bool | None = None
|
||||
uses_https: bool | None = None
|
||||
http_status_code: int | None = None
|
||||
address: str | None = None
|
||||
city: str | None = None
|
||||
postal_code: str | None = None
|
||||
country: str = "LU"
|
||||
notes: str | None = None
|
||||
tags: list[str] | None = None
|
||||
location_lat: float | None = None
|
||||
location_lng: float | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
# Nested (optional, included in detail view)
|
||||
score: "ProspectScoreResponse | None" = None
|
||||
primary_email: str | None = None
|
||||
primary_phone: str | None = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ProspectDetailResponse(ProspectResponse):
|
||||
"""Full prospect detail with all related data."""
|
||||
|
||||
tech_profile: "TechProfileResponse | None" = None
|
||||
performance_profile: "PerformanceProfileResponse | None" = None
|
||||
contacts: list["ProspectContactResponse"] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ProspectListResponse(BaseModel):
|
||||
"""Paginated prospect list response."""
|
||||
|
||||
items: list[ProspectResponse]
|
||||
total: int
|
||||
page: int
|
||||
per_page: int
|
||||
pages: int
|
||||
|
||||
|
||||
class ProspectDeleteResponse(BaseModel):
|
||||
"""Response for prospect deletion."""
|
||||
|
||||
message: str
|
||||
|
||||
|
||||
class ProspectImportResponse(BaseModel):
|
||||
"""Response for domain import."""
|
||||
|
||||
created: int
|
||||
skipped: int
|
||||
total: int
|
||||
|
||||
|
||||
# Forward references resolved at module level
|
||||
from app.modules.prospecting.schemas.contact import ( # noqa: E402
|
||||
ProspectContactCreate,
|
||||
ProspectContactResponse,
|
||||
)
|
||||
from app.modules.prospecting.schemas.performance_profile import (
|
||||
PerformanceProfileResponse, # noqa: E402
|
||||
)
|
||||
from app.modules.prospecting.schemas.score import ProspectScoreResponse # noqa: E402
|
||||
from app.modules.prospecting.schemas.tech_profile import (
|
||||
TechProfileResponse, # noqa: E402
|
||||
)
|
||||
|
||||
ProspectCreate.model_rebuild()
|
||||
ProspectResponse.model_rebuild()
|
||||
ProspectDetailResponse.model_rebuild()
|
||||
38
app/modules/prospecting/schemas/scan_job.py
Normal file
38
app/modules/prospecting/schemas/scan_job.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# app/modules/prospecting/schemas/scan_job.py
|
||||
"""Pydantic schemas for scan jobs."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ScanJobResponse(BaseModel):
|
||||
"""Schema for scan job response."""
|
||||
|
||||
id: int
|
||||
job_type: str
|
||||
status: str
|
||||
total_items: int
|
||||
processed_items: int
|
||||
failed_items: int
|
||||
skipped_items: int
|
||||
progress_percent: float
|
||||
started_at: datetime | None = None
|
||||
completed_at: datetime | None = None
|
||||
error_log: str | None = None
|
||||
celery_task_id: str | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ScanJobListResponse(BaseModel):
|
||||
"""Paginated scan job list."""
|
||||
|
||||
items: list[ScanJobResponse]
|
||||
total: int
|
||||
page: int
|
||||
per_page: int
|
||||
pages: int
|
||||
27
app/modules/prospecting/schemas/score.py
Normal file
27
app/modules/prospecting/schemas/score.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# app/modules/prospecting/schemas/score.py
|
||||
"""Pydantic schemas for opportunity scoring."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ProspectScoreResponse(BaseModel):
|
||||
"""Schema for prospect score response."""
|
||||
|
||||
id: int
|
||||
prospect_id: int
|
||||
score: int
|
||||
technical_health_score: int
|
||||
modernity_score: int
|
||||
business_value_score: int
|
||||
engagement_score: int
|
||||
reason_flags: list[str] = []
|
||||
score_breakdown: dict[str, int] | None = None
|
||||
lead_tier: str | None = None
|
||||
notes: str | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
33
app/modules/prospecting/schemas/tech_profile.py
Normal file
33
app/modules/prospecting/schemas/tech_profile.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# app/modules/prospecting/schemas/tech_profile.py
|
||||
"""Pydantic schemas for technology profile."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class TechProfileResponse(BaseModel):
|
||||
"""Schema for technology profile response."""
|
||||
|
||||
id: int
|
||||
prospect_id: int
|
||||
cms: str | None = None
|
||||
cms_version: str | None = None
|
||||
server: str | None = None
|
||||
server_version: str | None = None
|
||||
hosting_provider: str | None = None
|
||||
cdn: str | None = None
|
||||
has_valid_cert: bool | None = None
|
||||
cert_issuer: str | None = None
|
||||
cert_expires_at: datetime | None = None
|
||||
js_framework: str | None = None
|
||||
analytics: str | None = None
|
||||
tag_manager: str | None = None
|
||||
ecommerce_platform: str | None = None
|
||||
scan_source: str | None = None
|
||||
scan_error: str | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
Reference in New Issue
Block a user