[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
|
||||
|
||||
Reference in New Issue
Block a user