from datetime import datetime from enum import Enum from typing import List import logging from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, HttpUrl, Field, field_validator from sqlalchemy.ext.asyncio import AsyncSession from app.infrastructure.database import get_db from app import crud from app.core.exceptions import CallEventAlreadyExistsError, DatabaseError logger = logging.getLogger(__name__) class CallDirection(str, Enum): """Направление звонка""" in_ = "in" out = "out" class UisCallEvent(BaseModel): """Схема события звонка от UIS""" eventType: str = Field(..., description="Тип события") call_session_id: int = Field(..., gt=0, description="Уникальный ID сессии звонка") direction: CallDirection = Field(..., description="Направление звонка") employee_id: int = Field(..., gt=0, description="ID сотрудника") employee_full_name: str = Field(..., min_length=1, description="ФИО сотрудника") contact_phone_number: str = Field(..., description="Телефон контакта") called_phone_number: str = Field(..., description="Набранный телефон") communication_group_name: str = Field(..., description="Группа коммуникации") start_time: datetime = Field(..., description="Время начала звонка") finish_time: datetime = Field(..., description="Время окончания звонка") talk_time_duration: int = Field(..., ge=0, description="Длительность разговора (сек)") full_record_file_link: HttpUrl = Field(..., description="Ссылка на запись") campaign_name: str = Field(..., description="Название кампании") @field_validator('finish_time') @classmethod def validate_finish_time(cls, v: datetime, info) -> datetime: """Проверяем, что finish_time >= start_time""" if 'start_time' in info.data and v < info.data['start_time']: raise ValueError('finish_time must be greater than or equal to start_time') return v router = APIRouter() @router.post("/webhook", status_code=204) async def create_call_event( callEvent: UisCallEvent, db: AsyncSession = Depends(get_db) ) -> None: """ Webhook для получения событий звонков от UIS **Обрабатывает события:** - Сохраняет событие звонка в базу данных - Проверяет уникальность call_session_id - Логирует успешную обработку **Возможные ошибки:** - 409 Conflict: Событие с таким call_session_id уже существует - 422 Unprocessable Entity: Ошибка валидации данных - 500 Internal Server Error: Ошибка сервера/БД """ logger.info( f"Received webhook event", extra={ "event_type": callEvent.eventType, "call_session_id": callEvent.call_session_id, "employee_id": callEvent.employee_id } ) # Преобразуем UisCallEvent в данные для CallEvent (БД) event_data = { "call_session_id": callEvent.call_session_id, "direction": callEvent.direction.value, "employee_id": callEvent.employee_id, "last_answered_employee_full_name": callEvent.employee_full_name, "finish_time": int(callEvent.finish_time.timestamp()), "talk_time_duration": callEvent.talk_time_duration, "full_record_file_link": str(callEvent.full_record_file_link), # Поля, которых нет в UisCallEvent - заполняем значениями по умолчанию "notification_mnemonic": callEvent.eventType, "tcm_topcrm_notification_name": callEvent.campaign_name, "total_time_duration": callEvent.talk_time_duration, # Упрощение "wait_time_duration": 0, # Нет в запросе "total_wait_time_duration": 0, # Нет в запросе "clean_talk_time_duration": callEvent.talk_time_duration, # Упрощение } # Сохраняем в БД (исключения обрабатываются в exception handlers) await crud.create_call_event(db, event_data)