mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-18 07:19:35 +00:00
- 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
227 lines
8.0 KiB
Python
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
|