[2ff88f42] Full End-to-End integration: Webhooks, Auto-Enrichment, Notion-Sync, UI updates and new Connector Architecture
This commit is contained in:
@@ -32,7 +32,7 @@ setup_logging()
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from .database import init_db, get_db, Company, Signal, EnrichmentData, RoboticsCategory, Contact, Industry, JobRoleMapping, ReportedMistake
|
||||
from .database import init_db, get_db, Company, Signal, EnrichmentData, RoboticsCategory, Contact, Industry, JobRoleMapping, ReportedMistake, MarketingMatrix
|
||||
from .services.deduplication import Deduplicator
|
||||
from .services.discovery import DiscoveryService
|
||||
from .services.scraping import ScraperService
|
||||
@@ -42,8 +42,7 @@ from .services.classification import ClassificationService
|
||||
app = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
version=settings.VERSION,
|
||||
description="Backend for Company Explorer (Robotics Edition)",
|
||||
root_path="/ce"
|
||||
description="Backend for Company Explorer (Robotics Edition)"
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
@@ -65,6 +64,7 @@ class CompanyCreate(BaseModel):
|
||||
city: Optional[str] = None
|
||||
country: str = "DE"
|
||||
website: Optional[str] = None
|
||||
crm_id: Optional[str] = None
|
||||
|
||||
class BulkImportRequest(BaseModel):
|
||||
names: List[str]
|
||||
@@ -84,6 +84,20 @@ class ReportMistakeRequest(BaseModel):
|
||||
quote: Optional[str] = None
|
||||
user_comment: Optional[str] = None
|
||||
|
||||
class ProvisioningRequest(BaseModel):
|
||||
so_contact_id: int
|
||||
so_person_id: Optional[int] = None
|
||||
crm_name: Optional[str] = None
|
||||
crm_website: Optional[str] = None
|
||||
|
||||
class ProvisioningResponse(BaseModel):
|
||||
status: str
|
||||
company_name: str
|
||||
website: Optional[str] = None
|
||||
vertical_name: Optional[str] = None
|
||||
role_name: Optional[str] = None
|
||||
texts: Dict[str, Optional[str]] = {}
|
||||
|
||||
# --- Events ---
|
||||
@app.on_event("startup")
|
||||
def on_startup():
|
||||
@@ -100,6 +114,141 @@ def on_startup():
|
||||
def health_check(username: str = Depends(authenticate_user)):
|
||||
return {"status": "ok", "version": settings.VERSION, "db": settings.DATABASE_URL}
|
||||
|
||||
@app.post("/api/provision/superoffice-contact", response_model=ProvisioningResponse)
|
||||
def provision_superoffice_contact(
|
||||
req: ProvisioningRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db),
|
||||
username: str = Depends(authenticate_user)
|
||||
):
|
||||
# 1. Find Company (via SO ID)
|
||||
company = db.query(Company).filter(Company.crm_id == str(req.so_contact_id)).first()
|
||||
|
||||
if not company:
|
||||
# AUTO-CREATE Logic
|
||||
if not req.crm_name:
|
||||
# Cannot create without name. Should ideally not happen if Connector does its job.
|
||||
raise HTTPException(400, "Cannot create company: crm_name missing")
|
||||
|
||||
company = Company(
|
||||
name=req.crm_name,
|
||||
crm_id=str(req.so_contact_id),
|
||||
crm_name=req.crm_name,
|
||||
crm_website=req.crm_website,
|
||||
status="NEW"
|
||||
)
|
||||
db.add(company)
|
||||
db.commit()
|
||||
db.refresh(company)
|
||||
logger.info(f"Auto-created company {company.name} from SuperOffice request.")
|
||||
|
||||
# Trigger Discovery
|
||||
background_tasks.add_task(run_discovery_task, company.id)
|
||||
|
||||
return ProvisioningResponse(
|
||||
status="processing",
|
||||
company_name=company.name
|
||||
)
|
||||
|
||||
# 1b. Check Status & Progress
|
||||
# If NEW or DISCOVERED, we are not ready to provide texts.
|
||||
if company.status in ["NEW", "DISCOVERED"]:
|
||||
# If we have a website, ensure analysis is triggered
|
||||
if company.status == "DISCOVERED" or (company.website and company.website != "k.A."):
|
||||
background_tasks.add_task(run_analysis_task, company.id)
|
||||
elif company.status == "NEW":
|
||||
# Ensure discovery runs
|
||||
background_tasks.add_task(run_discovery_task, company.id)
|
||||
|
||||
return ProvisioningResponse(
|
||||
status="processing",
|
||||
company_name=company.name
|
||||
)
|
||||
|
||||
# 1c. Update CRM Snapshot Data (The Double Truth)
|
||||
changed = False
|
||||
if req.crm_name:
|
||||
company.crm_name = req.crm_name
|
||||
changed = True
|
||||
if req.crm_website:
|
||||
company.crm_website = req.crm_website
|
||||
changed = True
|
||||
|
||||
# Simple Mismatch Check
|
||||
if company.website and company.crm_website:
|
||||
def norm(u): return str(u).lower().replace("https://", "").replace("http://", "").replace("www.", "").strip("/")
|
||||
if norm(company.website) != norm(company.crm_website):
|
||||
company.data_mismatch_score = 0.8 # High mismatch
|
||||
changed = True
|
||||
else:
|
||||
if company.data_mismatch_score != 0.0:
|
||||
company.data_mismatch_score = 0.0
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
company.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
# 2. Find Contact (Person)
|
||||
if req.so_person_id is None:
|
||||
# Just a company sync, no texts needed
|
||||
return ProvisioningResponse(
|
||||
status="success",
|
||||
company_name=company.name,
|
||||
website=company.website,
|
||||
vertical_name=company.industry_ai
|
||||
)
|
||||
|
||||
person = db.query(Contact).filter(Contact.so_person_id == req.so_person_id).first()
|
||||
|
||||
# 3. Determine Role
|
||||
role_name = None
|
||||
if person and person.role:
|
||||
role_name = person.role
|
||||
elif req.job_title:
|
||||
# Simple classification fallback
|
||||
mappings = db.query(JobRoleMapping).all()
|
||||
for m in mappings:
|
||||
# Check pattern type (Regex vs Simple) - simplified here
|
||||
pattern_clean = m.pattern.replace("%", "").lower()
|
||||
if pattern_clean in req.job_title.lower():
|
||||
role_name = m.role
|
||||
break
|
||||
|
||||
# 4. Determine Vertical (Industry)
|
||||
vertical_name = company.industry_ai
|
||||
|
||||
# 5. Fetch Texts from Matrix
|
||||
texts = {"subject": None, "intro": None, "social_proof": None}
|
||||
|
||||
if vertical_name and role_name:
|
||||
industry_obj = db.query(Industry).filter(Industry.name == vertical_name).first()
|
||||
|
||||
if industry_obj:
|
||||
# Find any mapping for this role to query the Matrix
|
||||
# (Assuming Matrix is linked to *one* canonical mapping for this role string)
|
||||
role_ids = [m.id for m in db.query(JobRoleMapping).filter(JobRoleMapping.role == role_name).all()]
|
||||
|
||||
if role_ids:
|
||||
matrix_entry = db.query(MarketingMatrix).filter(
|
||||
MarketingMatrix.industry_id == industry_obj.id,
|
||||
MarketingMatrix.role_id.in_(role_ids)
|
||||
).first()
|
||||
|
||||
if matrix_entry:
|
||||
texts["subject"] = matrix_entry.subject
|
||||
texts["intro"] = matrix_entry.intro
|
||||
texts["social_proof"] = matrix_entry.social_proof
|
||||
|
||||
return ProvisioningResponse(
|
||||
status="success",
|
||||
company_name=company.name,
|
||||
website=company.website,
|
||||
vertical_name=vertical_name,
|
||||
role_name=role_name,
|
||||
texts=texts
|
||||
)
|
||||
|
||||
@app.get("/api/companies")
|
||||
def list_companies(
|
||||
skip: int = 0,
|
||||
@@ -234,6 +383,7 @@ def create_company(company: CompanyCreate, db: Session = Depends(get_db), userna
|
||||
city=company.city,
|
||||
country=company.country,
|
||||
website=company.website,
|
||||
crm_id=company.crm_id,
|
||||
status="NEW"
|
||||
)
|
||||
db.add(new_company)
|
||||
@@ -665,10 +815,23 @@ def run_analysis_task(company_id: int):
|
||||
# --- Serve Frontend ---
|
||||
static_path = "/frontend_static"
|
||||
if not os.path.exists(static_path):
|
||||
static_path = os.path.join(os.path.dirname(__file__), "../static")
|
||||
# Local dev fallback
|
||||
static_path = os.path.join(os.path.dirname(__file__), "../../frontend/dist")
|
||||
if not os.path.exists(static_path):
|
||||
static_path = os.path.join(os.path.dirname(__file__), "../static")
|
||||
|
||||
logger.info(f"Static files path: {static_path} (Exists: {os.path.exists(static_path)})")
|
||||
|
||||
if os.path.exists(static_path):
|
||||
@app.get("/")
|
||||
async def serve_index():
|
||||
return FileResponse(os.path.join(static_path, "index.html"))
|
||||
|
||||
app.mount("/", StaticFiles(directory=static_path, html=True), name="static")
|
||||
else:
|
||||
@app.get("/")
|
||||
def root_no_frontend():
|
||||
return {"message": "Company Explorer API is running, but frontend was not found.", "path_tried": static_path}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
@@ -93,6 +93,10 @@ class Contact(Base):
|
||||
job_title = Column(String) # Visitenkarten-Titel
|
||||
language = Column(String, default="De") # "De", "En"
|
||||
|
||||
# SuperOffice Mapping
|
||||
so_contact_id = Column(Integer, nullable=True, index=True) # SuperOffice Contact ID (Company)
|
||||
so_person_id = Column(Integer, nullable=True, unique=True, index=True) # SuperOffice Person ID
|
||||
|
||||
role = Column(String) # Operativer Entscheider, etc.
|
||||
status = Column(String, default="") # Marketing Status
|
||||
|
||||
@@ -248,6 +252,30 @@ class ReportedMistake(Base):
|
||||
company = relationship("Company", back_populates="reported_mistakes")
|
||||
|
||||
|
||||
class MarketingMatrix(Base):
|
||||
"""
|
||||
Stores the static marketing texts for Industry x Role combinations.
|
||||
Source: Notion (synced).
|
||||
"""
|
||||
__tablename__ = "marketing_matrix"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# The combination keys
|
||||
industry_id = Column(Integer, ForeignKey("industries.id"), nullable=False)
|
||||
role_id = Column(Integer, ForeignKey("job_role_mappings.id"), nullable=False)
|
||||
|
||||
# The Content
|
||||
subject = Column(Text, nullable=True)
|
||||
intro = Column(Text, nullable=True)
|
||||
social_proof = Column(Text, nullable=True)
|
||||
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
industry = relationship("Industry")
|
||||
role = relationship("JobRoleMapping")
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# UTILS
|
||||
# ==============================================================================
|
||||
|
||||
@@ -72,6 +72,10 @@ def extract_select(prop):
|
||||
if not prop or "select" not in prop or not prop["select"]: return ""
|
||||
return prop["select"]["name"]
|
||||
|
||||
def extract_number(prop):
|
||||
if not prop or "number" not in prop: return None
|
||||
return prop["number"]
|
||||
|
||||
def sync():
|
||||
logger.info("--- Starting Enhanced Sync ---")
|
||||
|
||||
@@ -131,6 +135,16 @@ def sync():
|
||||
ind.pains = extract_rich_text(props.get("Pains"))
|
||||
ind.gains = extract_rich_text(props.get("Gains"))
|
||||
|
||||
# Metrics & Scraper Config (NEW)
|
||||
ind.metric_type = extract_select(props.get("Metric Type"))
|
||||
ind.min_requirement = extract_number(props.get("Min. Requirement"))
|
||||
ind.whale_threshold = extract_number(props.get("Whale Threshold"))
|
||||
ind.proxy_factor = extract_number(props.get("Proxy Factor"))
|
||||
|
||||
ind.scraper_search_term = extract_rich_text(props.get("Scraper Search Term"))
|
||||
ind.scraper_keywords = extract_rich_text(props.get("Scraper Keywords"))
|
||||
ind.standardization_logic = extract_rich_text(props.get("Standardization Logic"))
|
||||
|
||||
# Status / Priority
|
||||
prio = extract_select(props.get("Priorität"))
|
||||
if not prio: prio = extract_select(props.get("Freigegeben"))
|
||||
|
||||
Reference in New Issue
Block a user