T11 review caught that get_ticket was the one function without the *, marker all other functions in the module use. One-line fix, no caller impact. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
91 lines
2.8 KiB
Python
91 lines
2.8 KiB
Python
"""CRUD + status transitions for internal_tickets (the no-PSA fallback ticket model)."""
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.internal_ticket import InternalTicket
|
|
|
|
|
|
async def create_ticket(
|
|
db: AsyncSession,
|
|
*,
|
|
account_id: UUID,
|
|
created_by_user_id: UUID,
|
|
problem_statement: str,
|
|
customer_name: Optional[str] = None,
|
|
customer_contact: Optional[str] = None,
|
|
) -> InternalTicket:
|
|
"""Create a new internal ticket in 'open' status."""
|
|
ticket = InternalTicket(
|
|
account_id=account_id,
|
|
created_by_user_id=created_by_user_id,
|
|
problem_statement=problem_statement,
|
|
customer_name=customer_name,
|
|
customer_contact=customer_contact,
|
|
)
|
|
db.add(ticket)
|
|
await db.flush()
|
|
return ticket
|
|
|
|
|
|
async def update_status(
|
|
db: AsyncSession,
|
|
*,
|
|
ticket_id: UUID,
|
|
status: str,
|
|
resolution_notes: Optional[str] = None,
|
|
assigned_user_id: Optional[UUID] = None,
|
|
) -> InternalTicket:
|
|
"""Transition a ticket to a new status. Sets resolved_at when status='resolved'."""
|
|
ticket = await db.get(InternalTicket, ticket_id)
|
|
if not ticket:
|
|
raise ValueError(f"InternalTicket {ticket_id} not found")
|
|
ticket.status = status
|
|
if status == 'resolved':
|
|
ticket.resolved_at = datetime.now(timezone.utc)
|
|
if resolution_notes is not None:
|
|
ticket.resolution_notes = resolution_notes
|
|
if assigned_user_id is not None:
|
|
ticket.assigned_user_id = assigned_user_id
|
|
await db.flush()
|
|
return ticket
|
|
|
|
|
|
async def get_ticket(db: AsyncSession, *, ticket_id: UUID) -> Optional[InternalTicket]:
|
|
"""Fetch a ticket by ID. Returns None if not found."""
|
|
return await db.get(InternalTicket, ticket_id)
|
|
|
|
|
|
async def list_tickets_for_account(
|
|
db: AsyncSession,
|
|
*,
|
|
account_id: UUID,
|
|
status: Optional[str] = None,
|
|
limit: int = 100,
|
|
) -> list[InternalTicket]:
|
|
"""List tickets for an account, optionally filtered by status, newest first."""
|
|
stmt = select(InternalTicket).where(InternalTicket.account_id == account_id)
|
|
if status:
|
|
stmt = stmt.where(InternalTicket.status == status)
|
|
stmt = stmt.order_by(InternalTicket.created_at.desc()).limit(limit)
|
|
result = await db.execute(stmt)
|
|
return list(result.scalars())
|
|
|
|
|
|
async def promote_to_psa(
|
|
db: AsyncSession,
|
|
*,
|
|
ticket_id: UUID,
|
|
psa_ticket_id: str,
|
|
) -> InternalTicket:
|
|
"""Mark an internal ticket as promoted to PSA."""
|
|
ticket = await db.get(InternalTicket, ticket_id)
|
|
if not ticket:
|
|
raise ValueError(f"InternalTicket {ticket_id} not found")
|
|
ticket.psa_promoted_ticket_id = psa_ticket_id
|
|
await db.flush()
|
|
return ticket
|