import uuid from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func from sqlalchemy.orm import selectinload from tickets.orm import SupportTicket, TicketMessage from tickets.models import TicketCreate, TicketUpdate, MessageCreate from shared.exceptions import NotFoundError async def create_ticket(db: AsyncSession, data: TicketCreate) -> SupportTicket: ticket = SupportTicket(**data.model_dump()) db.add(ticket) await db.commit() return await _get_ticket(db, ticket.id, include_internal=True) async def get_ticket(db: AsyncSession, ticket_id: uuid.UUID, include_internal: bool = True) -> SupportTicket: return await _get_ticket(db, ticket_id, include_internal) async def list_tickets( db: AsyncSession, status: str | None, priority: str | None, customer_id: str | None, page: int, limit: int, ) -> tuple[list[SupportTicket], int]: limit = min(100, max(1, limit)) offset = (max(1, page) - 1) * limit q = select(SupportTicket).options(selectinload(SupportTicket.messages)) if status: q = q.where(SupportTicket.status == status) if priority: q = q.where(SupportTicket.priority == priority) if customer_id: q = q.where(SupportTicket.customer_id == customer_id) total = (await db.execute(select(func.count()).select_from(q.subquery()))).scalar() rows = (await db.execute(q.order_by(SupportTicket.created_at.desc()).limit(limit).offset(offset))).scalars().all() return rows, total async def update_ticket(db: AsyncSession, ticket_id: uuid.UUID, data: TicketUpdate) -> SupportTicket: ticket = await _get_ticket(db, ticket_id) for field, value in data.model_dump(exclude_unset=True).items(): setattr(ticket, field, value) await db.commit() return await _get_ticket(db, ticket_id) async def add_message(db: AsyncSession, ticket_id: uuid.UUID, data: MessageCreate) -> SupportTicket: ticket = await _get_ticket(db, ticket_id) msg = TicketMessage(ticket_id=ticket_id, **data.model_dump()) db.add(msg) # Auto-advance ticket status based on who replied (skip if already resolved/closed) if ticket.status not in ("resolved", "closed"): if data.sender_type == "staff" and not data.is_internal: ticket.status = "waiting_on_customer" elif data.sender_type == "customer": ticket.status = "waiting_on_staff" await db.commit() return await _get_ticket(db, ticket_id) async def escalate_to_issue(db: AsyncSession, ticket_id: uuid.UUID, entry_id: uuid.UUID) -> SupportTicket: ticket = await _get_ticket(db, ticket_id) ticket.linked_entry_id = entry_id await db.commit() return await _get_ticket(db, ticket_id) async def list_by_customer(db: AsyncSession, customer_id: str) -> list[SupportTicket]: q = select(SupportTicket).options(selectinload(SupportTicket.messages)).where( SupportTicket.customer_id == customer_id ).order_by(SupportTicket.created_at.desc()) return (await db.execute(q)).scalars().all() async def list_by_device(db: AsyncSession, device_id: str) -> list[SupportTicket]: q = select(SupportTicket).options(selectinload(SupportTicket.messages)).where( SupportTicket.device_id == device_id ).order_by(SupportTicket.created_at.desc()) return (await db.execute(q)).scalars().all() async def _get_ticket(db: AsyncSession, ticket_id: uuid.UUID, include_internal: bool = True) -> SupportTicket: q = select(SupportTicket).options(selectinload(SupportTicket.messages)).where(SupportTicket.id == ticket_id) result = (await db.execute(q)).scalar_one_or_none() if not result: raise NotFoundError("Ticket") if not include_internal: result.messages = [m for m in result.messages if not m.is_internal] return result