Files
orion/app/modules/prospecting/migrations/versions/prospecting_001_initial.py
Samir Boulahtit 6d6eba75bf
Some checks failed
CI / pytest (push) Failing after 48m31s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
CI / ruff (push) Successful in 11s
CI / validate (push) Successful in 23s
CI / dependency-scanning (push) Successful in 28s
feat(prospecting): add complete prospecting module for lead discovery and scoring
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>
2026-02-28 00:59:47 +01:00

223 lines
12 KiB
Python

"""prospecting: initial tables for lead discovery and campaign management
Revision ID: prospecting_001
Revises: None
Create Date: 2026-02-27
"""
import sqlalchemy as sa
from alembic import op
revision = "prospecting_001"
down_revision = None
branch_labels = ("prospecting",)
depends_on = None
def upgrade() -> None:
# --- prospects ---
op.create_table(
"prospects",
sa.Column("id", sa.Integer(), primary_key=True, index=True),
sa.Column("channel", sa.String(10), nullable=False, server_default="digital"),
sa.Column("business_name", sa.String(255), nullable=True),
sa.Column("domain_name", sa.String(255), nullable=True, unique=True, index=True),
sa.Column("status", sa.String(20), nullable=False, server_default="pending"),
sa.Column("source", sa.String(100), nullable=True),
sa.Column("has_website", sa.Boolean(), nullable=True),
sa.Column("uses_https", sa.Boolean(), nullable=True),
sa.Column("http_status_code", sa.Integer(), nullable=True),
sa.Column("redirect_url", sa.Text(), nullable=True),
sa.Column("address", sa.String(500), nullable=True),
sa.Column("city", sa.String(100), nullable=True),
sa.Column("postal_code", sa.String(10), nullable=True),
sa.Column("country", sa.String(2), nullable=False, server_default="LU"),
sa.Column("notes", sa.Text(), nullable=True),
sa.Column("tags", sa.Text(), nullable=True),
sa.Column("captured_by_user_id", sa.Integer(), nullable=True),
sa.Column("location_lat", sa.Float(), nullable=True),
sa.Column("location_lng", sa.Float(), nullable=True),
sa.Column("last_http_check_at", sa.DateTime(), nullable=True),
sa.Column("last_tech_scan_at", sa.DateTime(), nullable=True),
sa.Column("last_perf_scan_at", sa.DateTime(), nullable=True),
sa.Column("last_contact_scrape_at", sa.DateTime(), nullable=True),
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
)
# --- prospect_tech_profiles ---
op.create_table(
"prospect_tech_profiles",
sa.Column("id", sa.Integer(), primary_key=True, index=True),
sa.Column("prospect_id", sa.Integer(), sa.ForeignKey("prospects.id", ondelete="CASCADE"), nullable=False, unique=True),
sa.Column("cms", sa.String(100), nullable=True),
sa.Column("cms_version", sa.String(50), nullable=True),
sa.Column("server", sa.String(100), nullable=True),
sa.Column("server_version", sa.String(50), nullable=True),
sa.Column("hosting_provider", sa.String(100), nullable=True),
sa.Column("cdn", sa.String(100), nullable=True),
sa.Column("has_valid_cert", sa.Boolean(), nullable=True),
sa.Column("cert_issuer", sa.String(200), nullable=True),
sa.Column("cert_expires_at", sa.DateTime(), nullable=True),
sa.Column("js_framework", sa.String(100), nullable=True),
sa.Column("analytics", sa.String(200), nullable=True),
sa.Column("tag_manager", sa.String(100), nullable=True),
sa.Column("ecommerce_platform", sa.String(100), nullable=True),
sa.Column("tech_stack_json", sa.Text(), nullable=True),
sa.Column("scan_source", sa.String(50), nullable=True),
sa.Column("scan_error", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
)
# --- prospect_performance_profiles ---
op.create_table(
"prospect_performance_profiles",
sa.Column("id", sa.Integer(), primary_key=True, index=True),
sa.Column("prospect_id", sa.Integer(), sa.ForeignKey("prospects.id", ondelete="CASCADE"), nullable=False, unique=True),
sa.Column("performance_score", sa.Integer(), nullable=True),
sa.Column("accessibility_score", sa.Integer(), nullable=True),
sa.Column("best_practices_score", sa.Integer(), nullable=True),
sa.Column("seo_score", sa.Integer(), nullable=True),
sa.Column("first_contentful_paint_ms", sa.Integer(), nullable=True),
sa.Column("largest_contentful_paint_ms", sa.Integer(), nullable=True),
sa.Column("total_blocking_time_ms", sa.Integer(), nullable=True),
sa.Column("cumulative_layout_shift", sa.Float(), nullable=True),
sa.Column("speed_index", sa.Integer(), nullable=True),
sa.Column("time_to_interactive_ms", sa.Integer(), nullable=True),
sa.Column("is_mobile_friendly", sa.Boolean(), nullable=True),
sa.Column("viewport_configured", sa.Boolean(), nullable=True),
sa.Column("font_size_ok", sa.Boolean(), nullable=True),
sa.Column("tap_targets_ok", sa.Boolean(), nullable=True),
sa.Column("total_bytes", sa.Integer(), nullable=True),
sa.Column("html_bytes", sa.Integer(), nullable=True),
sa.Column("css_bytes", sa.Integer(), nullable=True),
sa.Column("js_bytes", sa.Integer(), nullable=True),
sa.Column("image_bytes", sa.Integer(), nullable=True),
sa.Column("font_bytes", sa.Integer(), nullable=True),
sa.Column("total_requests", sa.Integer(), nullable=True),
sa.Column("js_requests", sa.Integer(), nullable=True),
sa.Column("css_requests", sa.Integer(), nullable=True),
sa.Column("image_requests", sa.Integer(), nullable=True),
sa.Column("lighthouse_json", sa.Text(), nullable=True),
sa.Column("scan_strategy", sa.String(20), nullable=True),
sa.Column("scan_error", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
)
# --- prospect_scores ---
op.create_table(
"prospect_scores",
sa.Column("id", sa.Integer(), primary_key=True, index=True),
sa.Column("prospect_id", sa.Integer(), sa.ForeignKey("prospects.id", ondelete="CASCADE"), nullable=False, unique=True),
sa.Column("score", sa.Integer(), nullable=False, server_default="0", index=True),
sa.Column("technical_health_score", sa.Integer(), nullable=False, server_default="0"),
sa.Column("modernity_score", sa.Integer(), nullable=False, server_default="0"),
sa.Column("business_value_score", sa.Integer(), nullable=False, server_default="0"),
sa.Column("engagement_score", sa.Integer(), nullable=False, server_default="0"),
sa.Column("reason_flags", sa.Text(), nullable=True),
sa.Column("score_breakdown", sa.Text(), nullable=True),
sa.Column("lead_tier", sa.String(20), nullable=True, index=True),
sa.Column("notes", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
)
# --- prospect_contacts ---
op.create_table(
"prospect_contacts",
sa.Column("id", sa.Integer(), primary_key=True, index=True),
sa.Column("prospect_id", sa.Integer(), sa.ForeignKey("prospects.id", ondelete="CASCADE"), nullable=False, index=True),
sa.Column("contact_type", sa.String(20), nullable=False),
sa.Column("value", sa.String(500), nullable=False),
sa.Column("label", sa.String(100), nullable=True),
sa.Column("source_url", sa.Text(), nullable=True),
sa.Column("source_element", sa.String(100), nullable=True),
sa.Column("is_validated", sa.Boolean(), nullable=False, server_default="0"),
sa.Column("validation_error", sa.Text(), nullable=True),
sa.Column("is_primary", sa.Boolean(), nullable=False, server_default="0"),
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
)
# --- prospect_scan_jobs ---
op.create_table(
"prospect_scan_jobs",
sa.Column("id", sa.Integer(), primary_key=True, index=True),
sa.Column("job_type", sa.String(30), nullable=False),
sa.Column("status", sa.String(20), nullable=False, server_default="pending"),
sa.Column("total_items", sa.Integer(), nullable=False, server_default="0"),
sa.Column("processed_items", sa.Integer(), nullable=False, server_default="0"),
sa.Column("failed_items", sa.Integer(), nullable=False, server_default="0"),
sa.Column("skipped_items", sa.Integer(), nullable=False, server_default="0"),
sa.Column("started_at", sa.DateTime(), nullable=True),
sa.Column("completed_at", sa.DateTime(), nullable=True),
sa.Column("config", sa.Text(), nullable=True),
sa.Column("result_summary", sa.Text(), nullable=True),
sa.Column("error_log", sa.Text(), nullable=True),
sa.Column("source_file", sa.String(500), nullable=True),
sa.Column("celery_task_id", sa.String(255), nullable=True),
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
)
# --- prospect_interactions ---
op.create_table(
"prospect_interactions",
sa.Column("id", sa.Integer(), primary_key=True, index=True),
sa.Column("prospect_id", sa.Integer(), sa.ForeignKey("prospects.id", ondelete="CASCADE"), nullable=False, index=True),
sa.Column("interaction_type", sa.String(20), nullable=False),
sa.Column("subject", sa.String(255), nullable=True),
sa.Column("notes", sa.Text(), nullable=True),
sa.Column("outcome", sa.String(20), nullable=True),
sa.Column("next_action", sa.String(255), nullable=True),
sa.Column("next_action_date", sa.Date(), nullable=True),
sa.Column("created_by_user_id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
)
# --- campaign_templates ---
op.create_table(
"campaign_templates",
sa.Column("id", sa.Integer(), primary_key=True, index=True),
sa.Column("name", sa.String(255), nullable=False),
sa.Column("lead_type", sa.String(30), nullable=False),
sa.Column("channel", sa.String(20), nullable=False, server_default="email"),
sa.Column("language", sa.String(5), nullable=False, server_default="fr"),
sa.Column("subject_template", sa.String(500), nullable=True),
sa.Column("body_template", sa.Text(), nullable=False),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="1"),
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
)
# --- campaign_sends ---
op.create_table(
"campaign_sends",
sa.Column("id", sa.Integer(), primary_key=True, index=True),
sa.Column("template_id", sa.Integer(), sa.ForeignKey("campaign_templates.id", ondelete="SET NULL"), nullable=True),
sa.Column("prospect_id", sa.Integer(), sa.ForeignKey("prospects.id", ondelete="CASCADE"), nullable=False, index=True),
sa.Column("channel", sa.String(20), nullable=False),
sa.Column("rendered_subject", sa.String(500), nullable=True),
sa.Column("rendered_body", sa.Text(), nullable=True),
sa.Column("status", sa.String(20), nullable=False, server_default="draft"),
sa.Column("sent_at", sa.DateTime(), nullable=True),
sa.Column("sent_by_user_id", sa.Integer(), nullable=True),
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
)
def downgrade() -> None:
op.drop_table("campaign_sends")
op.drop_table("campaign_templates")
op.drop_table("prospect_interactions")
op.drop_table("prospect_scan_jobs")
op.drop_table("prospect_contacts")
op.drop_table("prospect_scores")
op.drop_table("prospect_performance_profiles")
op.drop_table("prospect_tech_profiles")
op.drop_table("prospects")