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, )