Initial Switch to V2. Completely Overhauled Backend, Frontend and General Structure.

This commit is contained in:
2026-04-17 14:37:36 +03:00
parent eb773c5531
commit 0a8a42d69b
447 changed files with 70696 additions and 492 deletions

View File

@@ -0,0 +1,91 @@
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