Initial commit

This commit is contained in:
2025-11-19 22:54:33 +07:00
commit 8f5b984598
18 changed files with 761 additions and 0 deletions

View File

View File

@@ -0,0 +1,118 @@
from datetime import datetime
from enum import Enum
from typing import List
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, HttpUrl
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app import crud
class CallDirection(str, Enum):
in_ = "in"
out = "out"
class UisCallEvent(BaseModel):
eventType: str
call_session_id: str
direction: CallDirection
employee_id: int
employee_full_name: str
contact_phone_number: str
called_phone_number: str
communication_group_name: str
start_time: datetime
finish_time: datetime
talk_time_duration: int
full_record_file_link: HttpUrl
campaign_name: str
class Config:
from_attributes = True
class CallEventResponse(BaseModel):
id: int
event_type: str
call_session_id: str
direction: str
employee_id: int
employee_full_name: str
contact_phone_number: str
called_phone_number: str
communication_group_name: str
start_time: datetime
finish_time: datetime
talk_time_duration: int
full_record_file_link: str
campaign_name: str
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
router = APIRouter()
@router.post("/webhook", response_model=CallEventResponse, status_code=201)
async def create_call_event(callEvent: UisCallEvent, db: AsyncSession = Depends(get_db)):
"""Webhook для получения событий звонков от UIS"""
# Проверяем, не существует ли уже событие с таким call_session_id
existing_event = await crud.get_call_event_by_session_id(db, callEvent.call_session_id)
if existing_event:
raise HTTPException(status_code=400, detail="Call event with this session_id already exists")
# Преобразуем Pydantic модель в словарь для БД
call_event_data = {
"event_type": callEvent.eventType,
"call_session_id": callEvent.call_session_id,
"direction": callEvent.direction,
"employee_id": callEvent.employee_id,
"employee_full_name": callEvent.employee_full_name,
"contact_phone_number": callEvent.contact_phone_number,
"called_phone_number": callEvent.called_phone_number,
"communication_group_name": callEvent.communication_group_name,
"start_time": callEvent.start_time,
"finish_time": callEvent.finish_time,
"talk_time_duration": callEvent.talk_time_duration,
"full_record_file_link": str(callEvent.full_record_file_link),
"campaign_name": callEvent.campaign_name,
}
db_call_event = await crud.create_call_event(db, call_event_data)
return db_call_event
@router.get("/events", response_model=List[CallEventResponse])
async def get_call_events(
skip: int = 0,
limit: int = 100,
db: AsyncSession = Depends(get_db)
):
"""Получить список всех событий звонков"""
events = await crud.get_all_call_events(db, skip=skip, limit=limit)
return events
@router.get("/events/{call_session_id}", response_model=CallEventResponse)
async def get_call_event(call_session_id: str, db: AsyncSession = Depends(get_db)):
"""Получить событие звонка по session_id"""
event = await crud.get_call_event_by_session_id(db, call_session_id)
if not event:
raise HTTPException(status_code=404, detail="Call event not found")
return event
@router.get("/events/employee/{employee_id}", response_model=List[CallEventResponse])
async def get_employee_call_events(
employee_id: int,
skip: int = 0,
limit: int = 100,
db: AsyncSession = Depends(get_db)
):
"""Получить все звонки конкретного сотрудника"""
events = await crud.get_call_events_by_employee(db, employee_id, skip=skip, limit=limit)
return events

View File

@@ -0,0 +1,22 @@
import os
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from dotenv import load_dotenv
load_dotenv()
# Для Alembic используем синхронный движок с psycopg2
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://postgres:postgres@localhost:5432/ingest_db")
# Преобразуем asyncpg URL в psycopg2 для синхронного движка
SYNC_DATABASE_URL = DATABASE_URL.replace("postgresql+asyncpg://", "postgresql://")
engine = create_engine(SYNC_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

View File

@@ -0,0 +1,11 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.api.uis import router as uis_router
app = FastAPI(
title="Ingest Service API",
description="Микросервис для приема событий звонков",
version="1.0.0",
)
app.include_router(uis_router, prefix="/v1/uis", tags=["UIS Webhooks"])

View File

@@ -0,0 +1,35 @@
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, Enum as SQLEnum
from app.infrastructure import Base
import enum
class CallDirectionEnum(str, enum.Enum):
in_ = "in"
out = "out"
class CallEvent(Base):
"""Модель для хранения событий звонков"""
__tablename__ = "call_events"
id = Column(Integer, primary_key=True, index=True)
event_type = Column(String, nullable=False)
call_session_id = Column(String, unique=True, index=True, nullable=False)
direction = Column(SQLEnum(CallDirectionEnum), nullable=False)
employee_id = Column(Integer, nullable=False, index=True)
employee_full_name = Column(String, nullable=False)
contact_phone_number = Column(String, nullable=False)
called_phone_number = Column(String, nullable=False)
communication_group_name = Column(String)
start_time = Column(DateTime, nullable=False)
finish_time = Column(DateTime, nullable=False)
talk_time_duration = Column(Integer, nullable=False)
full_record_file_link = Column(String, nullable=False)
campaign_name = Column(String)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def __repr__(self):
return f"<CallEvent(id={self.id}, call_session_id={self.call_session_id})>"

View File

@@ -0,0 +1,4 @@
from app.models.call_event import CallEvent
__all__ = ["CallEvent"]

View File

@@ -0,0 +1,20 @@
from sqlalchemy import Column, BigInteger, Integer, UUID, String
from app.core.database import Base
class CallEvent(Base):
__tablename__ = "call_events"
id = Column(UUID, primary_key=True, index=True)
call_session_id = Column(BigInteger, nullable=False, index=True)
direction = Column(String, nullable=False)
notification_mnemonic = Column(String, nullable=False)
last_answered_employee_full_name = Column(String, nullable=True)
employee_id = Column(Integer, nullable=True)
finish_time = Column(Integer, nullable=False)
total_time_duration = Column(Integer, nullable=False)
wait_time_duration = Column(Integer, nullable=False)
total_wait_time_duration = Column(Integer, nullable=False)
talk_time_duration = Column(Integer, nullable=False)
clean_talk_time_duration = Column(Integer, nullable=False)
full_record_file_link = Column(String, nullable=False)
tcm_topcrm_notification_name = Column(String, nullable=False)