Files
RemnaWaveBOT/app/services/support_ticket_service.py
2026-05-06 15:41:56 +03:00

209 lines
6.7 KiB
Python

from __future__ import annotations
import math
from dataclasses import dataclass, field
from datetime import datetime
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.db.base import utcnow
from app.db.models import SupportTicket
OPEN_SUPPORT_TICKET_STATUS = "OPEN"
ANSWERED_SUPPORT_TICKET_STATUS = "ANSWERED"
CLOSED_SUPPORT_TICKET_STATUS = "CLOSED"
@dataclass(slots=True)
class AdminSupportTicketListItem:
id: int
public_id: str
telegram_id: int
username: str | None
display_name: str
status: str
created_at: datetime
updated_at: datetime
closed_at: datetime | None
@dataclass(slots=True)
class AdminSupportTicketsPage:
items: list[AdminSupportTicketListItem] = field(default_factory=list)
page: int = 1
page_size: int = 8
total_items: int = 0
total_pages: int = 1
class SupportTicketService:
def __init__(self, *, session_factory: async_sessionmaker[AsyncSession]) -> None:
self._session_factory = session_factory
async def create_ticket(
self,
*,
public_id: str,
telegram_id: int,
username: str | None,
display_name: str,
user_message: str,
support_chat_id: int,
support_thread_id: int | None,
support_message_id: int,
) -> SupportTicket:
async with self._session_factory() as session:
ticket = SupportTicket(
public_id=public_id,
telegram_id=telegram_id,
username=username,
display_name=display_name,
user_message=user_message,
support_chat_id=support_chat_id,
support_thread_id=support_thread_id,
support_message_id=support_message_id,
status=OPEN_SUPPORT_TICKET_STATUS,
)
session.add(ticket)
await session.commit()
await session.refresh(ticket)
return ticket
async def get_ticket(self, *, ticket_id: int) -> SupportTicket | None:
async with self._session_factory() as session:
return await session.get(SupportTicket, ticket_id)
async def get_ticket_by_support_message(
self,
*,
support_chat_id: int,
support_message_id: int,
) -> SupportTicket | None:
async with self._session_factory() as session:
return await session.scalar(
select(SupportTicket).where(
SupportTicket.support_chat_id == support_chat_id,
SupportTicket.support_message_id == support_message_id,
)
)
async def mark_answered(self, *, ticket_id: int) -> SupportTicket | None:
async with self._session_factory() as session:
ticket = await session.get(SupportTicket, ticket_id)
if ticket is None:
return None
if ticket.status != CLOSED_SUPPORT_TICKET_STATUS:
ticket.status = ANSWERED_SUPPORT_TICKET_STATUS
ticket.updated_at = utcnow()
await session.commit()
await session.refresh(ticket)
return ticket
async def append_note(
self,
*,
ticket_id: int,
note_text: str,
status: str | None = None,
) -> SupportTicket | None:
cleaned_note = note_text.strip()
if not cleaned_note:
return None
async with self._session_factory() as session:
ticket = await session.get(SupportTicket, ticket_id)
if ticket is None:
return None
if ticket.support_note:
ticket.support_note = f"{ticket.support_note}\n\n{cleaned_note}"
else:
ticket.support_note = cleaned_note
if status and ticket.status != CLOSED_SUPPORT_TICKET_STATUS:
ticket.status = status
ticket.updated_at = utcnow()
await session.commit()
await session.refresh(ticket)
return ticket
async def close_ticket(
self,
*,
ticket_id: int,
closed_by_telegram_id: int | None,
) -> SupportTicket | None:
async with self._session_factory() as session:
ticket = await session.get(SupportTicket, ticket_id)
if ticket is None:
return None
ticket.status = CLOSED_SUPPORT_TICKET_STATUS
ticket.closed_by_telegram_id = closed_by_telegram_id
ticket.closed_at = utcnow()
ticket.updated_at = utcnow()
await session.commit()
await session.refresh(ticket)
return ticket
async def get_open_tickets_count(self) -> int:
async with self._session_factory() as session:
total = await session.scalar(
select(func.count(SupportTicket.id)).where(
SupportTicket.status != CLOSED_SUPPORT_TICKET_STATUS
)
)
return int(total or 0)
async def get_admin_tickets_page(
self,
*,
page: int,
page_size: int = 8,
) -> AdminSupportTicketsPage:
resolved_page_size = max(1, min(page_size, 20))
async with self._session_factory() as session:
total_items = int(
await session.scalar(select(func.count(SupportTicket.id))) or 0
)
total_pages = max(1, math.ceil(total_items / resolved_page_size)) if total_items else 1
resolved_page = min(max(page, 1), total_pages)
rows = list(
(
await session.scalars(
select(SupportTicket)
.order_by(
SupportTicket.created_at.desc(),
SupportTicket.id.desc(),
)
.offset((resolved_page - 1) * resolved_page_size)
.limit(resolved_page_size)
)
).all()
)
return AdminSupportTicketsPage(
items=[
AdminSupportTicketListItem(
id=row.id,
public_id=row.public_id,
telegram_id=row.telegram_id,
username=row.username,
display_name=row.display_name,
status=row.status,
created_at=row.created_at,
updated_at=row.updated_at,
closed_at=row.closed_at,
)
for row in rows
],
page=resolved_page,
page_size=resolved_page_size,
total_items=total_items,
total_pages=total_pages,
)