Files
ai-contract-platform/backend/app/services/pilot_request_service.py
T

181 lines
7.7 KiB
Python

from datetime import datetime, timezone
from typing import Any, Dict, Optional
from uuid import uuid4
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from app.models.pilot_request import PilotRequest
from app.repositories.pilot_request_repository import PilotRequestRepository
from app.services.tenant_service import DEFAULT_TENANT_ID, TenantService
ALLOWED_PILOT_REQUEST_STATUSES = {"new", "contacted", "scheduled", "closed"}
ALLOWED_PRIMARY_GOALS = {"contract_review", "contract_generation", "knowledge_base", "pilot_delivery"}
ALLOWED_DEPLOYMENT_PREFERENCES = {"saas", "private", "hybrid", "undecided"}
class PilotRequestService:
def __init__(self, db: Session):
self.db = db
self.repository = PilotRequestRepository(db)
self.tenant_service = TenantService(db)
def submit_request(
self,
*,
contact_name: str,
company_name: str,
role_title: Optional[str],
email: str,
phone: Optional[str],
expected_contract_volume: Optional[str],
primary_goal: str,
deployment_preference: Optional[str],
scenario_description: str,
) -> dict:
self.tenant_service.ensure_default_tenant()
normalized_primary_goal = self._normalize_primary_goal(primary_goal)
normalized_deployment = self._normalize_deployment_preference(deployment_preference)
pilot_request = PilotRequest(
id=f"pilot-{uuid4().hex[:12]}",
tenant_id=DEFAULT_TENANT_ID,
contact_name=self._normalize_required(contact_name, "contact name"),
company_name=self._normalize_required(company_name, "company name"),
role_title=self._normalize_optional(role_title),
email=self._normalize_required(email, "email").lower(),
phone=self._normalize_optional(phone),
expected_contract_volume=self._normalize_optional(expected_contract_volume),
primary_goal=normalized_primary_goal,
deployment_preference=normalized_deployment,
scenario_description=self._normalize_required(scenario_description, "scenario description"),
status="new",
source="website",
admin_notes=None,
last_contact_at=None,
)
created = self.repository.create(pilot_request)
return self._to_item(created)
def list_requests(
self,
current_user: Dict[str, Any],
*,
page: int = 1,
page_size: int = 20,
status_value: Optional[str] = None,
) -> dict:
self._require_admin(current_user)
normalized_status = self._normalize_status_filter(status_value)
items = self.repository.list_paginated(
current_user["tenant_id"],
page=max(page, 1),
page_size=max(min(page_size, 100), 1),
status_value=normalized_status,
)
total = self.repository.count(current_user["tenant_id"], status_value=normalized_status)
return {
"items": [self._to_item(item) for item in items],
"total": total,
"page": max(page, 1),
"page_size": max(min(page_size, 100), 1),
}
def get_summary(self, current_user: Dict[str, Any]) -> dict:
self._require_admin(current_user)
tenant_id = current_user["tenant_id"]
latest_items = self.repository.list_paginated(tenant_id, page=1, page_size=5)
total = self.repository.count(tenant_id)
counts = {
"new": self.repository.count(tenant_id, "new"),
"contacted": self.repository.count(tenant_id, "contacted"),
"scheduled": self.repository.count(tenant_id, "scheduled"),
"closed": self.repository.count(tenant_id, "closed"),
}
return {
"total": total,
"counts": counts,
"latest_items": [self._to_item(item) for item in latest_items],
}
def update_request(
self,
current_user: Dict[str, Any],
request_id: str,
*,
status_value: str,
admin_notes: Optional[str] = None,
) -> dict:
self._require_admin(current_user)
pilot_request = self.repository.get_by_id(current_user["tenant_id"], request_id)
if pilot_request is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="pilot request not found")
normalized_status = self._normalize_status_filter(status_value, required=True)
pilot_request.status = normalized_status
pilot_request.admin_notes = self._normalize_optional(admin_notes)
if normalized_status in {"contacted", "scheduled", "closed"}:
pilot_request.last_contact_at = datetime.now(timezone.utc)
updated = self.repository.update(pilot_request)
return self._to_item(updated)
def _require_admin(self, current_user: Dict[str, Any]):
if current_user.get("role") != "admin":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="forbidden")
def _normalize_required(self, value: Optional[str], label: str) -> str:
normalized = (value or "").strip()
if not normalized:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"{label} is required")
return normalized
def _normalize_optional(self, value: Optional[str]) -> Optional[str]:
normalized = (value or "").strip()
return normalized or None
def _normalize_primary_goal(self, value: str) -> str:
normalized = self._normalize_required(value, "primary goal").strip().lower()
if normalized not in ALLOWED_PRIMARY_GOALS:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid primary goal")
return normalized
def _normalize_deployment_preference(self, value: Optional[str]) -> Optional[str]:
normalized = self._normalize_optional(value)
if normalized is None:
return None
lowered = normalized.lower()
if lowered not in ALLOWED_DEPLOYMENT_PREFERENCES:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid deployment preference")
return lowered
def _normalize_status_filter(self, value: Optional[str], required: bool = False) -> Optional[str]:
normalized = self._normalize_optional(value)
if normalized is None:
if required:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="status is required")
return None
lowered = normalized.lower()
if lowered not in ALLOWED_PILOT_REQUEST_STATUSES:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid status")
return lowered
def _to_item(self, pilot_request: PilotRequest) -> dict:
return {
"id": pilot_request.id,
"tenant_id": pilot_request.tenant_id,
"contact_name": pilot_request.contact_name,
"company_name": pilot_request.company_name,
"role_title": pilot_request.role_title,
"email": pilot_request.email,
"phone": pilot_request.phone,
"expected_contract_volume": pilot_request.expected_contract_volume,
"primary_goal": pilot_request.primary_goal,
"deployment_preference": pilot_request.deployment_preference,
"scenario_description": pilot_request.scenario_description,
"status": pilot_request.status,
"source": pilot_request.source,
"admin_notes": pilot_request.admin_notes,
"last_contact_at": pilot_request.last_contact_at.isoformat() if pilot_request.last_contact_at else None,
"created_at": pilot_request.created_at.isoformat(),
"updated_at": pilot_request.updated_at.isoformat(),
}