Add chat takeover

This commit is contained in:
2026-03-10 20:52:00 -06:00
parent 2e401a544f
commit c18ef16ef6
5 changed files with 96 additions and 2 deletions

View File

@@ -86,4 +86,13 @@ public class ChatController {
List<MessageResponse> messages = chatService.getMessages(id, user.getId(), user.getRole()); List<MessageResponse> messages = chatService.getMessages(id, user.getId(), user.getRole());
return ResponseEntity.ok(messages); return ResponseEntity.ok(messages);
} }
@PostMapping("/conversations/{id}/request-human")
@PreAuthorize("hasRole('CUSTOMER')")
public ResponseEntity<ConversationResponse> requestHumanTakeover(@PathVariable Long id) {
User user = getCurrentUser();
ConversationResponse conversation = chatService.requestHumanTakeover(id, user.getId(), user.getRole());
chatRealtimeService.publishConversationUpdate(id);
return ResponseEntity.ok(conversation);
}
} }

View File

@@ -9,19 +9,23 @@ public class ConversationResponse {
private Long customerId; private Long customerId;
private Long staffId; private Long staffId;
private String status; private String status;
private String mode;
private String lastMessage; private String lastMessage;
private LocalDateTime humanRequestedAt;
private LocalDateTime createdAt; private LocalDateTime createdAt;
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
public ConversationResponse() { 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.id = id;
this.customerId = customerId; this.customerId = customerId;
this.staffId = staffId; this.staffId = staffId;
this.status = status; this.status = status;
this.mode = mode;
this.lastMessage = lastMessage; this.lastMessage = lastMessage;
this.humanRequestedAt = humanRequestedAt;
this.createdAt = createdAt; this.createdAt = createdAt;
this.updatedAt = updatedAt; this.updatedAt = updatedAt;
} }
@@ -32,7 +36,9 @@ public class ConversationResponse {
response.setCustomerId(conversation.getCustomerId()); response.setCustomerId(conversation.getCustomerId());
response.setStaffId(conversation.getStaffId()); response.setStaffId(conversation.getStaffId());
response.setStatus(conversation.getStatus().name()); response.setStatus(conversation.getStatus().name());
response.setMode(conversation.getMode().name());
response.setLastMessage(lastMessage); response.setLastMessage(lastMessage);
response.setHumanRequestedAt(conversation.getHumanRequestedAt());
response.setCreatedAt(conversation.getCreatedAt()); response.setCreatedAt(conversation.getCreatedAt());
response.setUpdatedAt(conversation.getUpdatedAt()); response.setUpdatedAt(conversation.getUpdatedAt());
return response; return response;
@@ -70,6 +76,14 @@ public class ConversationResponse {
this.status = status; this.status = status;
} }
public String getMode() {
return mode;
}
public void setMode(String mode) {
this.mode = mode;
}
public String getLastMessage() { public String getLastMessage() {
return lastMessage; return lastMessage;
} }
@@ -78,6 +92,14 @@ public class ConversationResponse {
this.lastMessage = lastMessage; this.lastMessage = lastMessage;
} }
public LocalDateTime getHumanRequestedAt() {
return humanRequestedAt;
}
public void setHumanRequestedAt(LocalDateTime humanRequestedAt) {
this.humanRequestedAt = humanRequestedAt;
}
public LocalDateTime getCreatedAt() { public LocalDateTime getCreatedAt() {
return createdAt; return createdAt;
} }

View File

@@ -24,6 +24,13 @@ public class Conversation {
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20)") @Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20)")
private ConversationStatus status = ConversationStatus.OPEN; 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 @CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false) @Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt; private LocalDateTime createdAt;
@@ -36,14 +43,20 @@ public class Conversation {
OPEN, CLOSED OPEN, CLOSED
} }
public enum ConversationMode {
AUTOMATED, HUMAN
}
public Conversation() { 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.id = id;
this.customerId = customerId; this.customerId = customerId;
this.staffId = staffId; this.staffId = staffId;
this.status = status; this.status = status;
this.mode = mode;
this.humanRequestedAt = humanRequestedAt;
this.createdAt = createdAt; this.createdAt = createdAt;
this.updatedAt = updatedAt; this.updatedAt = updatedAt;
} }
@@ -80,6 +93,22 @@ public class Conversation {
this.status = status; 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() { public LocalDateTime getCreatedAt() {
return createdAt; return createdAt;
} }

View File

@@ -17,6 +17,7 @@ import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -53,6 +54,7 @@ public class ChatService {
Conversation conversation = new Conversation(); Conversation conversation = new Conversation();
conversation.setCustomerId(customer.getCustomerId()); conversation.setCustomerId(customer.getCustomerId());
conversation.setStatus(Conversation.ConversationStatus.OPEN); conversation.setStatus(Conversation.ConversationStatus.OPEN);
conversation.setMode(Conversation.ConversationMode.AUTOMATED);
conversation = conversationRepository.save(conversation); conversation = conversationRepository.save(conversation);
Message message = new Message(); Message message = new Message();
@@ -132,12 +134,35 @@ public class ChatService {
if (role == User.Role.STAFF && conversation.getStaffId() == null) { if (role == User.Role.STAFF && conversation.getStaffId() == null) {
conversation.setStaffId(userId); conversation.setStaffId(userId);
}
if (role == User.Role.STAFF) {
conversation.setMode(Conversation.ConversationMode.HUMAN);
conversationRepository.save(conversation); conversationRepository.save(conversation);
} }
return MessageResponse.fromEntity(message); 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<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"));

View File

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