Update chat conversation status
This commit is contained in:
@@ -4,6 +4,7 @@ import com.petshop.backend.dto.chat.ConversationRequest;
|
|||||||
import com.petshop.backend.dto.chat.ConversationResponse;
|
import com.petshop.backend.dto.chat.ConversationResponse;
|
||||||
import com.petshop.backend.dto.chat.MessageRequest;
|
import com.petshop.backend.dto.chat.MessageRequest;
|
||||||
import com.petshop.backend.dto.chat.MessageResponse;
|
import com.petshop.backend.dto.chat.MessageResponse;
|
||||||
|
import com.petshop.backend.dto.chat.UpdateConversationRequest;
|
||||||
import com.petshop.backend.entity.User;
|
import com.petshop.backend.entity.User;
|
||||||
import com.petshop.backend.repository.CustomerRepository;
|
import com.petshop.backend.repository.CustomerRepository;
|
||||||
import com.petshop.backend.repository.UserRepository;
|
import com.petshop.backend.repository.UserRepository;
|
||||||
@@ -97,11 +98,11 @@ public class ChatController {
|
|||||||
return ResponseEntity.ok(conversation);
|
return ResponseEntity.ok(conversation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/conversations/{id}/close")
|
@PutMapping("/conversations/{id}")
|
||||||
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
|
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
|
||||||
public ResponseEntity<ConversationResponse> closeConversation(@PathVariable Long id) {
|
public ResponseEntity<ConversationResponse> updateConversation(@PathVariable Long id, @Valid @RequestBody UpdateConversationRequest request) {
|
||||||
User user = getCurrentUser();
|
User user = getCurrentUser();
|
||||||
ConversationResponse conversation = chatService.closeConversation(id, user.getId(), user.getRole());
|
ConversationResponse conversation = chatService.updateConversation(id, user.getId(), user.getRole(), request);
|
||||||
chatRealtimeService.publishConversationUpdate(id);
|
chatRealtimeService.publishConversationUpdate(id);
|
||||||
return ResponseEntity.ok(conversation);
|
return ResponseEntity.ok(conversation);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.petshop.backend.dto.chat;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
|
||||||
|
public class UpdateConversationRequest {
|
||||||
|
@NotBlank(message = "Status is required")
|
||||||
|
@Pattern(regexp = "^(OPEN|CLOSED)$", message = "Status must be OPEN or CLOSED")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
public UpdateConversationRequest() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public UpdateConversationRequest(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import com.petshop.backend.dto.chat.ConversationRequest;
|
|||||||
import com.petshop.backend.dto.chat.ConversationResponse;
|
import com.petshop.backend.dto.chat.ConversationResponse;
|
||||||
import com.petshop.backend.dto.chat.MessageRequest;
|
import com.petshop.backend.dto.chat.MessageRequest;
|
||||||
import com.petshop.backend.dto.chat.MessageResponse;
|
import com.petshop.backend.dto.chat.MessageResponse;
|
||||||
|
import com.petshop.backend.dto.chat.UpdateConversationRequest;
|
||||||
import com.petshop.backend.entity.Conversation;
|
import com.petshop.backend.entity.Conversation;
|
||||||
import com.petshop.backend.entity.Customer;
|
import com.petshop.backend.entity.Customer;
|
||||||
import com.petshop.backend.entity.Message;
|
import com.petshop.backend.entity.Message;
|
||||||
@@ -172,7 +173,7 @@ public class ChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public ConversationResponse closeConversation(Long conversationId, Long userId, User.Role role) {
|
public ConversationResponse updateConversation(Long conversationId, Long userId, User.Role role, UpdateConversationRequest request) {
|
||||||
Conversation conversation = conversationRepository.findById(conversationId)
|
Conversation conversation = conversationRepository.findById(conversationId)
|
||||||
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
|
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
|
||||||
|
|
||||||
@@ -185,7 +186,7 @@ public class ChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
conversation.setStatus(Conversation.ConversationStatus.CLOSED);
|
conversation.setStatus(Conversation.ConversationStatus.valueOf(request.getStatus()));
|
||||||
conversation = conversationRepository.save(conversation);
|
conversation = conversationRepository.save(conversation);
|
||||||
|
|
||||||
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
|
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
|
||||||
@@ -193,6 +194,11 @@ public class ChatService {
|
|||||||
return ConversationResponse.fromEntity(conversation, lastMessage);
|
return ConversationResponse.fromEntity(conversation, lastMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ConversationResponse closeConversation(Long conversationId, Long userId, User.Role role) {
|
||||||
|
return updateConversation(conversationId, userId, role, new UpdateConversationRequest("CLOSED"));
|
||||||
|
}
|
||||||
|
|
||||||
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"));
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.petshop.backend.service;
|
package com.petshop.backend.service;
|
||||||
|
|
||||||
import com.petshop.backend.dto.chat.ConversationRequest;
|
|
||||||
import com.petshop.backend.dto.chat.MessageRequest;
|
import com.petshop.backend.dto.chat.MessageRequest;
|
||||||
|
import com.petshop.backend.dto.chat.UpdateConversationRequest;
|
||||||
import com.petshop.backend.entity.Conversation;
|
import com.petshop.backend.entity.Conversation;
|
||||||
import com.petshop.backend.entity.Customer;
|
import com.petshop.backend.entity.Customer;
|
||||||
import com.petshop.backend.entity.Message;
|
import com.petshop.backend.entity.Message;
|
||||||
@@ -60,7 +60,7 @@ class ChatServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void closeConversationMarksConversationClosed() {
|
void updateConversationMarksConversationClosed() {
|
||||||
Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.OPEN);
|
Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.OPEN);
|
||||||
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||||
when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer));
|
when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer));
|
||||||
@@ -68,7 +68,7 @@ class ChatServiceTest {
|
|||||||
when(messageRepository.findByConversationIdOrderByTimestampAsc(99L))
|
when(messageRepository.findByConversationIdOrderByTimestampAsc(99L))
|
||||||
.thenReturn(List.of(message("hello")));
|
.thenReturn(List.of(message("hello")));
|
||||||
|
|
||||||
var response = chatService.closeConversation(99L, 10L, User.Role.CUSTOMER);
|
var response = chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("CLOSED"));
|
||||||
|
|
||||||
assertEquals("CLOSED", response.getStatus());
|
assertEquals("CLOSED", response.getStatus());
|
||||||
assertEquals("hello", response.getLastMessage());
|
assertEquals("hello", response.getLastMessage());
|
||||||
@@ -76,12 +76,85 @@ class ChatServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void closeConversationRejectsOtherCustomer() {
|
void updateConversationRejectsOtherCustomer() {
|
||||||
Conversation conversation = conversation(99L, 2L, null, Conversation.ConversationStatus.OPEN);
|
Conversation conversation = conversation(99L, 2L, null, Conversation.ConversationStatus.OPEN);
|
||||||
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||||
when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer));
|
when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer));
|
||||||
|
|
||||||
assertThrows(AccessDeniedException.class, () -> chatService.closeConversation(99L, 10L, User.Role.CUSTOMER));
|
assertThrows(AccessDeniedException.class,
|
||||||
|
() -> chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("CLOSED")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateConversationIsIdempotent() {
|
||||||
|
Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.CLOSED);
|
||||||
|
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());
|
||||||
|
|
||||||
|
var response = chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("CLOSED"));
|
||||||
|
|
||||||
|
assertEquals("CLOSED", response.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void staffCanCloseAssignedConversation() {
|
||||||
|
Conversation conversation = conversation(99L, 1L, 77L, Conversation.ConversationStatus.OPEN);
|
||||||
|
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||||
|
when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)).thenReturn(List.of());
|
||||||
|
|
||||||
|
var response = chatService.updateConversation(99L, 77L, User.Role.STAFF, new UpdateConversationRequest("CLOSED"));
|
||||||
|
|
||||||
|
assertEquals("CLOSED", response.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void staffCanCloseUnassignedConversation() {
|
||||||
|
Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.OPEN);
|
||||||
|
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||||
|
when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)).thenReturn(List.of());
|
||||||
|
|
||||||
|
var response = chatService.updateConversation(99L, 77L, User.Role.STAFF, new UpdateConversationRequest("CLOSED"));
|
||||||
|
|
||||||
|
assertEquals("CLOSED", response.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void adminCanCloseAnyConversation() {
|
||||||
|
Conversation conversation = conversation(99L, 2L, 88L, Conversation.ConversationStatus.OPEN);
|
||||||
|
when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation));
|
||||||
|
when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)).thenReturn(List.of());
|
||||||
|
|
||||||
|
var response = chatService.updateConversation(99L, 1L, User.Role.ADMIN, new UpdateConversationRequest("CLOSED"));
|
||||||
|
|
||||||
|
assertEquals("CLOSED", response.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateConversationCanReopenClosedConversation() {
|
||||||
|
Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.CLOSED);
|
||||||
|
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());
|
||||||
|
|
||||||
|
var response = chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("OPEN"));
|
||||||
|
|
||||||
|
assertEquals("OPEN", response.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateConversationRejectsInvalidStatus() {
|
||||||
|
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));
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("INVALID")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user