diff --git a/docker-compose.yml b/docker-compose.yml index 1966e7e6..423dab50 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,8 @@ services: - "3306:3306" volumes: - db_data:/var/lib/mysql + - ./src/main/resources/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql + - ./src/main/resources/data.sql:/docker-entrypoint-initdb.d/02-data.sql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"] interval: 5s diff --git a/pom.xml b/pom.xml index 47a732fe..5ad436e8 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,11 @@ spring-boot-starter-validation + + org.springframework.boot + spring-boot-starter-websocket + + com.mysql mysql-connector-j diff --git a/src/main/java/com/petshop/backend/config/WebSocketConfig.java b/src/main/java/com/petshop/backend/config/WebSocketConfig.java new file mode 100644 index 00000000..84944c25 --- /dev/null +++ b/src/main/java/com/petshop/backend/config/WebSocketConfig.java @@ -0,0 +1,25 @@ +package com.petshop.backend.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + config.enableSimpleBroker("/topic", "/queue"); + config.setApplicationDestinationPrefixes("/app"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws/chat") + .setAllowedOriginPatterns("*") + .withSockJS(); + } +} diff --git a/src/main/java/com/petshop/backend/controller/AdoptionController.java b/src/main/java/com/petshop/backend/controller/AdoptionController.java index d200a3db..2ff0bef7 100644 --- a/src/main/java/com/petshop/backend/controller/AdoptionController.java +++ b/src/main/java/com/petshop/backend/controller/AdoptionController.java @@ -14,7 +14,6 @@ import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/adoptions") -@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class AdoptionController { private final AdoptionService adoptionService; @@ -24,6 +23,7 @@ public class AdoptionController { } @GetMapping + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity> getAllAdoptions( @RequestParam(required = false) String q, Pageable pageable) { @@ -31,16 +31,19 @@ public class AdoptionController { } @GetMapping("/{id}") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity getAdoptionById(@PathVariable Long id) { return ResponseEntity.ok(adoptionService.getAdoptionById(id)); } @PostMapping + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity createAdoption(@Valid @RequestBody AdoptionRequest request) { return ResponseEntity.status(HttpStatus.CREATED).body(adoptionService.createAdoption(request)); } @PutMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity updateAdoption( @PathVariable Long id, @Valid @RequestBody AdoptionRequest request) { @@ -48,12 +51,14 @@ public class AdoptionController { } @DeleteMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity deleteAdoption(@PathVariable Long id) { adoptionService.deleteAdoption(id); return ResponseEntity.noContent().build(); } @DeleteMapping + @PreAuthorize("hasRole('ADMIN')") public ResponseEntity bulkDeleteAdoptions(@Valid @RequestBody BulkDeleteRequest request) { adoptionService.bulkDeleteAdoptions(request); return ResponseEntity.noContent().build(); diff --git a/src/main/java/com/petshop/backend/controller/AppointmentController.java b/src/main/java/com/petshop/backend/controller/AppointmentController.java index b607a577..fb9fb1c6 100644 --- a/src/main/java/com/petshop/backend/controller/AppointmentController.java +++ b/src/main/java/com/petshop/backend/controller/AppointmentController.java @@ -17,7 +17,6 @@ import java.util.List; @RestController @RequestMapping("/api/v1/appointments") -@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class AppointmentController { private final AppointmentService appointmentService; @@ -27,6 +26,7 @@ public class AppointmentController { } @GetMapping + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity> getAllAppointments( @RequestParam(required = false) String q, Pageable pageable) { @@ -34,16 +34,19 @@ public class AppointmentController { } @GetMapping("/{id}") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity getAppointmentById(@PathVariable Long id) { return ResponseEntity.ok(appointmentService.getAppointmentById(id)); } @PostMapping + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity createAppointment(@Valid @RequestBody AppointmentRequest request) { return ResponseEntity.status(HttpStatus.CREATED).body(appointmentService.createAppointment(request)); } @PutMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity updateAppointment( @PathVariable Long id, @Valid @RequestBody AppointmentRequest request) { @@ -51,12 +54,14 @@ public class AppointmentController { } @DeleteMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity deleteAppointment(@PathVariable Long id) { appointmentService.deleteAppointment(id); return ResponseEntity.noContent().build(); } @DeleteMapping + @PreAuthorize("hasRole('ADMIN')") public ResponseEntity bulkDeleteAppointments(@Valid @RequestBody BulkDeleteRequest request) { appointmentService.bulkDeleteAppointments(request); return ResponseEntity.noContent().build(); diff --git a/src/main/java/com/petshop/backend/controller/CategoryController.java b/src/main/java/com/petshop/backend/controller/CategoryController.java index 5e1b80c4..fb938dd9 100644 --- a/src/main/java/com/petshop/backend/controller/CategoryController.java +++ b/src/main/java/com/petshop/backend/controller/CategoryController.java @@ -14,7 +14,6 @@ import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/categories") -@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class CategoryController { private final CategoryService categoryService; @@ -36,11 +35,13 @@ public class CategoryController { } @PostMapping + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity createCategory(@Valid @RequestBody CategoryRequest request) { return ResponseEntity.status(HttpStatus.CREATED).body(categoryService.createCategory(request)); } @PutMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity updateCategory( @PathVariable Long id, @Valid @RequestBody CategoryRequest request) { @@ -48,12 +49,14 @@ public class CategoryController { } @DeleteMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity deleteCategory(@PathVariable Long id) { categoryService.deleteCategory(id); return ResponseEntity.noContent().build(); } @DeleteMapping + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity bulkDeleteCategories(@Valid @RequestBody BulkDeleteRequest request) { categoryService.bulkDeleteCategories(request); return ResponseEntity.noContent().build(); diff --git a/src/main/java/com/petshop/backend/controller/ChatController.java b/src/main/java/com/petshop/backend/controller/ChatController.java new file mode 100644 index 00000000..181f1ab9 --- /dev/null +++ b/src/main/java/com/petshop/backend/controller/ChatController.java @@ -0,0 +1,80 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.chat.ConversationRequest; +import com.petshop.backend.dto.chat.ConversationResponse; +import com.petshop.backend.dto.chat.MessageRequest; +import com.petshop.backend.dto.chat.MessageResponse; +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.service.ChatService; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/chat") +public class ChatController { + + private final ChatService chatService; + private final UserRepository userRepository; + + public ChatController(ChatService chatService, UserRepository userRepository) { + this.chatService = chatService; + this.userRepository = userRepository; + } + + private User getCurrentUser() { + UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + return userRepository.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + } + + @PostMapping("/conversations") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity createConversation(@Valid @RequestBody ConversationRequest request) { + User user = getCurrentUser(); + ConversationResponse response = chatService.createConversation(user.getId(), request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @GetMapping("/conversations") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity> getConversations() { + User user = getCurrentUser(); + List conversations = chatService.getConversations(user.getId(), user.getRole()); + return ResponseEntity.ok(conversations); + } + + @GetMapping("/conversations/{id}") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity getConversation(@PathVariable Long id) { + User user = getCurrentUser(); + ConversationResponse conversation = chatService.getConversation(id, user.getId(), user.getRole()); + return ResponseEntity.ok(conversation); + } + + @PostMapping("/conversations/{id}/messages") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity sendMessage( + @PathVariable Long id, + @Valid @RequestBody MessageRequest request) { + User user = getCurrentUser(); + MessageResponse message = chatService.sendMessage(id, user.getId(), request); + return ResponseEntity.status(HttpStatus.CREATED).body(message); + } + + @GetMapping("/conversations/{id}/messages") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity> getMessages(@PathVariable Long id) { + User user = getCurrentUser(); + List messages = chatService.getMessages(id, user.getId(), user.getRole()); + return ResponseEntity.ok(messages); + } +} diff --git a/src/main/java/com/petshop/backend/controller/ServiceController.java b/src/main/java/com/petshop/backend/controller/ServiceController.java index a7160a62..4f6503dc 100644 --- a/src/main/java/com/petshop/backend/controller/ServiceController.java +++ b/src/main/java/com/petshop/backend/controller/ServiceController.java @@ -14,7 +14,6 @@ import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/services") -@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class ServiceController { private final ServiceService serviceService; @@ -36,11 +35,13 @@ public class ServiceController { } @PostMapping + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity createService(@Valid @RequestBody ServiceRequest request) { return ResponseEntity.status(HttpStatus.CREATED).body(serviceService.createService(request)); } @PutMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity updateService( @PathVariable Long id, @Valid @RequestBody ServiceRequest request) { @@ -48,12 +49,14 @@ public class ServiceController { } @DeleteMapping("/{id}") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity deleteService(@PathVariable Long id) { serviceService.deleteService(id); return ResponseEntity.noContent().build(); } @DeleteMapping + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity bulkDeleteServices(@Valid @RequestBody BulkDeleteRequest request) { serviceService.bulkDeleteServices(request); return ResponseEntity.noContent().build(); diff --git a/src/main/java/com/petshop/backend/dto/chat/ConversationRequest.java b/src/main/java/com/petshop/backend/dto/chat/ConversationRequest.java new file mode 100644 index 00000000..a6ef1db1 --- /dev/null +++ b/src/main/java/com/petshop/backend/dto/chat/ConversationRequest.java @@ -0,0 +1,23 @@ +package com.petshop.backend.dto.chat; + +import jakarta.validation.constraints.NotBlank; + +public class ConversationRequest { + @NotBlank(message = "Initial message is required") + private String message; + + public ConversationRequest() { + } + + public ConversationRequest(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/petshop/backend/dto/chat/ConversationResponse.java b/src/main/java/com/petshop/backend/dto/chat/ConversationResponse.java new file mode 100644 index 00000000..d9dbb1f8 --- /dev/null +++ b/src/main/java/com/petshop/backend/dto/chat/ConversationResponse.java @@ -0,0 +1,96 @@ +package com.petshop.backend.dto.chat; + +import com.petshop.backend.entity.Conversation; + +import java.time.LocalDateTime; + +public class ConversationResponse { + private Long id; + private Long customerId; + private Long staffId; + private String status; + private String lastMessage; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public ConversationResponse() { + } + + public ConversationResponse(Long id, Long customerId, Long staffId, String status, String lastMessage, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.customerId = customerId; + this.staffId = staffId; + this.status = status; + this.lastMessage = lastMessage; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public static ConversationResponse fromEntity(Conversation conversation, String lastMessage) { + ConversationResponse response = new ConversationResponse(); + response.setId(conversation.getId()); + response.setCustomerId(conversation.getCustomerId()); + response.setStaffId(conversation.getStaffId()); + response.setStatus(conversation.getStatus().name()); + response.setLastMessage(lastMessage); + response.setCreatedAt(conversation.getCreatedAt()); + response.setUpdatedAt(conversation.getUpdatedAt()); + return response; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public Long getStaffId() { + return staffId; + } + + public void setStaffId(Long staffId) { + this.staffId = staffId; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getLastMessage() { + return lastMessage; + } + + public void setLastMessage(String lastMessage) { + this.lastMessage = lastMessage; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java b/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java new file mode 100644 index 00000000..cb03d310 --- /dev/null +++ b/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java @@ -0,0 +1,23 @@ +package com.petshop.backend.dto.chat; + +import jakarta.validation.constraints.NotBlank; + +public class MessageRequest { + @NotBlank(message = "Message content is required") + private String content; + + public MessageRequest() { + } + + public MessageRequest(String content) { + this.content = content; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/src/main/java/com/petshop/backend/dto/chat/MessageResponse.java b/src/main/java/com/petshop/backend/dto/chat/MessageResponse.java new file mode 100644 index 00000000..25cffae5 --- /dev/null +++ b/src/main/java/com/petshop/backend/dto/chat/MessageResponse.java @@ -0,0 +1,85 @@ +package com.petshop.backend.dto.chat; + +import com.petshop.backend.entity.Message; + +import java.time.LocalDateTime; + +public class MessageResponse { + private Long id; + private Long conversationId; + private Long senderId; + private String content; + private LocalDateTime timestamp; + private Boolean isRead; + + public MessageResponse() { + } + + public MessageResponse(Long id, Long conversationId, Long senderId, String content, LocalDateTime timestamp, Boolean isRead) { + this.id = id; + this.conversationId = conversationId; + this.senderId = senderId; + this.content = content; + this.timestamp = timestamp; + this.isRead = isRead; + } + + public static MessageResponse fromEntity(Message message) { + MessageResponse response = new MessageResponse(); + response.setId(message.getId()); + response.setConversationId(message.getConversationId()); + response.setSenderId(message.getSenderId()); + response.setContent(message.getContent()); + response.setTimestamp(message.getTimestamp()); + response.setIsRead(message.getIsRead()); + return response; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getConversationId() { + return conversationId; + } + + public void setConversationId(Long conversationId) { + this.conversationId = conversationId; + } + + public Long getSenderId() { + return senderId; + } + + public void setSenderId(Long senderId) { + this.senderId = senderId; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public Boolean getIsRead() { + return isRead; + } + + public void setIsRead(Boolean isRead) { + this.isRead = isRead; + } +} diff --git a/src/main/java/com/petshop/backend/entity/Conversation.java b/src/main/java/com/petshop/backend/entity/Conversation.java new file mode 100644 index 00000000..1d8008e1 --- /dev/null +++ b/src/main/java/com/petshop/backend/entity/Conversation.java @@ -0,0 +1,98 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "conversation") +public class Conversation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long customerId; + + @Column + private Long staffId; + + @Enumerated(EnumType.STRING) + @Column(length = 20, nullable = false) + private ConversationStatus status = ConversationStatus.OPEN; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + public enum ConversationStatus { + OPEN, CLOSED + } + + public Conversation() { + } + + public Conversation(Long id, Long customerId, Long staffId, ConversationStatus status, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.customerId = customerId; + this.staffId = staffId; + this.status = status; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public Long getStaffId() { + return staffId; + } + + public void setStaffId(Long staffId) { + this.staffId = staffId; + } + + public ConversationStatus getStatus() { + return status; + } + + public void setStatus(ConversationStatus status) { + this.status = status; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/petshop/backend/entity/Message.java b/src/main/java/com/petshop/backend/entity/Message.java new file mode 100644 index 00000000..33777bf5 --- /dev/null +++ b/src/main/java/com/petshop/backend/entity/Message.java @@ -0,0 +1,91 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "message") +public class Message { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long conversationId; + + @Column(nullable = false) + private Long senderId; + + @Column(nullable = false, columnDefinition = "TEXT") + private String content; + + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime timestamp; + + @Column(nullable = false) + private Boolean isRead = false; + + public Message() { + } + + public Message(Long id, Long conversationId, Long senderId, String content, LocalDateTime timestamp, Boolean isRead) { + this.id = id; + this.conversationId = conversationId; + this.senderId = senderId; + this.content = content; + this.timestamp = timestamp; + this.isRead = isRead; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getConversationId() { + return conversationId; + } + + public void setConversationId(Long conversationId) { + this.conversationId = conversationId; + } + + public Long getSenderId() { + return senderId; + } + + public void setSenderId(Long senderId) { + this.senderId = senderId; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public Boolean getIsRead() { + return isRead; + } + + public void setIsRead(Boolean isRead) { + this.isRead = isRead; + } +} diff --git a/src/main/java/com/petshop/backend/repository/ConversationRepository.java b/src/main/java/com/petshop/backend/repository/ConversationRepository.java new file mode 100644 index 00000000..142853d6 --- /dev/null +++ b/src/main/java/com/petshop/backend/repository/ConversationRepository.java @@ -0,0 +1,13 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.Conversation; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ConversationRepository extends JpaRepository { + List findByCustomerId(Long customerId); + List findByStaffId(Long staffId); +} diff --git a/src/main/java/com/petshop/backend/repository/MessageRepository.java b/src/main/java/com/petshop/backend/repository/MessageRepository.java new file mode 100644 index 00000000..6c87be6e --- /dev/null +++ b/src/main/java/com/petshop/backend/repository/MessageRepository.java @@ -0,0 +1,12 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.Message; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface MessageRepository extends JpaRepository { + List findByConversationIdOrderByTimestampAsc(Long conversationId); +} diff --git a/src/main/java/com/petshop/backend/service/ChatService.java b/src/main/java/com/petshop/backend/service/ChatService.java new file mode 100644 index 00000000..e7d7ec00 --- /dev/null +++ b/src/main/java/com/petshop/backend/service/ChatService.java @@ -0,0 +1,126 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.chat.ConversationRequest; +import com.petshop.backend.dto.chat.ConversationResponse; +import com.petshop.backend.dto.chat.MessageRequest; +import com.petshop.backend.dto.chat.MessageResponse; +import com.petshop.backend.entity.Conversation; +import com.petshop.backend.entity.Message; +import com.petshop.backend.entity.User; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.ConversationRepository; +import com.petshop.backend.repository.MessageRepository; +import com.petshop.backend.repository.UserRepository; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class ChatService { + + private final ConversationRepository conversationRepository; + private final MessageRepository messageRepository; + private final UserRepository userRepository; + + public ChatService(ConversationRepository conversationRepository, + MessageRepository messageRepository, + UserRepository userRepository) { + this.conversationRepository = conversationRepository; + this.messageRepository = messageRepository; + this.userRepository = userRepository; + } + + @Transactional + public ConversationResponse createConversation(Long userId, ConversationRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("User not found")); + + Conversation conversation = new Conversation(); + conversation.setCustomerId(userId); + conversation.setStatus(Conversation.ConversationStatus.OPEN); + conversation = conversationRepository.save(conversation); + + Message message = new Message(); + message.setConversationId(conversation.getId()); + message.setSenderId(userId); + message.setContent(request.getMessage()); + message.setIsRead(false); + messageRepository.save(message); + + return ConversationResponse.fromEntity(conversation, request.getMessage()); + } + + public List getConversations(Long userId, User.Role role) { + List conversations; + + if (role == User.Role.CUSTOMER) { + conversations = conversationRepository.findByCustomerId(userId); + } else if (role == User.Role.STAFF) { + conversations = conversationRepository.findByStaffId(userId); + if (conversations.isEmpty()) { + conversations = conversationRepository.findAll(); + } + } else { + conversations = conversationRepository.findAll(); + } + + return conversations.stream() + .map(conv -> { + List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conv.getId()); + String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1).getContent(); + return ConversationResponse.fromEntity(conv, lastMessage); + }) + .collect(Collectors.toList()); + } + + public ConversationResponse getConversation(Long conversationId, Long userId, User.Role role) { + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); + + if (role == User.Role.CUSTOMER && !conversation.getCustomerId().equals(userId)) { + throw new AccessDeniedException("You can only view your own conversations"); + } + + List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); + String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1).getContent(); + + return ConversationResponse.fromEntity(conversation, lastMessage); + } + + @Transactional + public MessageResponse sendMessage(Long conversationId, Long userId, MessageRequest request) { + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); + + Message message = new Message(); + message.setConversationId(conversationId); + message.setSenderId(userId); + message.setContent(request.getContent()); + message.setIsRead(false); + message = messageRepository.save(message); + + if (conversation.getStaffId() == null && !userId.equals(conversation.getCustomerId())) { + conversation.setStaffId(userId); + conversationRepository.save(conversation); + } + + return MessageResponse.fromEntity(message); + } + + public List getMessages(Long conversationId, Long userId, User.Role role) { + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); + + if (role == User.Role.CUSTOMER && !conversation.getCustomerId().equals(userId)) { + throw new AccessDeniedException("You can only view messages from your own conversations"); + } + + List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); + return messages.stream() + .map(MessageResponse::fromEntity) + .collect(Collectors.toList()); + } +}