system-prompts-and-models-o.../salesflow-saas/backend/app/api/v1/integrations_crm.py
Sami Assiri 07557c4be9 feat(dealix): GTM polish, CRM/AI APIs, launch verification hardening
- Add integrations CRM and AI routing APIs; Salesforce OAuth refresh; lead CRM metadata
- Marketer hub, settings CRM UI, OS views; premium landing and strategy_summary differentiators
- Docs: API-MAP, product guide, competitive matrix, launch simulation, AGENT-MAP LLM routing
- Sync script: strategy legal + competitive matrix to public; pytest DB isolation (.pytest_dealix.sqlite)
- Tests: CRM status and AI routing smoke; check_go_live_gate UTF-8 stdout on Windows
- Alembic migrations for strategic deal links and lead company/sector/city

Made-with: Cursor
2026-04-13 05:08:39 +03:00

227 lines
8.0 KiB
Python

"""CRM integrations API — Salesforce & HubSpot sync (JWT)."""
from __future__ import annotations
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user, require_role
from app.config import get_settings
from app.database import get_db
from app.models.tenant import Tenant
from app.models.user import User
from app.services.crm_sync_service import CRMSyncService
from app.services.lead_service import LeadService
from app.services.operations_hub import upsert_connector_status
router = APIRouter(prefix="/integrations/crm", tags=["Integrations — CRM"])
settings = get_settings()
class TenantCRMUpdate(BaseModel):
"""Store non-secret CRM overrides on tenant.settings['crm'] (encrypt at rest in production)."""
salesforce: dict | None = None
hubspot: dict | None = None
async def _tenant(db: AsyncSession, tenant_id: UUID) -> Tenant:
r = await db.execute(select(Tenant).where(Tenant.id == tenant_id))
t = r.scalar_one_or_none()
if not t:
raise HTTPException(status_code=404, detail="Tenant not found")
return t
@router.get("/status")
async def crm_status(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Which CRM keys are present (env vs tenant) — no secrets returned."""
tenant = await _tenant(db, current_user.tenant_id)
crm = (tenant.settings or {}).get("crm") or {}
sf_t = (crm.get("salesforce") or {})
hs_t = (crm.get("hubspot") or {})
return {
"salesforce": {
"env_refresh_configured": bool(
settings.SALESFORCE_CLIENT_ID
and settings.SALESFORCE_CLIENT_SECRET
and settings.SALESFORCE_REFRESH_TOKEN
),
"tenant_refresh_override": bool(sf_t.get("refresh_token")),
"domain": sf_t.get("domain") or settings.SALESFORCE_DOMAIN or "login.salesforce.com",
},
"hubspot": {
"env_token_configured": bool(settings.HUBSPOT_API_KEY),
"tenant_token_override": bool(hs_t.get("private_app_token") or hs_t.get("access_token")),
},
"docs": {
"integration_master_ar": "/strategy/INTEGRATION_MASTER_AR.md",
"api_map": "docs/API-MAP.md",
},
}
@router.put("/tenant-settings", dependencies=[Depends(require_role("owner", "manager", "admin"))])
async def put_tenant_crm_settings(
body: TenantCRMUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
tenant = await _tenant(db, current_user.tenant_id)
base = dict(tenant.settings or {})
crm = dict(base.get("crm") or {})
if body.salesforce is not None:
crm["salesforce"] = {**(crm.get("salesforce") or {}), **body.salesforce}
if body.hubspot is not None:
crm["hubspot"] = {**(crm.get("hubspot") or {}), **body.hubspot}
base["crm"] = crm
tenant.settings = base
await db.flush()
return {"status": "ok"}
@router.post("/salesforce/test", dependencies=[Depends(require_role("owner", "manager", "admin"))])
async def salesforce_test(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
svc = CRMSyncService(db)
try:
creds = await svc._get_crm_credentials(str(current_user.tenant_id), "salesforce")
except Exception as e:
raise HTTPException(status_code=400, detail=str(e)[:500]) from e
if not creds:
raise HTTPException(status_code=400, detail="Salesforce not configured (refresh token + client id/secret)")
probe = await svc.salesforce_identity_probe(creds)
ok = bool(probe.get("ok"))
await upsert_connector_status(
db,
current_user.tenant_id,
"crm_salesforce",
status="ok" if ok else "error",
success=ok,
last_error=None if ok else str(probe.get("detail") or probe)[:500],
)
return probe
@router.post("/salesforce/push-lead/{lead_id}", dependencies=[Depends(require_role("owner", "manager", "admin"))])
async def salesforce_push_lead(
lead_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
svc = CRMSyncService(db)
res = await svc.sync_lead_to_crm(str(current_user.tenant_id), str(lead_id), "salesforce")
if res.get("status") == "success" and res.get("salesforce_id"):
ls = LeadService(db)
await ls.merge_lead_extra_metadata(
str(current_user.tenant_id),
str(lead_id),
{"salesforce_lead_id": res["salesforce_id"]},
)
await upsert_connector_status(
db, current_user.tenant_id, "crm_salesforce", status="ok", success=True,
)
elif res.get("status") == "error":
await upsert_connector_status(
db,
current_user.tenant_id,
"crm_salesforce",
status="error",
last_error=str(res.get("message", res))[:500],
)
return res
class PullBody(BaseModel):
since: str | None = Field(None, description="Ignored for Salesforce MVP; reserved for SOQL")
@router.post("/salesforce/pull-leads", dependencies=[Depends(require_role("owner", "manager", "admin"))])
async def salesforce_pull_leads(
body: PullBody | None = None,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
_ = body
svc = CRMSyncService(db)
res = await svc.full_sync(str(current_user.tenant_id), "salesforce")
if res.get("status") == "completed":
await upsert_connector_status(
db, current_user.tenant_id, "crm_salesforce", status="ok", success=True,
)
return res
@router.post("/hubspot/test", dependencies=[Depends(require_role("owner", "manager", "admin"))])
async def hubspot_test(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
svc = CRMSyncService(db)
creds = await svc._get_crm_credentials(str(current_user.tenant_id), "hubspot")
if not creds:
raise HTTPException(status_code=400, detail="HubSpot token not configured")
probe = await svc.hubspot_identity_probe(creds.get("api_key", ""))
ok = bool(probe.get("ok"))
await upsert_connector_status(
db,
current_user.tenant_id,
"crm_hubspot",
status="ok" if ok else "error",
success=ok,
last_error=None if ok else str(probe.get("detail") or probe)[:500],
)
return probe
@router.post("/hubspot/push-lead/{lead_id}", dependencies=[Depends(require_role("owner", "manager", "admin"))])
async def hubspot_push_lead(
lead_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
svc = CRMSyncService(db)
res = await svc.sync_lead_to_crm(str(current_user.tenant_id), str(lead_id), "hubspot")
if res.get("status") == "success" and res.get("hubspot_id"):
ls = LeadService(db)
await ls.merge_lead_extra_metadata(
str(current_user.tenant_id),
str(lead_id),
{"hubspot_contact_id": res["hubspot_id"]},
)
await upsert_connector_status(
db, current_user.tenant_id, "crm_hubspot", status="ok", success=True,
)
elif res.get("status") == "error":
await upsert_connector_status(
db,
current_user.tenant_id,
"crm_hubspot",
status="error",
last_error=str(res.get("message", res))[:500],
)
return res
@router.post("/hubspot/pull-contacts", dependencies=[Depends(require_role("owner", "manager", "admin"))])
async def hubspot_pull_contacts(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
svc = CRMSyncService(db)
res = await svc.full_sync(str(current_user.tenant_id), "hubspot")
if res.get("status") == "completed":
await upsert_connector_status(
db, current_user.tenant_id, "crm_hubspot", status="ok", success=True,
)
return res