Catalyst / admin/Synapse-Cortex 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Synapse-Cortex

public

Self Hosted ITSM Tool with RBAC/Tenanting and MFA

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
Synapse-Cortex / synapse-cortex / app / queries.py 4330 B · main
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import uuid
from datetime import datetime, timedelta, timezone

from sqlalchemy import or_
from sqlalchemy.orm import Query, Session

from . import models

OPEN_TICKET_STATUSES = [
    models.TicketStatus.NEW,
    models.TicketStatus.IN_PROGRESS,
    models.TicketStatus.AWAITING_USER,
]

# Every queue-card filter name maps to a predicate applied to a base
# tenant-scoped Ticket query. Shared by the dashboard (for counts) and the
# tickets list page (for the filtered view a card links to), so the two can
# never drift out of sync.
QUEUE_FILTERS = ("unassigned", "awaiting_user", "sla_breaching", "resolved_today")


def tenant_tickets(db: Session, tenant_id: uuid.UUID) -> Query:
    return db.query(models.Ticket).filter(models.Ticket.tenant_id == tenant_id)


def apply_queue_filter(query: Query, filter_name: str) -> Query:
    now = datetime.now(timezone.utc)

    if filter_name == "unassigned":
        return query.filter(
            models.Ticket.assigned_to_id.is_(None), models.Ticket.status.in_(OPEN_TICKET_STATUSES)
        )
    if filter_name == "awaiting_user":
        return query.filter(models.Ticket.status == models.TicketStatus.AWAITING_USER)
    if filter_name == "sla_breaching":
        return query.filter(
            models.Ticket.status.in_(OPEN_TICKET_STATUSES),
            models.Ticket.resolution_due_at.isnot(None),
            models.Ticket.resolution_due_at <= now,
        )
    if filter_name == "resolved_today":
        today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
        return query.filter(
            models.Ticket.status.in_([models.TicketStatus.RESOLVED, models.TicketStatus.CLOSED]),
            models.Ticket.resolved_at.isnot(None),
            models.Ticket.resolved_at >= today_start,
        )
    return query


def apply_ticket_search(query: Query, search_text: str) -> Query:
    """Keyword search across title/description/ticket number. Splits on
    whitespace and requires every keyword to match somewhere (AND across
    keywords, OR across fields per keyword), so "printer jammed" narrows
    results rather than broadening them."""
    keywords = [kw for kw in search_text.strip().split() if kw]
    for kw in keywords:
        pattern = f"%{kw}%"
        query = query.filter(
            or_(
                models.Ticket.title.ilike(pattern),
                models.Ticket.description.ilike(pattern),
                models.Ticket.ticket_number.ilike(pattern),
            )
        )
    return query


TICKET_SORTS = ("newest", "oldest", "priority", "status")

_PRIORITY_ORDER = {
    models.TicketPriority.CRITICAL: 0,
    models.TicketPriority.HIGH: 1,
    models.TicketPriority.MEDIUM: 2,
    models.TicketPriority.LOW: 3,
}


def apply_ticket_sort(query: Query, sort_name: str):
    """Returns (query, in_python_sort_key_or_None). Priority has no natural
    DB column ordering (it's an enum whose values don't sort the way we want
    across dialects), so that one case sorts in Python after fetching."""
    if sort_name == "oldest":
        return query.order_by(models.Ticket.created_at.asc()), None
    if sort_name == "status":
        return query.order_by(models.Ticket.status.asc(), models.Ticket.created_at.desc()), None
    if sort_name == "priority":
        return query, lambda t: _PRIORITY_ORDER.get(t.priority, 99)
    return query.order_by(models.Ticket.created_at.desc()), None


def queue_counts(db: Session, tenant_id: uuid.UUID) -> dict:
    base = tenant_tickets(db, tenant_id)
    return {name: apply_queue_filter(base, name).count() for name in QUEUE_FILTERS}


def next_ticket_number(db: Session, tenant_id: uuid.UUID) -> str:
    count = db.query(models.Ticket).filter(models.Ticket.tenant_id == tenant_id).count()
    return f"TCK-{count + 1:06d}"


def apply_sla(db: Session, ticket: "models.Ticket") -> None:
    policy = (
        db.query(models.SLAPolicy)
        .filter(models.SLAPolicy.tenant_id == ticket.tenant_id, models.SLAPolicy.priority == ticket.priority)
        .first()
    )
    if not policy:
        return
    ticket.sla_policy_id = policy.id
    anchor = ticket.created_at or datetime.now(timezone.utc)
    ticket.response_due_at = anchor + timedelta(minutes=policy.response_time_minutes)
    ticket.resolution_due_at = anchor + timedelta(minutes=policy.resolution_time_minutes)