Update chat conversation status

This commit is contained in:
2026-03-29 21:07:10 -06:00
parent 3cb3faa073
commit df42f68c04
4 changed files with 115 additions and 10 deletions

View File

@@ -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);
} }

View File

@@ -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;
}
}

View File

@@ -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"));

View File

@@ -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