181 lines
7.7 KiB
Python
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(),
|
|
}
|