update Unified Inbox
This commit is contained in:
parent
1565859a0c
commit
371deb40fa
@ -14,7 +14,8 @@ from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(tags=['2. Application Service']),
|
||||
create=extend_schema(tags=['2. Application Service']),
|
||||
get_summary=extend_schema(tags=['2. Application Service']),
|
||||
retrieve=extend_schema(tags=['2. Application Service']),
|
||||
)
|
||||
class AuditLogViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status, permissions
|
||||
from datetime import datetime, timezone
|
||||
from drf_spectacular.utils import extend_schema
|
||||
|
||||
# Import Service Layer
|
||||
from api.services.health_service import HealthService
|
||||
@ -9,6 +9,7 @@ from api.services.health_service import HealthService
|
||||
# Dependency Injection: สร้าง Instance ของ Service
|
||||
health_service = HealthService()
|
||||
|
||||
@extend_schema(tags=['2. Application Service'])
|
||||
class SystemHealthCheck(APIView):
|
||||
"""
|
||||
GET /api/v1/health/
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
from typing import Optional
|
||||
|
||||
from helpdesk.models import Ticket
|
||||
from django.db.models import QuerySet
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
class TicketRepository:
|
||||
|
||||
def get_ticket_by_id(self, ticket_id: int) -> Ticket:
|
||||
"""ดึง Ticket ตาม ID"""
|
||||
return Ticket.objects.select_related('creator', 'assigned_to').get(pk=ticket_id)
|
||||
def get_ticket_by_id(self, ticket_id: int) -> Optional[Ticket]:
|
||||
"""ดึง Ticket ตาม ID (คืนค่า None หากไม่พบ)"""
|
||||
try:
|
||||
return Ticket.objects.select_related('creator', 'assigned_to').get(pk=ticket_id)
|
||||
# ดักจับ ObjectDoesNotExist แล้วคืนค่า None เพื่อให้ TicketService ตรวจสอบ if ticket is None และโยน TicketNotFoundError แทน
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
def get_unified_inbox_list(self) -> QuerySet[Ticket]:
|
||||
"""
|
||||
|
||||
@ -5,7 +5,7 @@ from django.db.transaction import atomic
|
||||
|
||||
from helpdesk.repositories.ticket_repository import TicketRepository
|
||||
from helpdesk.models import Ticket, TicketStatus
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
class TicketNotFoundError(Exception):
|
||||
"""Raised when the requested Ticket is not found."""
|
||||
@ -47,16 +47,24 @@ class TicketService:
|
||||
|
||||
return self.ticket_repo.update_ticket(ticket, update_data)
|
||||
|
||||
def update_ticket_status(self, ticket_id: int, new_status: TicketStatus) -> Ticket:
|
||||
def update_ticket_status(self, ticket_id: int, new_status: str) -> Ticket:
|
||||
"""อัปเดตสถานะของ Ticket และตรวจสอบ Business Rule"""
|
||||
ticket = self.ticket_repo.get_ticket_by_id(ticket_id)
|
||||
if ticket is None:
|
||||
raise TicketNotFoundError(f"Ticket with id={ticket_id} not found.")
|
||||
|
||||
# Business Rule: ห้ามเปลี่ยนจาก CLOSED -> OPEN
|
||||
# Business Rule ห้ามเปลี่ยนจาก CLOSED -> OPEN
|
||||
if ticket.status == TicketStatus.CLOSED and new_status == TicketStatus.OPEN:
|
||||
raise ValueError("cannot transition from CLOSED to OPEN")
|
||||
raise ValueError("Cannot transition from CLOSED to OPEN")
|
||||
|
||||
return self.ticket_repo.update_ticket(ticket, {"status": new_status})
|
||||
# ตรวจสอบว่า new_status เป็นค่าที่ถูกต้องใน enum หรือไม่ (เผื่อกรณี View ส่ง string ที่ผิดมา)
|
||||
if new_status not in TicketStatus:
|
||||
raise ValueError(f"Invalid status value: {new_status}")
|
||||
|
||||
updated_ticket = self.ticket_repo.update_ticket(ticket, {"status": new_status})
|
||||
|
||||
# คืน updated_ticket
|
||||
return updated_ticket
|
||||
|
||||
def get_inbox_summary(self):
|
||||
return self.ticket_repo.get_unified_inbox_list()
|
||||
@ -77,7 +85,7 @@ class TicketService:
|
||||
}
|
||||
|
||||
@atomic
|
||||
def assign_ticket_to_user(self, ticket_id: int, assigned_user):
|
||||
def assign_ticket_to_user(self, ticket_id: int, assignee_id: int):
|
||||
"""
|
||||
มอบหมาย Ticket ให้กับผู้ใช้ (Agent) และเรียก NotificationService ถ้ามี
|
||||
"""
|
||||
@ -85,6 +93,13 @@ class TicketService:
|
||||
if not ticket:
|
||||
raise TicketNotFoundError(f"Ticket with id={ticket_id} not found.")
|
||||
|
||||
# ดึง User Object จาก ID
|
||||
User = get_user_model()
|
||||
try:
|
||||
assigned_user = User.objects.get(pk=assignee_id)
|
||||
except User.DoesNotExist:
|
||||
raise ValueError("User not found")
|
||||
|
||||
updated_ticket = self.ticket_repo.update_ticket(ticket, {"assigned_to": assigned_user})
|
||||
|
||||
if self.notification_service:
|
||||
@ -93,6 +108,7 @@ class TicketService:
|
||||
user_id=assigned_user.id
|
||||
)
|
||||
|
||||
return ticket
|
||||
# คืน updated_ticket
|
||||
return updated_ticket
|
||||
|
||||
ticket_service = TicketService()
|
||||
@ -53,7 +53,7 @@ class AssignmentNotificationIntegrationTest(TestCase):
|
||||
# ACT: มอบหมาย ticket
|
||||
updated_ticket = ticket_service.assign_ticket_to_user(
|
||||
ticket_id=self.ticket.id,
|
||||
assigned_user=self.agent_2
|
||||
assignee_id=self.agent_2.id
|
||||
)
|
||||
|
||||
# ASSERT 1: ตรวจสอบว่า Ticket ถูกมอบหมาย
|
||||
|
||||
@ -55,5 +55,5 @@ class TicketServiceStatusTest(TestCase):
|
||||
new_status=TicketStatus.OPEN
|
||||
)
|
||||
|
||||
self.assertIn("cannot transition from CLOSED to OPEN", str(ctx.exception))
|
||||
self.assertIn("Cannot transition from CLOSED to OPEN", str(ctx.exception))
|
||||
self.mock_repo.update_ticket.assert_not_called()
|
||||
|
||||
@ -55,3 +55,56 @@ class TicketViewSetFunctionalTest(APITestCase):
|
||||
url = reverse('helpdesk-tickets-list')
|
||||
response = client.get(url)
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test_resolve_ticket_by_agent(self):
|
||||
"""POST /tickets/{id}/resolve/ ต้องเปลี่ยนสถานะ Ticket เป็น RESOLVED"""
|
||||
resolve_url = reverse('helpdesk-tickets-resolve', args=[self.ticket.id])
|
||||
|
||||
# ACT: Agent เรียก resolve
|
||||
response = self.client_agent.post(resolve_url)
|
||||
|
||||
# ASSERT
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['status'], 'RESOLVED')
|
||||
|
||||
# ตรวจสอบใน DB อีกครั้ง
|
||||
self.ticket.refresh_from_db()
|
||||
self.assertEqual(self.ticket.status, 'RESOLVED')
|
||||
|
||||
def test_resolve_ticket_unauthenticated_fails(self):
|
||||
"""POST /tickets/{id}/resolve/ ต้อง return 401 เมื่อไม่ล็อกอิน"""
|
||||
client = APIClient()
|
||||
resolve_url = reverse('helpdesk-tickets-resolve', args=[self.ticket.id])
|
||||
response = client.post(resolve_url)
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test_assign_ticket_by_agent(self):
|
||||
"""POST /tickets/{id}/assign/ ต้องมอบหมาย Ticket ให้ Agent ที่ระบุ"""
|
||||
# สร้าง Agent ที่สองเพื่อมอบหมายไปหา
|
||||
agent2 = UserModel.objects.create_user(username="agent2", email="agent2@example.com", password="p", is_staff=True)
|
||||
assign_url = reverse('helpdesk-tickets-assign', args=[self.ticket.id])
|
||||
payload = {'assignee_id': agent2.id}
|
||||
|
||||
# ACT: Agent1 เรียก assign
|
||||
response = self.client_agent.post(assign_url, payload, format='json')
|
||||
|
||||
# ASSERT
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# ตรวจสอบว่า assigned_to ใน Response ตรงกับ ID ของ agent2
|
||||
self.assertEqual(response.data['assigned_to']['id'], agent2.id)
|
||||
|
||||
# ตรวจสอบใน DB อีกครั้ง
|
||||
self.ticket.refresh_from_db()
|
||||
self.assertEqual(self.ticket.assigned_to, agent2)
|
||||
|
||||
def test_assign_ticket_to_non_existent_user_fails(self):
|
||||
"""POST /tickets/{id}/assign/ ต้อง return 400 เมื่อมอบหมายให้ User ที่ไม่มีอยู่"""
|
||||
assign_url = reverse('helpdesk-tickets-assign', args=[self.ticket.id])
|
||||
# ใช้ ID ที่ไม่มีอยู่จริง (สมมติว่า ID 999999 ไม่มี)
|
||||
payload = {'assignee_id': 999999}
|
||||
|
||||
response = self.client_agent.post(assign_url, payload, format='json')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
# ตรวจสอบว่าข้อความ Error สื่อถึงปัญหา (ขึ้นอยู่กับ logic ใน ViewSet/Service)
|
||||
self.assertIn('not found', response.data['detail'])
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
from rest_framework import viewsets
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import action
|
||||
from helpdesk.services.ticket_service import ticket_service
|
||||
from helpdesk.serializers.ticket_list_serializers import TicketListSerializer
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
@ -7,13 +9,55 @@ from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(tags=['2. Application Service']),
|
||||
create=extend_schema(tags=['2. Application Service']),
|
||||
retrieve=extend_schema(tags=['2. Application Service']),
|
||||
resolve_ticket=extend_schema(tags=['2. Application Service']),
|
||||
assign_ticket=extend_schema(tags=['2. Application Service']),
|
||||
)
|
||||
class TicketViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""View สำหรับดึงรายการ Ticket ทั้งหมด (Unified Inbox List)"""
|
||||
serializer_class = TicketListSerializer
|
||||
permission_classes = [IsAuthenticated] # กำหนดให้เฉพาะผู้ที่ล็อกอินแล้วเท่านั้น
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
# 1. Viewset เรียก Service Layer
|
||||
def get_queryset(self):
|
||||
return ticket_service.get_inbox_summary()
|
||||
|
||||
# Custom Actions สำหรับการจัดการ Ticket
|
||||
@action(detail=True, methods=['post'], url_path='resolve', url_name='resolve')
|
||||
@extend_schema(
|
||||
summary='ปิด (Resolve) Ticket ด้วย ID',
|
||||
description='เปลี่ยนสถานะ Ticket ที่กำหนดให้เป็น "Resolved" หรือ "Closed".',
|
||||
)
|
||||
def resolve_ticket(self, request, pk=None):
|
||||
try:
|
||||
updated_ticket = ticket_service.update_ticket_status(
|
||||
ticket_id=pk,
|
||||
new_status='RESOLVED',
|
||||
)
|
||||
return Response(self.get_serializer(updated_ticket).data)
|
||||
except Exception as e:
|
||||
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='assign', url_name='assign')
|
||||
@extend_schema(
|
||||
summary='มอบหมาย (Assign) Ticket ให้ Agent',
|
||||
description='มอบหมาย Ticket นี้ให้กับผู้ใช้/Agent ที่ระบุใน body (เช่น {"assignee_id": 123}).',
|
||||
request={
|
||||
'application/json': {'type': 'object', 'properties': {'assignee_id': {'type': 'integer'}}}
|
||||
}
|
||||
)
|
||||
def assign_ticket(self, request, pk=None):
|
||||
assignee_id = request.data.get('assignee_id')
|
||||
if not assignee_id:
|
||||
return Response({"assignee_id": "Field is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
updated_ticket = ticket_service.assign_ticket_to_user(
|
||||
ticket_id=pk,
|
||||
assignee_id=assignee_id
|
||||
)
|
||||
return Response(self.get_serializer(updated_ticket).data)
|
||||
except ValueError as e:
|
||||
return Response({"detail": "User not found"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Exception as e:
|
||||
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
@ -19,3 +19,4 @@ python-dotenv
|
||||
flower
|
||||
drf-nested-routers
|
||||
channels_redis
|
||||
channels
|
||||
Loading…
x
Reference in New Issue
Block a user