Add chat close endpoint
This commit is contained in:
@@ -96,4 +96,13 @@ public class ChatController {
|
|||||||
chatRealtimeService.publishConversationUpdate(id);
|
chatRealtimeService.publishConversationUpdate(id);
|
||||||
return ResponseEntity.ok(conversation);
|
return ResponseEntity.ok(conversation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/conversations/{id}/close")
|
||||||
|
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
|
||||||
|
public ResponseEntity<ConversationResponse> closeConversation(@PathVariable Long id) {
|
||||||
|
User user = getCurrentUser();
|
||||||
|
ConversationResponse conversation = chatService.closeConversation(id, user.getId(), user.getRole());
|
||||||
|
chatRealtimeService.publishConversationUpdate(id);
|
||||||
|
return ResponseEntity.ok(conversation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,10 @@ public class ChatService {
|
|||||||
Conversation conversation = conversationRepository.findById(conversationId)
|
Conversation conversation = conversationRepository.findById(conversationId)
|
||||||
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
|
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
|
||||||
|
|
||||||
|
if (conversation.getStatus() == Conversation.ConversationStatus.CLOSED) {
|
||||||
|
throw new AccessDeniedException("Conversation is closed");
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasConversationAccess(conversation, userId, role)) {
|
if (!hasConversationAccess(conversation, userId, role)) {
|
||||||
if (role == User.Role.CUSTOMER) {
|
if (role == User.Role.CUSTOMER) {
|
||||||
throw new AccessDeniedException("You can only send messages to your own conversations");
|
throw new AccessDeniedException("You can only send messages to your own conversations");
|
||||||
@@ -149,6 +153,10 @@ public class ChatService {
|
|||||||
Conversation conversation = conversationRepository.findById(conversationId)
|
Conversation conversation = conversationRepository.findById(conversationId)
|
||||||
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
|
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
|
||||||
|
|
||||||
|
if (conversation.getStatus() == Conversation.ConversationStatus.CLOSED) {
|
||||||
|
throw new AccessDeniedException("Conversation is closed");
|
||||||
|
}
|
||||||
|
|
||||||
if (role != User.Role.CUSTOMER || !hasConversationAccess(conversation, userId, role)) {
|
if (role != User.Role.CUSTOMER || !hasConversationAccess(conversation, userId, role)) {
|
||||||
throw new AccessDeniedException("You can only request human takeover for your own conversations");
|
throw new AccessDeniedException("You can only request human takeover for your own conversations");
|
||||||
}
|
}
|
||||||
@@ -163,6 +171,28 @@ public class ChatService {
|
|||||||
return ConversationResponse.fromEntity(conversation, lastMessage);
|
return ConversationResponse.fromEntity(conversation, lastMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ConversationResponse closeConversation(Long conversationId, Long userId, User.Role role) {
|
||||||
|
Conversation conversation = conversationRepository.findById(conversationId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
|
||||||
|
|
||||||
|
if (!hasConversationAccess(conversation, userId, role)) {
|
||||||
|
if (role == User.Role.CUSTOMER) {
|
||||||
|
throw new AccessDeniedException("You can only close your own conversations");
|
||||||
|
}
|
||||||
|
if (role == User.Role.STAFF) {
|
||||||
|
throw new AccessDeniedException("You can only close conversations assigned to you or unassigned conversations");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conversation.setStatus(Conversation.ConversationStatus.CLOSED);
|
||||||
|
conversation = conversationRepository.save(conversation);
|
||||||
|
|
||||||
|
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
|
||||||
|
String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1).getContent();
|
||||||
|
return ConversationResponse.fromEntity(conversation, lastMessage);
|
||||||
|
}
|
||||||
|
|
||||||
public List<MessageResponse> getMessages(Long conversationId, Long userId, User.Role role) {
|
public List<MessageResponse> getMessages(Long conversationId, Long userId, User.Role role) {
|
||||||
Conversation conversation = conversationRepository.findById(conversationId)
|
Conversation conversation = conversationRepository.findById(conversationId)
|
||||||
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
|
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package com.petshop.backend.service;
|
||||||
|
|
||||||
|
import com.petshop.backend.dto.chat.ConversationRequest;
|
||||||
|
import com.petshop.backend.dto.chat.MessageRequest;
|
||||||
|
import com.petshop.backend.entity.Conversation;
|
||||||
|
import com.petshop.backend.entity.Customer;
|
||||||
|
import com.petshop.backend.entity.Message;
|
||||||
|
import com.petshop.backend.entity.User;
|
||||||
|
import com.petshop.backend.repository.ConversationRepository;
|
||||||
|
import com.petshop.backend.repository.CustomerRepository;
|
||||||
|
import com.petshop.backend.repository.MessageRepository;
|
||||||
|
import com.petshop.backend.repository.UserRepository;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ChatServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ConversationRepository conversationRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private MessageRepository messageRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CustomerRepository customerRepository;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private ChatService chatService;
|
||||||
|
|
||||||
|
private Customer customer;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
customer = new Customer();
|
||||||
|
customer.setCustomerId(1L);
|
||||||
|
customer.setUserId(10L);
|
||||||
|
customer.setFirstName("Pat");
|
||||||
|
customer.setLastName("Owner");
|
||||||
|
customer.setEmail("pat@example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void closeConversationMarksConversationClosed() {
|
||||||
|
Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.OPEN);
|
||||||
|
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||||
|
when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer));
|
||||||
|
when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
when(messageRepository.findByConversationIdOrderByTimestampAsc(99L))
|
||||||
|
.thenReturn(List.of(message("hello")));
|
||||||
|
|
||||||
|
var response = chatService.closeConversation(99L, 10L, User.Role.CUSTOMER);
|
||||||
|
|
||||||
|
assertEquals("CLOSED", response.getStatus());
|
||||||
|
assertEquals("hello", response.getLastMessage());
|
||||||
|
verify(conversationRepository).save(conversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void closeConversationRejectsOtherCustomer() {
|
||||||
|
Conversation conversation = conversation(99L, 2L, null, Conversation.ConversationStatus.OPEN);
|
||||||
|
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||||
|
when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer));
|
||||||
|
|
||||||
|
assertThrows(AccessDeniedException.class, () -> chatService.closeConversation(99L, 10L, User.Role.CUSTOMER));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendMessageRejectsClosedConversation() {
|
||||||
|
Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.CLOSED);
|
||||||
|
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||||
|
|
||||||
|
assertThrows(AccessDeniedException.class,
|
||||||
|
() -> chatService.sendMessage(99L, 10L, User.Role.CUSTOMER, new MessageRequest("hello")));
|
||||||
|
|
||||||
|
verify(messageRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void requestHumanTakeoverRejectsClosedConversation() {
|
||||||
|
Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.CLOSED);
|
||||||
|
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||||
|
|
||||||
|
assertThrows(AccessDeniedException.class,
|
||||||
|
() -> chatService.requestHumanTakeover(99L, 10L, User.Role.CUSTOMER));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Conversation conversation(Long id, Long customerId, Long staffId, Conversation.ConversationStatus status) {
|
||||||
|
Conversation conversation = new Conversation();
|
||||||
|
conversation.setId(id);
|
||||||
|
conversation.setCustomerId(customerId);
|
||||||
|
conversation.setStaffId(staffId);
|
||||||
|
conversation.setStatus(status);
|
||||||
|
conversation.setMode(Conversation.ConversationMode.AUTOMATED);
|
||||||
|
conversation.setHumanRequestedAt(LocalDateTime.now());
|
||||||
|
return conversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Message message(String content) {
|
||||||
|
Message message = new Message();
|
||||||
|
message.setConversationId(99L);
|
||||||
|
message.setSenderId(10L);
|
||||||
|
message.setContent(content);
|
||||||
|
message.setIsRead(false);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user