209 lines
6.7 KiB
Python
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,
|
|
)
|