97 lines
4.4 KiB
Python
97 lines
4.4 KiB
Python
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) |