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());
+ }
+}