diff --git a/src/main/java/com/petshop/backend/controller/ChatController.java b/src/main/java/com/petshop/backend/controller/ChatController.java index 35541f65..4392b25d 100644 --- a/src/main/java/com/petshop/backend/controller/ChatController.java +++ b/src/main/java/com/petshop/backend/controller/ChatController.java @@ -86,4 +86,13 @@ public class ChatController { List messages = chatService.getMessages(id, user.getId(), user.getRole()); return ResponseEntity.ok(messages); } + + @PostMapping("/conversations/{id}/request-human") + @PreAuthorize("hasRole('CUSTOMER')") + public ResponseEntity requestHumanTakeover(@PathVariable Long id) { + User user = getCurrentUser(); + ConversationResponse conversation = chatService.requestHumanTakeover(id, user.getId(), user.getRole()); + chatRealtimeService.publishConversationUpdate(id); + return ResponseEntity.ok(conversation); + } } diff --git a/src/main/java/com/petshop/backend/dto/chat/ConversationResponse.java b/src/main/java/com/petshop/backend/dto/chat/ConversationResponse.java index d9dbb1f8..86078ab2 100644 --- a/src/main/java/com/petshop/backend/dto/chat/ConversationResponse.java +++ b/src/main/java/com/petshop/backend/dto/chat/ConversationResponse.java @@ -9,19 +9,23 @@ public class ConversationResponse { private Long customerId; private Long staffId; private String status; + private String mode; private String lastMessage; + private LocalDateTime humanRequestedAt; private LocalDateTime createdAt; private LocalDateTime updatedAt; public ConversationResponse() { } - public ConversationResponse(Long id, Long customerId, Long staffId, String status, String lastMessage, LocalDateTime createdAt, LocalDateTime updatedAt) { + public ConversationResponse(Long id, Long customerId, Long staffId, String status, String mode, String lastMessage, LocalDateTime humanRequestedAt, LocalDateTime createdAt, LocalDateTime updatedAt) { this.id = id; this.customerId = customerId; this.staffId = staffId; this.status = status; + this.mode = mode; this.lastMessage = lastMessage; + this.humanRequestedAt = humanRequestedAt; this.createdAt = createdAt; this.updatedAt = updatedAt; } @@ -32,7 +36,9 @@ public class ConversationResponse { response.setCustomerId(conversation.getCustomerId()); response.setStaffId(conversation.getStaffId()); response.setStatus(conversation.getStatus().name()); + response.setMode(conversation.getMode().name()); response.setLastMessage(lastMessage); + response.setHumanRequestedAt(conversation.getHumanRequestedAt()); response.setCreatedAt(conversation.getCreatedAt()); response.setUpdatedAt(conversation.getUpdatedAt()); return response; @@ -70,6 +76,14 @@ public class ConversationResponse { this.status = status; } + public String getMode() { + return mode; + } + + public void setMode(String mode) { + this.mode = mode; + } + public String getLastMessage() { return lastMessage; } @@ -78,6 +92,14 @@ public class ConversationResponse { this.lastMessage = lastMessage; } + public LocalDateTime getHumanRequestedAt() { + return humanRequestedAt; + } + + public void setHumanRequestedAt(LocalDateTime humanRequestedAt) { + this.humanRequestedAt = humanRequestedAt; + } + public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/src/main/java/com/petshop/backend/entity/Conversation.java b/src/main/java/com/petshop/backend/entity/Conversation.java index 0a907710..080228eb 100644 --- a/src/main/java/com/petshop/backend/entity/Conversation.java +++ b/src/main/java/com/petshop/backend/entity/Conversation.java @@ -24,6 +24,13 @@ public class Conversation { @Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20)") private ConversationStatus status = ConversationStatus.OPEN; + @Enumerated(EnumType.STRING) + @Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20)") + private ConversationMode mode = ConversationMode.AUTOMATED; + + @Column + private LocalDateTime humanRequestedAt; + @CreationTimestamp @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; @@ -36,14 +43,20 @@ public class Conversation { OPEN, CLOSED } + public enum ConversationMode { + AUTOMATED, HUMAN + } + public Conversation() { } - public Conversation(Long id, Long customerId, Long staffId, ConversationStatus status, LocalDateTime createdAt, LocalDateTime updatedAt) { + public Conversation(Long id, Long customerId, Long staffId, ConversationStatus status, ConversationMode mode, LocalDateTime humanRequestedAt, LocalDateTime createdAt, LocalDateTime updatedAt) { this.id = id; this.customerId = customerId; this.staffId = staffId; this.status = status; + this.mode = mode; + this.humanRequestedAt = humanRequestedAt; this.createdAt = createdAt; this.updatedAt = updatedAt; } @@ -80,6 +93,22 @@ public class Conversation { this.status = status; } + public ConversationMode getMode() { + return mode; + } + + public void setMode(ConversationMode mode) { + this.mode = mode; + } + + public LocalDateTime getHumanRequestedAt() { + return humanRequestedAt; + } + + public void setHumanRequestedAt(LocalDateTime humanRequestedAt) { + this.humanRequestedAt = humanRequestedAt; + } + public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/src/main/java/com/petshop/backend/service/ChatService.java b/src/main/java/com/petshop/backend/service/ChatService.java index ac240eb0..f66cbdb5 100644 --- a/src/main/java/com/petshop/backend/service/ChatService.java +++ b/src/main/java/com/petshop/backend/service/ChatService.java @@ -17,6 +17,7 @@ import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; @@ -53,6 +54,7 @@ public class ChatService { Conversation conversation = new Conversation(); conversation.setCustomerId(customer.getCustomerId()); conversation.setStatus(Conversation.ConversationStatus.OPEN); + conversation.setMode(Conversation.ConversationMode.AUTOMATED); conversation = conversationRepository.save(conversation); Message message = new Message(); @@ -132,12 +134,35 @@ public class ChatService { if (role == User.Role.STAFF && conversation.getStaffId() == null) { conversation.setStaffId(userId); + } + + if (role == User.Role.STAFF) { + conversation.setMode(Conversation.ConversationMode.HUMAN); conversationRepository.save(conversation); } return MessageResponse.fromEntity(message); } + @Transactional + public ConversationResponse requestHumanTakeover(Long conversationId, Long userId, User.Role role) { + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); + + if (role != User.Role.CUSTOMER || !hasConversationAccess(conversation, userId, role)) { + throw new AccessDeniedException("You can only request human takeover for your own conversations"); + } + + if (conversation.getHumanRequestedAt() == null) { + conversation.setHumanRequestedAt(LocalDateTime.now()); + } + conversationRepository.save(conversation); + + List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); + String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1).getContent(); + return ConversationResponse.fromEntity(conversation, lastMessage); + } + public List getMessages(Long conversationId, Long userId, User.Role role) { Conversation conversation = conversationRepository.findById(conversationId) .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); diff --git a/src/main/resources/db/migration/V4__conversation_mode_and_takeover.sql b/src/main/resources/db/migration/V4__conversation_mode_and_takeover.sql new file mode 100644 index 00000000..271c0e72 --- /dev/null +++ b/src/main/resources/db/migration/V4__conversation_mode_and_takeover.sql @@ -0,0 +1,9 @@ +ALTER TABLE conversation + ADD COLUMN mode VARCHAR(20) NOT NULL DEFAULT 'AUTOMATED' AFTER status, + ADD COLUMN humanRequestedAt TIMESTAMP NULL AFTER mode; + +UPDATE conversation +SET mode = CASE + WHEN staffId IS NULL THEN 'AUTOMATED' + ELSE 'HUMAN' +END;