diff --git a/backend/services/ticket_pagination_service.py b/backend/services/ticket_pagination_service.py new file mode 100644 index 000000000..521e0f547 --- /dev/null +++ b/backend/services/ticket_pagination_service.py @@ -0,0 +1,69 @@ +""" +Server-Side Ticket Pagination, Filtering, and Sorting Service. +Provides offset pagination, multi-attribute filtering, and dynamic field sorting for GET /tickets (#3978). +""" + +from typing import Any, Dict, List, Optional + + +class TicketPaginationService: + """ + Pagination, filtering, and sorting engine for ticket listings. + """ + + def __init__(self, default_page_size: int = 20, max_page_size: int = 100): + self.default_page_size = default_page_size + self.max_page_size = max_page_size + + def paginate_tickets( + self, + tickets: List[Dict[str, Any]], + page: int = 1, + limit: Optional[int] = None, + status: Optional[str] = None, + priority: Optional[str] = None, + sort_by: str = "created_at", + order: str = "desc", + ) -> Dict[str, Any]: + """ + Filter, sort, and paginate tickets list. + """ + page = max(1, page) + page_size = limit if limit and limit > 0 else self.default_page_size + page_size = min(page_size, self.max_page_size) + + filtered = tickets + + # Apply status filter + if status: + filtered = [t for t in filtered if t.get("status") == status] + + # Apply priority filter + if priority: + filtered = [t for t in filtered if t.get("priority") == priority] + + # Apply sorting + reverse = (order.lower() == "desc") + filtered = sorted( + filtered, + key=lambda t: t.get(sort_by) or "", + reverse=reverse, + ) + + # Apply pagination window + total_items = len(filtered) + total_pages = (total_items + page_size - 1) // page_size if total_items > 0 else 1 + start_idx = (page - 1) * page_size + end_idx = start_idx + page_size + + paginated_items = filtered[start_idx:end_idx] + + return { + "items": paginated_items, + "page": page, + "limit": page_size, + "total_items": total_items, + "total_pages": total_pages, + "has_next": page < total_pages, + "has_prev": page > 1, + } diff --git a/backend/tests/test_ticket_pagination_service.py b/backend/tests/test_ticket_pagination_service.py new file mode 100644 index 000000000..a4697c6ef --- /dev/null +++ b/backend/tests/test_ticket_pagination_service.py @@ -0,0 +1,39 @@ +import pytest +from backend.services.ticket_pagination_service import TicketPaginationService + + +def test_paginate_tickets_returns_correct_window(): + service = TicketPaginationService(default_page_size=2) + sample_tickets = [ + {"id": 1, "created_at": "2026-08-01T10:00:00Z", "status": "open", "priority": "high"}, + {"id": 2, "created_at": "2026-08-02T10:00:00Z", "status": "open", "priority": "low"}, + {"id": 3, "created_at": "2026-08-03T10:00:00Z", "status": "closed", "priority": "high"}, + ] + + res = service.paginate_tickets(sample_tickets, page=1, limit=2, sort_by="created_at", order="desc") + assert len(res["items"]) == 2 + assert res["items"][0]["id"] == 3 + assert res["total_items"] == 3 + assert res["total_pages"] == 2 + assert res["has_next"] is True + + +def test_paginate_tickets_filters_by_status_and_priority(): + service = TicketPaginationService() + sample_tickets = [ + {"id": 1, "status": "open", "priority": "high"}, + {"id": 2, "status": "closed", "priority": "high"}, + {"id": 3, "status": "open", "priority": "low"}, + ] + + res = service.paginate_tickets(sample_tickets, status="open", priority="high") + assert res["total_items"] == 1 + assert res["items"][0]["id"] == 1 + + +def test_paginate_tickets_empty_list_returns_default_pagination(): + service = TicketPaginationService() + res = service.paginate_tickets([]) + assert res["total_items"] == 0 + assert res["items"] == [] + assert res["total_pages"] == 1