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(
|
@extend_schema_view(
|
||||||
list=extend_schema(tags=['2. Application Service']),
|
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):
|
class AuditLogViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status, permissions
|
from rest_framework import status, permissions
|
||||||
from datetime import datetime, timezone
|
from drf_spectacular.utils import extend_schema
|
||||||
|
|
||||||
# Import Service Layer
|
# Import Service Layer
|
||||||
from api.services.health_service import HealthService
|
from api.services.health_service import HealthService
|
||||||
@ -9,6 +9,7 @@ from api.services.health_service import HealthService
|
|||||||
# Dependency Injection: สร้าง Instance ของ Service
|
# Dependency Injection: สร้าง Instance ของ Service
|
||||||
health_service = HealthService()
|
health_service = HealthService()
|
||||||
|
|
||||||
|
@extend_schema(tags=['2. Application Service'])
|
||||||
class SystemHealthCheck(APIView):
|
class SystemHealthCheck(APIView):
|
||||||
"""
|
"""
|
||||||
GET /api/v1/health/
|
GET /api/v1/health/
|
||||||
|
|||||||
@ -1,11 +1,18 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
from helpdesk.models import Ticket
|
from helpdesk.models import Ticket
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
class TicketRepository:
|
class TicketRepository:
|
||||||
|
|
||||||
def get_ticket_by_id(self, ticket_id: int) -> Ticket:
|
def get_ticket_by_id(self, ticket_id: int) -> Optional[Ticket]:
|
||||||
"""ดึง Ticket ตาม ID"""
|
"""ดึง Ticket ตาม ID (คืนค่า None หากไม่พบ)"""
|
||||||
|
try:
|
||||||
return Ticket.objects.select_related('creator', 'assigned_to').get(pk=ticket_id)
|
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]:
|
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.repositories.ticket_repository import TicketRepository
|
||||||
from helpdesk.models import Ticket, TicketStatus
|
from helpdesk.models import Ticket, TicketStatus
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
class TicketNotFoundError(Exception):
|
class TicketNotFoundError(Exception):
|
||||||
"""Raised when the requested Ticket is not found."""
|
"""Raised when the requested Ticket is not found."""
|
||||||
@ -47,16 +47,24 @@ class TicketService:
|
|||||||
|
|
||||||
return self.ticket_repo.update_ticket(ticket, update_data)
|
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)
|
ticket = self.ticket_repo.get_ticket_by_id(ticket_id)
|
||||||
if ticket is None:
|
if ticket is None:
|
||||||
raise TicketNotFoundError(f"Ticket with id={ticket_id} not found.")
|
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:
|
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):
|
def get_inbox_summary(self):
|
||||||
return self.ticket_repo.get_unified_inbox_list()
|
return self.ticket_repo.get_unified_inbox_list()
|
||||||
@ -77,7 +85,7 @@ class TicketService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@atomic
|
@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 ถ้ามี
|
มอบหมาย Ticket ให้กับผู้ใช้ (Agent) และเรียก NotificationService ถ้ามี
|
||||||
"""
|
"""
|
||||||
@ -85,6 +93,13 @@ class TicketService:
|
|||||||
if not ticket:
|
if not ticket:
|
||||||
raise TicketNotFoundError(f"Ticket with id={ticket_id} not found.")
|
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})
|
updated_ticket = self.ticket_repo.update_ticket(ticket, {"assigned_to": assigned_user})
|
||||||
|
|
||||||
if self.notification_service:
|
if self.notification_service:
|
||||||
@ -93,6 +108,7 @@ class TicketService:
|
|||||||
user_id=assigned_user.id
|
user_id=assigned_user.id
|
||||||
)
|
)
|
||||||
|
|
||||||
return ticket
|
# คืน updated_ticket
|
||||||
|
return updated_ticket
|
||||||
|
|
||||||
ticket_service = TicketService()
|
ticket_service = TicketService()
|
||||||
@ -53,7 +53,7 @@ class AssignmentNotificationIntegrationTest(TestCase):
|
|||||||
# ACT: มอบหมาย ticket
|
# ACT: มอบหมาย ticket
|
||||||
updated_ticket = ticket_service.assign_ticket_to_user(
|
updated_ticket = ticket_service.assign_ticket_to_user(
|
||||||
ticket_id=self.ticket.id,
|
ticket_id=self.ticket.id,
|
||||||
assigned_user=self.agent_2
|
assignee_id=self.agent_2.id
|
||||||
)
|
)
|
||||||
|
|
||||||
# ASSERT 1: ตรวจสอบว่า Ticket ถูกมอบหมาย
|
# ASSERT 1: ตรวจสอบว่า Ticket ถูกมอบหมาย
|
||||||
|
|||||||
@ -55,5 +55,5 @@ class TicketServiceStatusTest(TestCase):
|
|||||||
new_status=TicketStatus.OPEN
|
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()
|
self.mock_repo.update_ticket.assert_not_called()
|
||||||
|
|||||||
@ -55,3 +55,56 @@ class TicketViewSetFunctionalTest(APITestCase):
|
|||||||
url = reverse('helpdesk-tickets-list')
|
url = reverse('helpdesk-tickets-list')
|
||||||
response = client.get(url)
|
response = client.get(url)
|
||||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
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.services.ticket_service import ticket_service
|
||||||
from helpdesk.serializers.ticket_list_serializers import TicketListSerializer
|
from helpdesk.serializers.ticket_list_serializers import TicketListSerializer
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
@ -7,13 +9,55 @@ from drf_spectacular.utils import extend_schema, extend_schema_view
|
|||||||
|
|
||||||
@extend_schema_view(
|
@extend_schema_view(
|
||||||
list=extend_schema(tags=['2. Application Service']),
|
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):
|
class TicketViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
"""View สำหรับดึงรายการ Ticket ทั้งหมด (Unified Inbox List)"""
|
"""View สำหรับดึงรายการ Ticket ทั้งหมด (Unified Inbox List)"""
|
||||||
serializer_class = TicketListSerializer
|
serializer_class = TicketListSerializer
|
||||||
permission_classes = [IsAuthenticated] # กำหนดให้เฉพาะผู้ที่ล็อกอินแล้วเท่านั้น
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
# 1. Viewset เรียก Service Layer
|
# 1. Viewset เรียก Service Layer
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return ticket_service.get_inbox_summary()
|
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
|
flower
|
||||||
drf-nested-routers
|
drf-nested-routers
|
||||||
channels_redis
|
channels_redis
|
||||||
|
channels
|
||||||
Loading…
x
Reference in New Issue
Block a user