Merge pull request #2 from RecentRunner/customer

Fixes
This commit is contained in:
2026-03-09 01:25:43 -06:00
committed by GitHub
63 changed files with 5089 additions and 2339 deletions

4
.gitignore vendored
View File

@@ -34,3 +34,7 @@ build/
### Mac ###
.DS_Store
### Project Specific ###
tmp/
uploads/

View File

@@ -11,7 +11,8 @@ services:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql
- ./sql:/docker-entrypoint-initdb.d
- ./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

File diff suppressed because it is too large Load Diff

View File

@@ -43,6 +43,11 @@
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>

View File

@@ -2,6 +2,7 @@ package com.petshop.backend.config;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.service.UserBusinessLinkageService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@@ -11,28 +12,138 @@ public class DataInitializer implements CommandLineRunner {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final UserBusinessLinkageService userBusinessLinkageService;
public DataInitializer(UserRepository userRepository, PasswordEncoder passwordEncoder) {
public DataInitializer(UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.userBusinessLinkageService = userBusinessLinkageService;
}
@Override
public void run(String... args) {
if (userRepository.findByUsername("admin").isEmpty()) {
User admin = new User();
System.out.println("==== DataInitializer: Starting user creation ====");
User admin = userRepository.findByUsername("admin").orElse(null);
if (admin == null) {
System.out.println("Creating admin user...");
admin = new User();
admin.setUsername("admin");
admin.setPassword(passwordEncoder.encode("admin123"));
admin.setEmail("admin@petshop.com");
admin.setFullName("Admin User");
admin.setRole(User.Role.ADMIN);
userRepository.save(admin);
admin.setActive(true);
admin = userRepository.save(admin);
System.out.println("Admin user created successfully");
} else {
System.out.println("Admin user already exists");
// Normalize missing fields if needed
boolean updated = false;
if (admin.getFullName() == null || admin.getFullName().isEmpty()) {
admin.setFullName("Admin User");
updated = true;
}
if (admin.getEmail() == null || admin.getEmail().isEmpty()) {
admin.setEmail("admin@petshop.com");
updated = true;
}
if (admin.getActive() == null) {
admin.setActive(true);
updated = true;
}
if (admin.getRole() == null) {
admin.setRole(User.Role.ADMIN);
updated = true;
}
if (updated) {
admin = userRepository.save(admin);
System.out.println("Admin user normalized");
}
}
// Ensure linked employee
userBusinessLinkageService.ensureLinkedEmployee(admin);
if (userRepository.findByUsername("staff").isEmpty()) {
User staff = new User();
User staff = userRepository.findByUsername("staff").orElse(null);
if (staff == null) {
System.out.println("Creating staff user...");
staff = new User();
staff.setUsername("staff");
staff.setPassword(passwordEncoder.encode("staff123"));
staff.setEmail("staff@petshop.com");
staff.setFullName("Staff User");
staff.setRole(User.Role.STAFF);
userRepository.save(staff);
staff.setActive(true);
staff = userRepository.save(staff);
System.out.println("Staff user created successfully");
} else {
System.out.println("Staff user already exists");
// Normalize missing fields if needed
boolean updated = false;
if (staff.getFullName() == null || staff.getFullName().isEmpty()) {
staff.setFullName("Staff User");
updated = true;
}
if (staff.getEmail() == null || staff.getEmail().isEmpty()) {
staff.setEmail("staff@petshop.com");
updated = true;
}
if (staff.getActive() == null) {
staff.setActive(true);
updated = true;
}
if (staff.getRole() == null) {
staff.setRole(User.Role.STAFF);
updated = true;
}
if (updated) {
staff = userRepository.save(staff);
System.out.println("Staff user normalized");
}
}
// Ensure linked employee
userBusinessLinkageService.ensureLinkedEmployee(staff);
User customer = userRepository.findByUsername("customer").orElse(null);
if (customer == null) {
System.out.println("Creating customer user...");
customer = new User();
customer.setUsername("customer");
customer.setPassword(passwordEncoder.encode("customer123"));
customer.setEmail("customer@petshop.com");
customer.setFullName("Test Customer");
customer.setRole(User.Role.CUSTOMER);
customer.setActive(true);
customer = userRepository.save(customer);
System.out.println("Customer user created successfully");
} else {
System.out.println("Customer user already exists");
// Normalize missing fields if needed
boolean updated = false;
if (customer.getFullName() == null || customer.getFullName().isEmpty()) {
customer.setFullName("Test Customer");
updated = true;
}
if (customer.getEmail() == null || customer.getEmail().isEmpty()) {
customer.setEmail("customer@petshop.com");
updated = true;
}
if (customer.getActive() == null) {
customer.setActive(true);
updated = true;
}
if (customer.getRole() == null) {
customer.setRole(User.Role.CUSTOMER);
updated = true;
}
if (updated) {
customer = userRepository.save(customer);
System.out.println("Customer user normalized");
}
}
// Ensure linked customer
userBusinessLinkageService.ensureLinkedCustomer(customer);
System.out.println("==== DataInitializer: Completed ====");
}
}

View File

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

View File

@@ -3,12 +3,19 @@ package com.petshop.backend.controller;
import com.petshop.backend.dto.adoption.AdoptionRequest;
import com.petshop.backend.dto.adoption.AdoptionResponse;
import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.entity.Customer;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.service.AdoptionService;
import com.petshop.backend.util.AuthenticationHelper;
import jakarta.validation.Valid;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
@RestController
@@ -16,29 +23,61 @@ import org.springframework.web.bind.annotation.*;
public class AdoptionController {
private final AdoptionService adoptionService;
private final UserRepository userRepository;
private final CustomerRepository customerRepository;
public AdoptionController(AdoptionService adoptionService) {
public AdoptionController(AdoptionService adoptionService, UserRepository userRepository, CustomerRepository customerRepository) {
this.adoptionService = adoptionService;
this.userRepository = userRepository;
this.customerRepository = customerRepository;
}
@GetMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<Page<AdoptionResponse>> getAllAdoptions(
@RequestParam(required = false) String q,
Pageable pageable) {
return ResponseEntity.ok(adoptionService.getAllAdoptions(q, pageable));
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
Long customerId = null;
if (role != null && role.equals("CUSTOMER")) {
Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository);
customerId = customer.getCustomerId();
}
return ResponseEntity.ok(adoptionService.getAllAdoptions(q, pageable, customerId));
}
@GetMapping("/{id}")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<AdoptionResponse> getAdoptionById(@PathVariable Long id) {
return ResponseEntity.ok(adoptionService.getAdoptionById(id));
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
Long customerId = null;
if (role != null && role.equals("CUSTOMER")) {
Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository);
customerId = customer.getCustomerId();
}
return ResponseEntity.ok(adoptionService.getAdoptionById(id, customerId));
}
@PostMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<AdoptionResponse> createAdoption(@Valid @RequestBody AdoptionRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(adoptionService.createAdoption(request));
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<AdoptionResponse> updateAdoption(
@PathVariable Long id,
@Valid @RequestBody AdoptionRequest request) {
@@ -46,12 +85,14 @@ public class AdoptionController {
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<Void> deleteAdoption(@PathVariable Long id) {
adoptionService.deleteAdoption(id);
return ResponseEntity.noContent().build();
}
@DeleteMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> bulkDeleteAdoptions(@Valid @RequestBody BulkDeleteRequest request) {
adoptionService.bulkDeleteAdoptions(request);
return ResponseEntity.noContent().build();

View File

@@ -3,12 +3,19 @@ package com.petshop.backend.controller;
import com.petshop.backend.dto.appointment.AppointmentRequest;
import com.petshop.backend.dto.appointment.AppointmentResponse;
import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.entity.Customer;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.service.AppointmentService;
import com.petshop.backend.util.AuthenticationHelper;
import jakarta.validation.Valid;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
@@ -19,29 +26,61 @@ import java.util.List;
public class AppointmentController {
private final AppointmentService appointmentService;
private final UserRepository userRepository;
private final CustomerRepository customerRepository;
public AppointmentController(AppointmentService appointmentService) {
public AppointmentController(AppointmentService appointmentService, UserRepository userRepository, CustomerRepository customerRepository) {
this.appointmentService = appointmentService;
this.userRepository = userRepository;
this.customerRepository = customerRepository;
}
@GetMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<Page<AppointmentResponse>> getAllAppointments(
@RequestParam(required = false) String q,
Pageable pageable) {
return ResponseEntity.ok(appointmentService.getAllAppointments(q, pageable));
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
Long customerId = null;
if (role != null && role.equals("CUSTOMER")) {
Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository);
customerId = customer.getCustomerId();
}
return ResponseEntity.ok(appointmentService.getAllAppointments(q, pageable, customerId));
}
@GetMapping("/{id}")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<AppointmentResponse> getAppointmentById(@PathVariable Long id) {
return ResponseEntity.ok(appointmentService.getAppointmentById(id));
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
Long customerId = null;
if (role != null && role.equals("CUSTOMER")) {
Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository);
customerId = customer.getCustomerId();
}
return ResponseEntity.ok(appointmentService.getAppointmentById(id, customerId));
}
@PostMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<AppointmentResponse> createAppointment(@Valid @RequestBody AppointmentRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(appointmentService.createAppointment(request));
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<AppointmentResponse> updateAppointment(
@PathVariable Long id,
@Valid @RequestBody AppointmentRequest request) {
@@ -49,12 +88,14 @@ public class AppointmentController {
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<Void> deleteAppointment(@PathVariable Long id) {
appointmentService.deleteAppointment(id);
return ResponseEntity.noContent().build();
}
@DeleteMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> bulkDeleteAppointments(@Valid @RequestBody BulkDeleteRequest request) {
appointmentService.bulkDeleteAppointments(request);
return ResponseEntity.noContent().build();

View File

@@ -1,11 +1,16 @@
package com.petshop.backend.controller;
import com.petshop.backend.dto.auth.AvatarUploadResponse;
import com.petshop.backend.dto.auth.LoginRequest;
import com.petshop.backend.dto.auth.LoginResponse;
import com.petshop.backend.dto.auth.ProfileUpdateRequest;
import com.petshop.backend.dto.auth.RegisterRequest;
import com.petshop.backend.dto.auth.RegisterResponse;
import com.petshop.backend.dto.auth.UserInfoResponse;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.security.JwtUtil;
import com.petshop.backend.service.UserBusinessLinkageService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -18,9 +23,17 @@ import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/v1/auth")
@@ -30,12 +43,58 @@ public class AuthController {
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
private final PasswordEncoder passwordEncoder;
private final UserBusinessLinkageService userBusinessLinkageService;
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder) {
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) {
this.authenticationManager = authenticationManager;
this.userRepository = userRepository;
this.jwtUtil = jwtUtil;
this.passwordEncoder = passwordEncoder;
this.userBusinessLinkageService = userBusinessLinkageService;
}
@PostMapping("/register")
public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest request) {
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Username already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
if (userRepository.findByEmail(request.getEmail()).isPresent()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Email already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setEmail(request.getEmail());
user.setFullName(request.getFullName());
user.setRole(User.Role.CUSTOMER);
user.setActive(true);
User savedUser = userRepository.save(user);
// Create or link customer record
userBusinessLinkageService.ensureLinkedCustomer(savedUser);
UserDetails userDetails = new org.springframework.security.core.userdetails.User(
savedUser.getUsername(),
savedUser.getPassword(),
java.util.Collections.emptyList()
);
String token = jwtUtil.generateToken(userDetails);
return ResponseEntity.status(HttpStatus.CREATED).body(new RegisterResponse(
savedUser.getId(),
savedUser.getUsername(),
savedUser.getEmail(),
savedUser.getRole().name(),
token
));
}
@PostMapping("/login")
@@ -80,14 +139,162 @@ public class AuthController {
return ResponseEntity.ok(new UserInfoResponse(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getFullName(),
user.getAvatarUrl(),
user.getRole().name()
));
}
@PutMapping("/me")
public ResponseEntity<?> updateProfile(@Valid @RequestBody ProfileUpdateRequest request) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
if (request.getUsername() != null && !request.getUsername().equals(user.getUsername())) {
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Username already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
user.setUsername(request.getUsername());
}
if (request.getEmail() != null && !request.getEmail().equals(user.getEmail())) {
if (userRepository.findByEmail(request.getEmail()).isPresent()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Email already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
user.setEmail(request.getEmail());
}
if (request.getFullName() != null) {
user.setFullName(request.getFullName());
}
if (request.getPassword() != null && !request.getPassword().isEmpty()) {
user.setPassword(passwordEncoder.encode(request.getPassword()));
}
User updatedUser = userRepository.save(user);
return ResponseEntity.ok(new UserInfoResponse(
updatedUser.getId(),
updatedUser.getUsername(),
updatedUser.getEmail(),
updatedUser.getFullName(),
updatedUser.getAvatarUrl(),
updatedUser.getRole().name()
));
}
@PostMapping("/me/avatar")
public ResponseEntity<?> uploadAvatar(@RequestParam("avatar") MultipartFile file) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
if (file.isEmpty()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Please select a file to upload");
return ResponseEntity.badRequest().body(error);
}
if (file.getSize() > 5 * 1024 * 1024) {
Map<String, String> error = new HashMap<>();
error.put("message", "File size must not exceed 5MB");
return ResponseEntity.badRequest().body(error);
}
String contentType = file.getContentType();
if (contentType == null || (!contentType.equals("image/jpeg") && !contentType.equals("image/png") && !contentType.equals("image/gif"))) {
Map<String, String> error = new HashMap<>();
error.put("message", "Only JPG, PNG, and GIF images are allowed");
return ResponseEntity.badRequest().body(error);
}
try {
String uploadDir = "uploads/avatars";
File directory = new File(uploadDir);
if (!directory.exists()) {
directory.mkdirs();
}
String originalFilename = file.getOriginalFilename();
String extension = originalFilename != null && originalFilename.contains(".")
? originalFilename.substring(originalFilename.lastIndexOf("."))
: ".jpg";
String filename = UUID.randomUUID().toString() + extension;
Path filePath = Paths.get(uploadDir, filename);
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
String avatarUrl = "/uploads/avatars/" + filename;
user.setAvatarUrl(avatarUrl);
userRepository.save(user);
return ResponseEntity.ok(new AvatarUploadResponse(avatarUrl, "Avatar uploaded successfully"));
} catch (IOException e) {
Map<String, String> error = new HashMap<>();
error.put("message", "Failed to upload avatar: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
@GetMapping("/me/avatar")
public ResponseEntity<?> getAvatar() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
if (user.getAvatarUrl() == null || user.getAvatarUrl().isEmpty()) {
Map<String, String> error = new HashMap<>();
error.put("message", "No avatar uploaded");
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
Map<String, String> response = new HashMap<>();
response.put("avatarUrl", user.getAvatarUrl());
return ResponseEntity.ok(response);
}
@DeleteMapping("/me/avatar")
public ResponseEntity<?> deleteAvatar() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
if (user.getAvatarUrl() != null && !user.getAvatarUrl().isEmpty()) {
try {
Path filePath = Paths.get("." + user.getAvatarUrl());
Files.deleteIfExists(filePath);
} catch (IOException e) {
}
user.setAvatarUrl(null);
userRepository.save(user);
}
Map<String, String> response = new HashMap<>();
response.put("message", "Avatar deleted successfully");
return ResponseEntity.ok(response);
}
@PostMapping("/logout")
public ResponseEntity<?> logout() {
Map<String, String> response = new HashMap<>();
response.put("message", "Logged out successfully");
response.put("note", "Token remains valid until expiration. Clear token from client storage.");
return ResponseEntity.ok(response);
}
}

View File

@@ -9,6 +9,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@@ -34,11 +35,13 @@ public class CategoryController {
}
@PostMapping
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<CategoryResponse> createCategory(@Valid @RequestBody CategoryRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(categoryService.createCategory(request));
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<CategoryResponse> updateCategory(
@PathVariable Long id,
@Valid @RequestBody CategoryRequest request) {
@@ -46,12 +49,14 @@ public class CategoryController {
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<Void> deleteCategory(@PathVariable Long id) {
categoryService.deleteCategory(id);
return ResponseEntity.noContent().build();
}
@DeleteMapping
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<Void> bulkDeleteCategories(@Valid @RequestBody BulkDeleteRequest request) {
categoryService.bulkDeleteCategories(request);
return ResponseEntity.noContent().build();

View File

@@ -0,0 +1,83 @@
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.CustomerRepository;
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;
private final CustomerRepository customerRepository;
public ChatController(ChatService chatService, UserRepository userRepository, CustomerRepository customerRepository) {
this.chatService = chatService;
this.userRepository = userRepository;
this.customerRepository = customerRepository;
}
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<ConversationResponse> 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<List<ConversationResponse>> getConversations() {
User user = getCurrentUser();
List<ConversationResponse> conversations = chatService.getConversations(user.getId(), user.getRole());
return ResponseEntity.ok(conversations);
}
@GetMapping("/conversations/{id}")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<ConversationResponse> 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<MessageResponse> sendMessage(
@PathVariable Long id,
@Valid @RequestBody MessageRequest request) {
User user = getCurrentUser();
MessageResponse message = chatService.sendMessage(id, user.getId(), user.getRole(), request);
return ResponseEntity.status(HttpStatus.CREATED).body(message);
}
@GetMapping("/conversations/{id}/messages")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<List<MessageResponse>> getMessages(@PathVariable Long id) {
User user = getCurrentUser();
List<MessageResponse> messages = chatService.getMessages(id, user.getId(), user.getRole());
return ResponseEntity.ok(messages);
}
}

View File

@@ -9,10 +9,12 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/customers")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public class CustomerController {
private final CustomerService customerService;

View File

@@ -0,0 +1,18 @@
package com.petshop.backend.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/health")
public class HealthController {
@GetMapping
public ResponseEntity<Map<String, String>> healthCheck() {
return ResponseEntity.ok(Map.of("status", "UP"));
}
}

View File

@@ -9,6 +9,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@@ -34,11 +35,13 @@ public class PetController {
}
@PostMapping
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<PetResponse> createPet(@Valid @RequestBody PetRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(petService.createPet(request));
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<PetResponse> updatePet(
@PathVariable Long id,
@Valid @RequestBody PetRequest request) {
@@ -46,12 +49,14 @@ public class PetController {
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<Void> deletePet(@PathVariable Long id) {
petService.deletePet(id);
return ResponseEntity.noContent().build();
}
@DeleteMapping
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<Void> bulkDeletePets(@Valid @RequestBody BulkDeleteRequest request) {
petService.bulkDeletePets(request);
return ResponseEntity.noContent().build();

View File

@@ -9,6 +9,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@@ -34,11 +35,13 @@ public class ProductController {
}
@PostMapping
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<ProductResponse> createProduct(@Valid @RequestBody ProductRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(productService.createProduct(request));
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<ProductResponse> updateProduct(
@PathVariable Long id,
@Valid @RequestBody ProductRequest request) {
@@ -46,12 +49,14 @@ public class ProductController {
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
@DeleteMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> bulkDeleteProducts(@Valid @RequestBody BulkDeleteRequest request) {
productService.bulkDeleteProducts(request);
return ResponseEntity.noContent().build();

View File

@@ -0,0 +1,133 @@
package com.petshop.backend.controller;
import com.petshop.backend.dto.refund.RefundRequest;
import com.petshop.backend.dto.refund.RefundResponse;
import com.petshop.backend.dto.refund.RefundUpdateRequest;
import com.petshop.backend.entity.Customer;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.service.RefundService;
import com.petshop.backend.util.AuthenticationHelper;
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.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/refunds")
public class RefundController {
private final RefundService refundService;
private final UserRepository userRepository;
private final CustomerRepository customerRepository;
public RefundController(RefundService refundService, UserRepository userRepository, CustomerRepository customerRepository) {
this.refundService = refundService;
this.userRepository = userRepository;
this.customerRepository = customerRepository;
}
@PostMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<?> createRefund(@Valid @RequestBody RefundRequest request) {
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
Long customerId = null;
if (role != null && role.equals("CUSTOMER")) {
Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository);
customerId = customer.getCustomerId();
}
RefundResponse refund = refundService.createRefund(request, customerId);
return ResponseEntity.status(HttpStatus.CREATED).body(refund);
} catch (RuntimeException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.badRequest().body(error);
}
}
@GetMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<List<RefundResponse>> getAllRefunds() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
Long customerId = null;
if (role != null && role.equals("CUSTOMER")) {
Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository);
customerId = customer.getCustomerId();
}
List<RefundResponse> refunds = refundService.getAllRefunds(customerId);
return ResponseEntity.ok(refunds);
}
@GetMapping("/{id}")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<?> getRefundById(@PathVariable Long id) {
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
Long customerId = null;
if (role != null && role.equals("CUSTOMER")) {
Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository);
customerId = customer.getCustomerId();
}
RefundResponse refund = refundService.getRefundById(id, customerId);
return ResponseEntity.ok(refund);
} catch (RuntimeException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<?> updateRefund(@PathVariable Long id, @Valid @RequestBody RefundUpdateRequest request) {
try {
RefundResponse refund = refundService.updateRefundStatus(id, request.getStatus());
return ResponseEntity.ok(refund);
} catch (RuntimeException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.badRequest().body(error);
}
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> deleteRefund(@PathVariable Long id) {
try {
refundService.deleteRefund(id);
Map<String, String> response = new HashMap<>();
response.put("message", "Refund deleted successfully");
return ResponseEntity.ok(response);
} catch (RuntimeException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
}

View File

@@ -8,6 +8,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@@ -33,6 +34,7 @@ public class SaleController {
}
@PostMapping
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<SaleResponse> createSale(@Valid @RequestBody SaleRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(saleService.createSale(request));
}

View File

@@ -9,6 +9,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@@ -34,11 +35,13 @@ public class ServiceController {
}
@PostMapping
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<ServiceResponse> createService(@Valid @RequestBody ServiceRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(serviceService.createService(request));
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<ServiceResponse> updateService(
@PathVariable Long id,
@Valid @RequestBody ServiceRequest request) {
@@ -46,12 +49,14 @@ public class ServiceController {
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<Void> deleteService(@PathVariable Long id) {
serviceService.deleteService(id);
return ResponseEntity.noContent().build();
}
@DeleteMapping
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<Void> bulkDeleteServices(@Valid @RequestBody BulkDeleteRequest request) {
serviceService.bulkDeleteServices(request);
return ResponseEntity.noContent().build();

View File

@@ -1,14 +1,20 @@
package com.petshop.backend.controller;
import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.dto.store.StoreRequest;
import com.petshop.backend.dto.store.StoreResponse;
import com.petshop.backend.service.StoreService;
import jakarta.validation.Valid;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/stores")
@PreAuthorize("hasRole('ADMIN')")
public class StoreController {
private final StoreService storeService;
@@ -23,4 +29,33 @@ public class StoreController {
Pageable pageable) {
return ResponseEntity.ok(storeService.getAllStores(q, pageable));
}
@GetMapping("/{id}")
public ResponseEntity<StoreResponse> getStoreById(@PathVariable Long id) {
return ResponseEntity.ok(storeService.getStoreById(id));
}
@PostMapping
public ResponseEntity<StoreResponse> createStore(@Valid @RequestBody StoreRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(storeService.createStore(request));
}
@PutMapping("/{id}")
public ResponseEntity<StoreResponse> updateStore(
@PathVariable Long id,
@Valid @RequestBody StoreRequest request) {
return ResponseEntity.ok(storeService.updateStore(id, request));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteStore(@PathVariable Long id) {
storeService.deleteStore(id);
return ResponseEntity.noContent().build();
}
@DeleteMapping
public ResponseEntity<Void> bulkDeleteStores(@Valid @RequestBody BulkDeleteRequest request) {
storeService.bulkDeleteStores(request);
return ResponseEntity.noContent().build();
}
}

View File

@@ -0,0 +1,54 @@
package com.petshop.backend.dto.auth;
import java.util.Objects;
public class AvatarUploadResponse {
private String avatarUrl;
private String message;
public AvatarUploadResponse() {
}
public AvatarUploadResponse(String avatarUrl, String message) {
this.avatarUrl = avatarUrl;
this.message = message;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AvatarUploadResponse that = (AvatarUploadResponse) o;
return Objects.equals(avatarUrl, that.avatarUrl) &&
Objects.equals(message, that.message);
}
@Override
public int hashCode() {
return Objects.hash(avatarUrl, message);
}
@Override
public String toString() {
return "AvatarUploadResponse{" +
"avatarUrl='" + avatarUrl + '\'' +
", message='" + message + '\'' +
'}';
}
}

View File

@@ -0,0 +1,77 @@
package com.petshop.backend.dto.auth;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Size;
import java.util.Objects;
public class ProfileUpdateRequest {
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
private String username;
@Email(message = "Email must be valid")
private String email;
@Size(max = 100, message = "Full name must not exceed 100 characters")
private String fullName;
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ProfileUpdateRequest that = (ProfileUpdateRequest) o;
return Objects.equals(username, that.username) &&
Objects.equals(email, that.email) &&
Objects.equals(fullName, that.fullName) &&
Objects.equals(password, that.password);
}
@Override
public int hashCode() {
return Objects.hash(username, email, fullName, password);
}
@Override
public String toString() {
return "ProfileUpdateRequest{" +
"username='" + username + '\'' +
", email='" + email + '\'' +
", fullName='" + fullName + '\'' +
", password='" + password + '\'' +
'}';
}
}

View File

@@ -0,0 +1,82 @@
package com.petshop.backend.dto.auth;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.util.Objects;
public class RegisterRequest {
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
private String username;
@NotBlank(message = "Password is required")
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;
@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
private String email;
@NotBlank(message = "Full name is required")
@Size(max = 100, message = "Full name must not exceed 100 characters")
private String fullName;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RegisterRequest that = (RegisterRequest) o;
return Objects.equals(username, that.username) &&
Objects.equals(password, that.password) &&
Objects.equals(email, that.email) &&
Objects.equals(fullName, that.fullName);
}
@Override
public int hashCode() {
return Objects.hash(username, password, email, fullName);
}
@Override
public String toString() {
return "RegisterRequest{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", email='" + email + '\'' +
", fullName='" + fullName + '\'' +
'}';
}
}

View File

@@ -0,0 +1,90 @@
package com.petshop.backend.dto.auth;
import java.util.Objects;
public class RegisterResponse {
private Long id;
private String username;
private String email;
private String role;
private String token;
public RegisterResponse() {
}
public RegisterResponse(Long id, String username, String email, String role, String token) {
this.id = id;
this.username = username;
this.email = email;
this.role = role;
this.token = token;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RegisterResponse that = (RegisterResponse) o;
return Objects.equals(id, that.id) &&
Objects.equals(username, that.username) &&
Objects.equals(email, that.email) &&
Objects.equals(role, that.role) &&
Objects.equals(token, that.token);
}
@Override
public int hashCode() {
return Objects.hash(id, username, email, role, token);
}
@Override
public String toString() {
return "RegisterResponse{" +
"id=" + id +
", username='" + username + '\'' +
", email='" + email + '\'' +
", role='" + role + '\'' +
", token='" + token + '\'' +
'}';
}
}

View File

@@ -5,14 +5,20 @@ import java.util.Objects;
public class UserInfoResponse {
private Long id;
private String username;
private String email;
private String fullName;
private String avatarUrl;
private String role;
public UserInfoResponse() {
}
public UserInfoResponse(Long id, String username, String role) {
public UserInfoResponse(Long id, String username, String email, String fullName, String avatarUrl, String role) {
this.id = id;
this.username = username;
this.email = email;
this.fullName = fullName;
this.avatarUrl = avatarUrl;
this.role = role;
}
@@ -32,6 +38,30 @@ public class UserInfoResponse {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public String getRole() {
return role;
}
@@ -45,12 +75,17 @@ public class UserInfoResponse {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserInfoResponse that = (UserInfoResponse) o;
return Objects.equals(id, that.id) && Objects.equals(username, that.username) && Objects.equals(role, that.role);
return Objects.equals(id, that.id) &&
Objects.equals(username, that.username) &&
Objects.equals(email, that.email) &&
Objects.equals(fullName, that.fullName) &&
Objects.equals(avatarUrl, that.avatarUrl) &&
Objects.equals(role, that.role);
}
@Override
public int hashCode() {
return Objects.hash(id, username, role);
return Objects.hash(id, username, email, fullName, avatarUrl, role);
}
@Override
@@ -58,6 +93,9 @@ public class UserInfoResponse {
return "UserInfoResponse{" +
"id=" + id +
", username='" + username + '\'' +
", email='" + email + '\'' +
", fullName='" + fullName + '\'' +
", avatarUrl='" + avatarUrl + '\'' +
", role='" + role + '\'' +
'}';
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
package com.petshop.backend.dto.refund;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.Objects;
public class RefundRequest {
@NotNull(message = "Sale ID is required")
private Long saleId;
@NotBlank(message = "Reason is required")
private String reason;
public Long getSaleId() {
return saleId;
}
public void setSaleId(Long saleId) {
this.saleId = saleId;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RefundRequest that = (RefundRequest) o;
return Objects.equals(saleId, that.saleId) &&
Objects.equals(reason, that.reason);
}
@Override
public int hashCode() {
return Objects.hash(saleId, reason);
}
@Override
public String toString() {
return "RefundRequest{" +
"saleId=" + saleId +
", reason='" + reason + '\'' +
'}';
}
}

View File

@@ -0,0 +1,128 @@
package com.petshop.backend.dto.refund;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Objects;
public class RefundResponse {
private Long id;
private Long saleId;
private Long customerId;
private BigDecimal amount;
private String reason;
private String status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public RefundResponse() {
}
public RefundResponse(Long id, Long saleId, Long customerId, BigDecimal amount, String reason, String status, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.id = id;
this.saleId = saleId;
this.customerId = customerId;
this.amount = amount;
this.reason = reason;
this.status = status;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getSaleId() {
return saleId;
}
public void setSaleId(Long saleId) {
this.saleId = saleId;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
public String getStatus() {
return status;
}
public void setStatus(String 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;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RefundResponse that = (RefundResponse) o;
return Objects.equals(id, that.id) &&
Objects.equals(saleId, that.saleId) &&
Objects.equals(customerId, that.customerId) &&
Objects.equals(amount, that.amount) &&
Objects.equals(reason, that.reason) &&
Objects.equals(status, that.status) &&
Objects.equals(createdAt, that.createdAt) &&
Objects.equals(updatedAt, that.updatedAt);
}
@Override
public int hashCode() {
return Objects.hash(id, saleId, customerId, amount, reason, status, createdAt, updatedAt);
}
@Override
public String toString() {
return "RefundResponse{" +
"id=" + id +
", saleId=" + saleId +
", customerId=" + customerId +
", amount=" + amount +
", reason='" + reason + '\'' +
", status='" + status + '\'' +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
'}';
}
}

View File

@@ -0,0 +1,37 @@
package com.petshop.backend.dto.refund;
import jakarta.validation.constraints.NotBlank;
import java.util.Objects;
public class RefundUpdateRequest {
@NotBlank(message = "Status is required")
private String status;
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RefundUpdateRequest that = (RefundUpdateRequest) o;
return Objects.equals(status, that.status);
}
@Override
public int hashCode() {
return Objects.hash(status);
}
@Override
public String toString() {
return "RefundUpdateRequest{" +
"status='" + status + '\'' +
'}';
}
}

View File

@@ -20,6 +20,8 @@ public class SaleRequest {
private Long originalSaleId;
private Long customerId;
public Long getStoreId() {
return storeId;
}
@@ -60,6 +62,14 @@ public class SaleRequest {
this.originalSaleId = originalSaleId;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -69,12 +79,13 @@ public class SaleRequest {
Objects.equals(paymentMethod, that.paymentMethod) &&
Objects.equals(items, that.items) &&
Objects.equals(isRefund, that.isRefund) &&
Objects.equals(originalSaleId, that.originalSaleId);
Objects.equals(originalSaleId, that.originalSaleId) &&
Objects.equals(customerId, that.customerId);
}
@Override
public int hashCode() {
return Objects.hash(storeId, paymentMethod, items, isRefund, originalSaleId);
return Objects.hash(storeId, paymentMethod, items, isRefund, originalSaleId, customerId);
}
@Override
@@ -85,6 +96,7 @@ public class SaleRequest {
", items=" + items +
", isRefund=" + isRefund +
", originalSaleId=" + originalSaleId +
", customerId=" + customerId +
'}';
}
}

View File

@@ -0,0 +1,78 @@
package com.petshop.backend.dto.store;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import java.util.Objects;
public class StoreRequest {
@NotBlank(message = "Store name is required")
private String storeName;
@NotBlank(message = "Address is required")
private String address;
@NotBlank(message = "Phone is required")
private String phone;
@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
private String email;
public String getStoreName() {
return storeName;
}
public void setStoreName(String storeName) {
this.storeName = storeName;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StoreRequest that = (StoreRequest) o;
return Objects.equals(storeName, that.storeName) &&
Objects.equals(address, that.address) &&
Objects.equals(phone, that.phone) &&
Objects.equals(email, that.email);
}
@Override
public int hashCode() {
return Objects.hash(storeName, address, phone, email);
}
@Override
public String toString() {
return "StoreRequest{" +
"storeName='" + storeName + '\'' +
", address='" + address + '\'' +
", phone='" + phone + '\'' +
", email='" + email + '\'' +
'}';
}
}

View File

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

View File

@@ -15,6 +15,9 @@ public class Customer {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long customerId;
@Column(name = "user_id")
private Long userId;
@Column(nullable = false, length = 50)
private String firstName;
@@ -38,8 +41,9 @@ public class Customer {
public Customer() {
}
public Customer(Long customerId, String firstName, String lastName, String email, String phone, LocalDateTime createdAt, LocalDateTime updatedAt) {
public Customer(Long customerId, Long userId, String firstName, String lastName, String email, String phone, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.customerId = customerId;
this.userId = userId;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
@@ -56,6 +60,14 @@ public class Customer {
this.customerId = customerId;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getFirstName() {
return firstName;
}
@@ -121,6 +133,7 @@ public class Customer {
public String toString() {
return "Customer{" +
"customerId=" + customerId +
", userId=" + userId +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +

View File

@@ -15,6 +15,9 @@ public class Employee {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long employeeId;
@Column(name = "user_id")
private Long userId;
@Column(nullable = false, length = 50)
private String firstName;
@@ -44,8 +47,9 @@ public class Employee {
public Employee() {
}
public Employee(Long employeeId, String firstName, String lastName, String email, String phone, String role, Boolean isActive, LocalDateTime createdAt, LocalDateTime updatedAt) {
public Employee(Long employeeId, Long userId, String firstName, String lastName, String email, String phone, String role, Boolean isActive, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.employeeId = employeeId;
this.userId = userId;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
@@ -64,6 +68,14 @@ public class Employee {
this.employeeId = employeeId;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getFirstName() {
return firstName;
}
@@ -145,6 +157,7 @@ public class Employee {
public String toString() {
return "Employee{" +
"employeeId=" + employeeId +
", userId=" + userId +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +

View File

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

View File

@@ -14,7 +14,8 @@ public class Pet {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long petId;
@Column(name = "petId")
private Long id;
@Column(nullable = false, length = 50)
private String petName;
@@ -45,8 +46,8 @@ public class Pet {
public Pet() {
}
public Pet(Long petId, String petName, String petSpecies, String petBreed, Integer petAge, String petStatus, BigDecimal petPrice, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.petId = petId;
public Pet(Long id, String petName, String petSpecies, String petBreed, Integer petAge, String petStatus, BigDecimal petPrice, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.id = id;
this.petName = petName;
this.petSpecies = petSpecies;
this.petBreed = petBreed;
@@ -58,11 +59,11 @@ public class Pet {
}
public Long getPetId() {
return petId;
return id;
}
public void setPetId(Long petId) {
this.petId = petId;
public void setPetId(Long id) {
this.id = id;
}
public String getPetName() {
@@ -134,18 +135,18 @@ public class Pet {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Pet pet = (Pet) o;
return Objects.equals(petId, pet.petId);
return Objects.equals(id, pet.id);
}
@Override
public int hashCode() {
return Objects.hash(petId);
return Objects.hash(id);
}
@Override
public String toString() {
return "Pet{" +
"petId=" + petId +
"id=" + id +
", petName='" + petName + '\'' +
", petSpecies='" + petSpecies + '\'' +
", petBreed='" + petBreed + '\'' +

View File

@@ -0,0 +1,151 @@
package com.petshop.backend.entity;
import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Objects;
@Entity
@Table(name = "refund")
public class Refund {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private Long saleId;
@Column(nullable = false)
private Long customerId;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal amount;
@Column(nullable = false, length = 500)
private String reason;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private RefundStatus status;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
public enum RefundStatus {
PENDING, APPROVED, REJECTED
}
public Refund() {
}
public Refund(Long id, Long saleId, Long customerId, BigDecimal amount, String reason, RefundStatus status, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.id = id;
this.saleId = saleId;
this.customerId = customerId;
this.amount = amount;
this.reason = reason;
this.status = status;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getSaleId() {
return saleId;
}
public void setSaleId(Long saleId) {
this.saleId = saleId;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
public RefundStatus getStatus() {
return status;
}
public void setStatus(RefundStatus 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;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Refund refund = (Refund) o;
return Objects.equals(id, refund.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return "Refund{" +
"id=" + id +
", saleId=" + saleId +
", customerId=" + customerId +
", amount=" + amount +
", reason='" + reason + '\'' +
", status=" + status +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
'}';
}
}

View File

@@ -29,6 +29,10 @@ public class Sale {
@JoinColumn(name = "storeId", nullable = false)
private StoreLocation store;
@ManyToOne
@JoinColumn(name = "customerId")
private Customer customer;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal totalAmount;
@@ -56,11 +60,12 @@ public class Sale {
public Sale() {
}
public Sale(Long saleId, LocalDateTime saleDate, Employee employee, StoreLocation store, BigDecimal totalAmount, String paymentMethod, Boolean isRefund, Sale originalSale, List<SaleItem> items, LocalDateTime createdAt, LocalDateTime updatedAt) {
public Sale(Long saleId, LocalDateTime saleDate, Employee employee, StoreLocation store, Customer customer, BigDecimal totalAmount, String paymentMethod, Boolean isRefund, Sale originalSale, List<SaleItem> items, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.saleId = saleId;
this.saleDate = saleDate;
this.employee = employee;
this.store = store;
this.customer = customer;
this.totalAmount = totalAmount;
this.paymentMethod = paymentMethod;
this.isRefund = isRefund;
@@ -102,6 +107,14 @@ public class Sale {
this.store = store;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public BigDecimal getTotalAmount() {
return totalAmount;
}
@@ -178,6 +191,7 @@ public class Sale {
", saleDate=" + saleDate +
", employee=" + employee +
", store=" + store +
", customer=" + customer +
", totalAmount=" + totalAmount +
", paymentMethod='" + paymentMethod + '\'' +
", isRefund=" + isRefund +

View File

@@ -21,10 +21,22 @@ public class User {
@Column(nullable = false)
private String password;
@Column(unique = true, length = 100)
private String email;
@Column(length = 100)
private String fullName;
@Column(length = 255)
private String avatarUrl;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20, columnDefinition = "VARCHAR(20)")
private Role role;
@Column(nullable = false)
private Boolean active = true;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@@ -34,17 +46,21 @@ public class User {
private LocalDateTime updatedAt;
public enum Role {
STAFF, ADMIN
CUSTOMER, STAFF, ADMIN
}
public User() {
}
public User(Long id, String username, String password, Role role, LocalDateTime createdAt, LocalDateTime updatedAt) {
public User(Long id, String username, String password, String email, String fullName, String avatarUrl, Role role, Boolean active, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.id = id;
this.username = username;
this.password = password;
this.email = email;
this.fullName = fullName;
this.avatarUrl = avatarUrl;
this.role = role;
this.active = active;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
@@ -73,6 +89,30 @@ public class User {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public Role getRole() {
return role;
}
@@ -81,6 +121,14 @@ public class User {
this.role = role;
}
public Boolean getActive() {
return active;
}
public void setActive(Boolean active) {
this.active = active;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
@@ -116,7 +164,11 @@ public class User {
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", email='" + email + '\'' +
", fullName='" + fullName + '\'' +
", avatarUrl='" + avatarUrl + '\'' +
", role=" + role +
", active=" + active +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
'}';

View File

@@ -16,4 +16,12 @@ public interface AdoptionRepository extends JpaRepository<Adoption, Long> {
"LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(a.pet.petName) LIKE LOWER(CONCAT('%', :q, '%'))")
Page<Adoption> searchAdoptions(@Param("q") String query, Pageable pageable);
Page<Adoption> findByCustomerCustomerId(Long customerId, Pageable pageable);
@Query("SELECT a FROM Adoption a WHERE a.customer.customerId = :customerId AND (" +
"LOWER(a.customer.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(a.pet.petName) LIKE LOWER(CONCAT('%', :q, '%')))")
Page<Adoption> searchAdoptionsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable);
}

View File

@@ -27,4 +27,13 @@ public interface AppointmentRepository extends JpaRepository<Appointment, Long>
"LOWER(a.service.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%'))")
Page<Appointment> searchAppointments(@Param("q") String query, Pageable pageable);
Page<Appointment> findByCustomerCustomerId(Long customerId, Pageable pageable);
@Query("SELECT DISTINCT a FROM Appointment a LEFT JOIN a.pets p WHERE a.customer.customerId = :customerId AND (" +
"LOWER(a.customer.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(a.service.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')))")
Page<Appointment> searchAppointmentsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable);
}

View File

@@ -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<Conversation, Long> {
List<Conversation> findByCustomerId(Long customerId);
List<Conversation> findByStaffId(Long staffId);
}

View File

@@ -8,9 +8,15 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {
Optional<Customer> findByUserId(Long userId);
List<Customer> findAllByEmail(String email);
@Query("SELECT c FROM Customer c WHERE " +
"LOWER(c.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(c.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +

View File

@@ -4,6 +4,11 @@ import com.petshop.backend.entity.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
Optional<Employee> findByUserId(Long userId);
List<Employee> findAllByEmail(String email);
}

View File

@@ -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<Message, Long> {
List<Message> findByConversationIdOrderByTimestampAsc(Long conversationId);
}

View File

@@ -0,0 +1,13 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.Refund;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface RefundRepository extends JpaRepository<Refund, Long> {
List<Refund> findByCustomerId(Long customerId);
List<Refund> findBySaleId(Long saleId);
}

View File

@@ -13,6 +13,7 @@ import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
boolean existsByUsername(String username);
@Query("SELECT u FROM User u WHERE " +

View File

@@ -36,15 +36,14 @@ public class SecurityConfig {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/login").permitAll()
.requestMatchers("/api/v1/auth/login", "/api/v1/auth/register").permitAll()
.requestMatchers("/api/v1/health").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/suppliers").hasRole("ADMIN")
.requestMatchers("/api/v1/inventory/**").hasRole("ADMIN")
.requestMatchers("/api/v1/suppliers/**").hasRole("ADMIN")
.requestMatchers("/api/v1/product-suppliers/**").hasRole("ADMIN")
.requestMatchers("/api/v1/purchase-orders/**").hasRole("ADMIN")
.requestMatchers("/api/v1/users/**").hasRole("ADMIN")
.requestMatchers("/api/v1/analytics/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/api/v1/pets/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/sales/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/services/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/categories/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

View File

@@ -28,19 +28,34 @@ public class AdoptionService {
this.customerRepository = customerRepository;
}
public Page<AdoptionResponse> getAllAdoptions(String query, Pageable pageable) {
public Page<AdoptionResponse> getAllAdoptions(String query, Pageable pageable, Long customerId) {
Page<Adoption> adoptions;
if (query != null && !query.trim().isEmpty()) {
adoptions = adoptionRepository.searchAdoptions(query, pageable);
if (customerId != null) {
if (query != null && !query.trim().isEmpty()) {
adoptions = adoptionRepository.searchAdoptionsByCustomer(customerId, query, pageable);
} else {
adoptions = adoptionRepository.findByCustomerCustomerId(customerId, pageable);
}
} else {
adoptions = adoptionRepository.findAll(pageable);
if (query != null && !query.trim().isEmpty()) {
adoptions = adoptionRepository.searchAdoptions(query, pageable);
} else {
adoptions = adoptionRepository.findAll(pageable);
}
}
return adoptions.map(this::mapToResponse);
}
public AdoptionResponse getAdoptionById(Long id) {
public AdoptionResponse getAdoptionById(Long id, Long customerId) {
Adoption adoption = adoptionRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Adoption not found with id: " + id));
if (customerId != null && !adoption.getCustomer().getCustomerId().equals(customerId)) {
throw new ResourceNotFoundException("You can only view your own adoptions");
}
return mapToResponse(adoption);
}

View File

@@ -39,19 +39,34 @@ public class AppointmentService {
this.petRepository = petRepository;
}
public Page<AppointmentResponse> getAllAppointments(String query, Pageable pageable) {
public Page<AppointmentResponse> getAllAppointments(String query, Pageable pageable, Long customerId) {
Page<Appointment> appointments;
if (query != null && !query.trim().isEmpty()) {
appointments = appointmentRepository.searchAppointments(query, pageable);
if (customerId != null) {
if (query != null && !query.trim().isEmpty()) {
appointments = appointmentRepository.searchAppointmentsByCustomer(customerId, query, pageable);
} else {
appointments = appointmentRepository.findByCustomerCustomerId(customerId, pageable);
}
} else {
appointments = appointmentRepository.findAll(pageable);
if (query != null && !query.trim().isEmpty()) {
appointments = appointmentRepository.searchAppointments(query, pageable);
} else {
appointments = appointmentRepository.findAll(pageable);
}
}
return appointments.map(this::mapToResponse);
}
public AppointmentResponse getAppointmentById(Long id) {
public AppointmentResponse getAppointmentById(Long id, Long customerId) {
Appointment appointment = appointmentRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + id));
if (customerId != null && !appointment.getCustomer().getCustomerId().equals(customerId)) {
throw new ResourceNotFoundException("You can only view your own appointments");
}
return mapToResponse(appointment);
}

View File

@@ -0,0 +1,144 @@
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.Customer;
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.CustomerRepository;
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;
private final CustomerRepository customerRepository;
public ChatService(ConversationRepository conversationRepository,
MessageRepository messageRepository,
UserRepository userRepository,
CustomerRepository customerRepository) {
this.conversationRepository = conversationRepository;
this.messageRepository = messageRepository;
this.userRepository = userRepository;
this.customerRepository = customerRepository;
}
@Transactional
public ConversationResponse createConversation(Long userId, ConversationRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
Customer customer = customerRepository.findByUserId(userId)
.orElseThrow(() -> new ResourceNotFoundException("Customer record not found for user"));
Conversation conversation = new Conversation();
conversation.setCustomerId(customer.getCustomerId());
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<ConversationResponse> getConversations(Long userId, User.Role role) {
List<Conversation> conversations;
if (role == User.Role.CUSTOMER) {
Customer customer = customerRepository.findByUserId(userId)
.orElseThrow(() -> new ResourceNotFoundException("Customer record not found for user"));
conversations = conversationRepository.findByCustomerId(customer.getCustomerId());
} 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<Message> 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) {
Customer customer = customerRepository.findByUserId(userId)
.orElseThrow(() -> new ResourceNotFoundException("Customer record not found for user"));
if (!conversation.getCustomerId().equals(customer.getCustomerId())) {
throw new AccessDeniedException("You can only view your own conversations");
}
}
List<Message> 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, User.Role role, 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 (role == User.Role.STAFF && conversation.getStaffId() == null) {
conversation.setStaffId(userId);
conversationRepository.save(conversation);
}
return MessageResponse.fromEntity(message);
}
public List<MessageResponse> getMessages(Long conversationId, Long userId, User.Role role) {
Conversation conversation = conversationRepository.findById(conversationId)
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
if (role == User.Role.CUSTOMER) {
Customer customer = customerRepository.findByUserId(userId)
.orElseThrow(() -> new ResourceNotFoundException("Customer record not found for user"));
if (!conversation.getCustomerId().equals(customer.getCustomerId())) {
throw new AccessDeniedException("You can only view messages from your own conversations");
}
}
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
return messages.stream()
.map(MessageResponse::fromEntity)
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,111 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.refund.RefundRequest;
import com.petshop.backend.dto.refund.RefundResponse;
import com.petshop.backend.entity.Refund;
import com.petshop.backend.entity.Sale;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.RefundRepository;
import com.petshop.backend.repository.SaleRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class RefundService {
private final RefundRepository refundRepository;
private final SaleRepository saleRepository;
public RefundService(RefundRepository refundRepository, SaleRepository saleRepository) {
this.refundRepository = refundRepository;
this.saleRepository = saleRepository;
}
@Transactional
public RefundResponse createRefund(RefundRequest request, Long customerId) {
Sale sale = saleRepository.findById(request.getSaleId())
.orElseThrow(() -> new RuntimeException("Sale not found"));
if (sale.getCustomer() == null) {
throw new RuntimeException("Sale has no associated customer");
}
if (customerId != null && !sale.getCustomer().getCustomerId().equals(customerId)) {
throw new RuntimeException("You can only create refunds for your own purchases");
}
Refund refund = new Refund();
refund.setSaleId(sale.getSaleId());
refund.setCustomerId(sale.getCustomer().getCustomerId());
refund.setAmount(sale.getTotalAmount());
refund.setReason(request.getReason());
refund.setStatus(Refund.RefundStatus.PENDING);
Refund savedRefund = refundRepository.save(refund);
return toResponse(savedRefund);
}
public RefundResponse getRefundById(Long id, Long customerId) {
Refund refund = refundRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Refund not found"));
if (customerId != null && !refund.getCustomerId().equals(customerId)) {
throw new RuntimeException("You can only view your own refunds");
}
return toResponse(refund);
}
public List<RefundResponse> getAllRefunds(Long customerId) {
List<Refund> refunds;
if (customerId != null) {
refunds = refundRepository.findByCustomerId(customerId);
} else {
refunds = refundRepository.findAll();
}
return refunds.stream()
.map(this::toResponse)
.collect(Collectors.toList());
}
@Transactional
public RefundResponse updateRefundStatus(Long id, String status) {
Refund refund = refundRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Refund not found"));
try {
refund.setStatus(Refund.RefundStatus.valueOf(status.toUpperCase()));
} catch (IllegalArgumentException e) {
throw new RuntimeException("Invalid status: " + status);
}
Refund updatedRefund = refundRepository.save(refund);
return toResponse(updatedRefund);
}
@Transactional
public void deleteRefund(Long id) {
if (!refundRepository.existsById(id)) {
throw new RuntimeException("Refund not found");
}
refundRepository.deleteById(id);
}
private RefundResponse toResponse(Refund refund) {
return new RefundResponse(
refund.getId(),
refund.getSaleId(),
refund.getCustomerId(),
refund.getAmount(),
refund.getReason(),
refund.getStatus().name(),
refund.getCreatedAt(),
refund.getUpdatedAt()
);
}
}

View File

@@ -6,6 +6,7 @@ import com.petshop.backend.entity.*;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.*;
import com.petshop.backend.util.AuthenticationHelper;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -25,13 +26,17 @@ public class SaleService {
private final StoreRepository storeRepository;
private final InventoryRepository inventoryRepository;
private final EmployeeRepository employeeRepository;
private final UserRepository userRepository;
private final CustomerRepository customerRepository;
public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, EmployeeRepository employeeRepository) {
public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, EmployeeRepository employeeRepository, UserRepository userRepository, CustomerRepository customerRepository) {
this.saleRepository = saleRepository;
this.productRepository = productRepository;
this.storeRepository = storeRepository;
this.inventoryRepository = inventoryRepository;
this.employeeRepository = employeeRepository;
this.userRepository = userRepository;
this.customerRepository = customerRepository;
}
public Page<SaleResponse> getAllSales(String query, Pageable pageable) {
@@ -52,9 +57,7 @@ public class SaleService {
@Transactional
public SaleResponse createSale(SaleRequest request) {
Employee employee = employeeRepository.findAll().stream()
.findFirst()
.orElseThrow(() -> new ResourceNotFoundException("No employees found"));
Employee employee = AuthenticationHelper.getAuthenticatedEmployee(userRepository, employeeRepository);
StoreLocation store = storeRepository.findById(request.getStoreId())
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId()));
@@ -66,6 +69,12 @@ public class SaleService {
sale.setPaymentMethod(request.getPaymentMethod());
sale.setIsRefund(request.getIsRefund() != null ? request.getIsRefund() : false);
if (request.getCustomerId() != null) {
Customer customer = customerRepository.findById(request.getCustomerId())
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));
sale.setCustomer(customer);
}
if (sale.getIsRefund() && request.getOriginalSaleId() != null) {
Sale originalSale = saleRepository.findById(request.getOriginalSaleId())
.orElseThrow(() -> new ResourceNotFoundException("Original sale not found with id: " + request.getOriginalSaleId()));

View File

@@ -1,11 +1,15 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.dto.store.StoreRequest;
import com.petshop.backend.dto.store.StoreResponse;
import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.StoreRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class StoreService {
@@ -26,6 +30,51 @@ public class StoreService {
return stores.map(this::mapToResponse);
}
public StoreResponse getStoreById(Long id) {
StoreLocation store = storeRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + id));
return mapToResponse(store);
}
@Transactional
public StoreResponse createStore(StoreRequest request) {
StoreLocation store = new StoreLocation();
store.setStoreName(request.getStoreName());
store.setAddress(request.getAddress());
store.setPhone(request.getPhone());
store.setEmail(request.getEmail());
store = storeRepository.save(store);
return mapToResponse(store);
}
@Transactional
public StoreResponse updateStore(Long id, StoreRequest request) {
StoreLocation store = storeRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + id));
store.setStoreName(request.getStoreName());
store.setAddress(request.getAddress());
store.setPhone(request.getPhone());
store.setEmail(request.getEmail());
store = storeRepository.save(store);
return mapToResponse(store);
}
@Transactional
public void deleteStore(Long id) {
if (!storeRepository.existsById(id)) {
throw new ResourceNotFoundException("Store not found with id: " + id);
}
storeRepository.deleteById(id);
}
@Transactional
public void bulkDeleteStores(BulkDeleteRequest request) {
storeRepository.deleteAllById(request.getIds());
}
private StoreResponse mapToResponse(StoreLocation store) {
return new StoreResponse(
store.getStoreId(),

View File

@@ -0,0 +1,138 @@
package com.petshop.backend.service;
import com.petshop.backend.entity.Customer;
import com.petshop.backend.entity.Employee;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.EmployeeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class UserBusinessLinkageService {
private final EmployeeRepository employeeRepository;
private final CustomerRepository customerRepository;
@Autowired
public UserBusinessLinkageService(EmployeeRepository employeeRepository, CustomerRepository customerRepository) {
this.employeeRepository = employeeRepository;
this.customerRepository = customerRepository;
}
@Transactional
public Employee ensureLinkedEmployee(User user) {
// Check if already linked
if (user.getId() != null) {
var existing = employeeRepository.findByUserId(user.getId());
if (existing.isPresent()) {
return existing.get();
}
}
// Check for email matches
List<Employee> emailMatches = employeeRepository.findAllByEmail(user.getEmail());
// If exactly one match exists and has no userId, link it
if (emailMatches.size() == 1) {
Employee employee = emailMatches.get(0);
if (employee.getUserId() == null) {
employee.setUserId(user.getId());
return employeeRepository.save(employee);
}
}
// Otherwise create a new linked Employee
Employee newEmployee = new Employee();
newEmployee.setUserId(user.getId());
newEmployee.setEmail(user.getEmail());
// Split fullName into firstName and lastName
String[] nameParts = splitFullName(user.getFullName());
newEmployee.setFirstName(nameParts[0]);
newEmployee.setLastName(nameParts[1]);
// Set required fields with deterministic values
newEmployee.setPhone("000-000-0000");
newEmployee.setIsActive(true);
// Map role based on user role
if (user.getRole() == User.Role.ADMIN) {
newEmployee.setRole("Manager");
} else if (user.getRole() == User.Role.STAFF) {
newEmployee.setRole("Staff");
} else {
newEmployee.setRole("Staff"); // fallback
}
return employeeRepository.save(newEmployee);
}
@Transactional
public Customer ensureLinkedCustomer(User user) {
// Check if already linked
if (user.getId() != null) {
var existing = customerRepository.findByUserId(user.getId());
if (existing.isPresent()) {
return existing.get();
}
}
// Check for email matches
List<Customer> emailMatches = customerRepository.findAllByEmail(user.getEmail());
// If exactly one match exists and has no userId, link it
if (emailMatches.size() == 1) {
Customer customer = emailMatches.get(0);
if (customer.getUserId() == null) {
customer.setUserId(user.getId());
return customerRepository.save(customer);
}
}
// Otherwise create a new linked Customer
Customer newCustomer = new Customer();
newCustomer.setUserId(user.getId());
newCustomer.setEmail(user.getEmail());
// Split fullName into firstName and lastName
String[] nameParts = splitFullName(user.getFullName());
newCustomer.setFirstName(nameParts[0]);
newCustomer.setLastName(nameParts[1]);
// Set required fields with deterministic values
newCustomer.setPhone("000-000-0001");
return customerRepository.save(newCustomer);
}
private String[] splitFullName(String fullName) {
if (fullName == null || fullName.trim().isEmpty()) {
return new String[]{"System", "User"};
}
String trimmed = fullName.trim();
int spaceIndex = trimmed.indexOf(' ');
if (spaceIndex == -1) {
// Single token
return new String[]{trimmed, "User"};
}
// Multiple tokens
String firstName = trimmed.substring(0, spaceIndex).trim();
String lastName = trimmed.substring(spaceIndex + 1).trim();
if (firstName.isEmpty()) {
firstName = "System";
}
if (lastName.isEmpty()) {
lastName = "User";
}
return new String[]{firstName, lastName};
}
}

View File

@@ -17,10 +17,12 @@ public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final UserBusinessLinkageService userBusinessLinkageService;
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.userBusinessLinkageService = userBusinessLinkageService;
}
public Page<UserResponse> getAllUsers(String query, Pageable pageable) {
@@ -44,9 +46,20 @@ public class UserService {
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setFullName(request.getFullName());
user.setEmail(request.getEmail());
user.setRole(request.getRole());
user.setActive(request.getActive() != null ? request.getActive() : true);
user = userRepository.save(user);
// Create or link business entity based on role
if (user.getRole() == User.Role.STAFF || user.getRole() == User.Role.ADMIN) {
userBusinessLinkageService.ensureLinkedEmployee(user);
} else if (user.getRole() == User.Role.CUSTOMER) {
userBusinessLinkageService.ensureLinkedCustomer(user);
}
return mapToResponse(user);
}
@@ -59,7 +72,10 @@ public class UserService {
if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) {
user.setPassword(passwordEncoder.encode(request.getPassword()));
}
user.setFullName(request.getFullName());
user.setEmail(request.getEmail());
user.setRole(request.getRole());
user.setActive(request.getActive() != null ? request.getActive() : true);
user = userRepository.save(user);
return mapToResponse(user);
@@ -82,7 +98,12 @@ public class UserService {
UserResponse response = new UserResponse();
response.setId(user.getId());
response.setUsername(user.getUsername());
response.setFullName(user.getFullName());
response.setEmail(user.getEmail());
response.setRole(user.getRole().toString());
response.setActive(user.getActive());
response.setCreatedAt(user.getCreatedAt());
response.setUpdatedAt(user.getUpdatedAt());
return response;
}
}

View File

@@ -0,0 +1,38 @@
package com.petshop.backend.util;
import com.petshop.backend.entity.Customer;
import com.petshop.backend.entity.Employee;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.EmployeeRepository;
import com.petshop.backend.repository.UserRepository;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
@Component
public class AuthenticationHelper {
public static User getAuthenticatedUser(UserRepository userRepository) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new RuntimeException("No authenticated user found");
}
String username = authentication.getName();
return userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found: " + username));
}
public static Employee getAuthenticatedEmployee(UserRepository userRepository, EmployeeRepository employeeRepository) {
User user = getAuthenticatedUser(userRepository);
return employeeRepository.findByUserId(user.getId())
.orElseThrow(() -> new RuntimeException("Employee record not found for user: " + user.getUsername()));
}
public static Customer getAuthenticatedCustomer(UserRepository userRepository, CustomerRepository customerRepository) {
User user = getAuthenticatedUser(userRepository);
return customerRepository.findByUserId(user.getId())
.orElseThrow(() -> new RuntimeException("Customer record not found for user: " + user.getUsername()));
}
}

View File

@@ -2,15 +2,25 @@ spring:
application:
name: petshop-backend
servlet:
multipart:
enabled: true
max-file-size: 5MB
max-request-size: 5MB
datasource:
url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC}
username: ${SPRING_DATASOURCE_USERNAME:petshop}
password: ${SPRING_DATASOURCE_PASSWORD:petshop}
driver-class-name: com.mysql.cj.jdbc.Driver
sql:
init:
mode: never
jpa:
hibernate:
ddl-auto: validate
ddl-auto: none
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: ${JPA_SHOW_SQL:false}

205
src/main/resources/data.sql Normal file
View File

@@ -0,0 +1,205 @@
-- Insert Sample Data
INSERT INTO storeLocation (storeName, address, phone, email)
VALUES
('Downtown Branch', '123 Main St', '123-456-7890', 'downtown@petshop.com'),
('North Branch', '456 North Ave', '987-654-3210', 'north@petshop.com'),
('West Side Store', '789 West Blvd', '555-123-4567', 'westside@petshop.com'),
('East End Shop', '321 East Road', '555-987-6543', 'eastend@petshop.com'),
('South Mall Location', '654 South Plaza', '555-246-8135', 'southmall@petshop.com');
INSERT INTO employee (firstName, lastName, email, phone, role, isActive)
VALUES
('John', 'Doe', 'john@petshop.com', '111-222-3333', 'Manager', TRUE),
('Sara', 'Smith', 'sara@petshop.com', '444-555-6666', 'Staff', TRUE),
('Michael', 'Johnson', 'michael@petshop.com', '222-333-4444', 'Groomer', TRUE),
('Lisa', 'Williams', 'lisa@petshop.com', '333-444-5555', 'Staff', TRUE),
('David', 'Brown', 'david@petshop.com', '555-666-7777', 'Veterinarian', TRUE),
('Emma', 'Davis', 'emma@petshop.com', '666-777-8888', 'Manager', FALSE);
INSERT INTO employeeStore (employeeId, storeId)
VALUES
(1, 1),
(2, 1),
(2, 2),
(3, 2),
(4, 3),
(5, 1),
(5, 4),
(6, 5);
INSERT INTO customer (firstName, lastName, email, phone)
VALUES
('Alex', 'Brown', 'alex@gmail.com', '777-888-9999'),
('Emily', 'Clark', 'emily@gmail.com', '666-555-4444'),
('James', 'Wilson', 'james@gmail.com', '888-999-0000'),
('Olivia', 'Martinez', 'olivia@gmail.com', '999-000-1111'),
('William', 'Anderson', 'william@gmail.com', '000-111-2222'),
('Sophia', 'Taylor', 'sophia@gmail.com', '111-222-3333');
INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice)
VALUES
('Buddy', 'Dog', 'Labrador', 2, 'Available', 500.00),
('Milo', 'Cat', 'Persian', 1, 'Available', 300.00),
('Charlie', 'Dog', 'Golden Retriever', 3, 'Available', 550.00),
('Luna', 'Cat', 'Siamese', 2, 'Adopted', 350.00),
('Max', 'Dog', 'Beagle', 1, 'Available', 450.00),
('Bella', 'Cat', 'Maine Coon', 4, 'Available', 400.00);
INSERT INTO adoption (petId, customerId, adoptionDate, adoptionStatus)
VALUES
(1, 1, '2026-01-15', 'Completed'),
(4, 3, '2026-01-20', 'Completed'),
(2, 2, '2026-01-25', 'Pending'),
(5, 4, '2026-02-01', 'Completed'),
(6, 5, '2026-02-02', 'Pending');
INSERT INTO supplier (supCompany, supContactFirstName, supContactLastName, supEmail, supPhone)
VALUES
('PetFood Inc', 'Robert', 'King', 'contact@petfood.com', '888-111-2222'),
('Toy World', 'Jennifer', 'Lee', 'sales@toyworld.com', '888-222-3333'),
('Pet Supplies Co', 'Kevin', 'White', 'info@petsupplies.com', '888-333-4444'),
('Animal Care Products', 'Nancy', 'Green', 'orders@animalcare.com', '888-444-5555'),
('Premium Pet Goods', 'Tom', 'Black', 'support@premiumpet.com', '888-555-6666');
INSERT INTO category (categoryName, categoryType)
VALUES
('Dog Food', 'Product'),
('Cat Toys', 'Product'),
('Bird Supplies', 'Product'),
('Aquarium', 'Product'),
('Small Animals', 'Product');
INSERT INTO product (prodName, prodPrice, categoryId, prodDesc)
VALUES
('Premium Dog Food', 50.00, 1, 'High quality dog food'),
('Cat Toy Ball', 10.00, 2, 'Colorful toy for cats'),
('Bird Cage Large', 120.00, 3, 'Spacious bird cage'),
('Fish Tank 20 Gallon', 80.00, 4, 'Complete aquarium kit'),
('Hamster Wheel', 15.00, 5, 'Exercise wheel for small pets'),
('Organic Dog Treats', 25.00, 1, 'Natural dog treats');
INSERT INTO productSupplier (supId, prodId, cost)
VALUES
(1, 1, 35.00),
(1, 2, 6.50),
(2, 2, 7.00),
(3, 3, 90.00),
(3, 4, 60.00),
(4, 5, 10.00),
(5, 6, 18.00),
(1, 6, 17.50);
INSERT INTO inventory (prodId, quantity)
VALUES
(1, 100),
(2, 200),
(3, 50),
(4, 30),
(5, 150),
(6, 75);
INSERT INTO service (serviceName, serviceDesc, serviceDuration, servicePrice)
VALUES
('Pet Grooming', 'Full grooming service', 60, 40.00),
('Nail Trimming', 'Quick nail trim', 15, 10.00),
('Bath and Brush', 'Bathing and brushing service', 45, 30.00),
('Veterinary Checkup', 'Complete health examination', 30, 75.00),
('Teeth Cleaning', 'Professional dental cleaning', 90, 100.00);
INSERT INTO appointment (serviceId, customerId, appointmentDate, appointmentTime, appointmentStatus)
VALUES
(1, 2, '2026-02-01', '10:30:00', 'Booked'),
(2, 1, '2026-02-03', '14:00:00', 'Booked'),
(3, 3, '2026-02-05', '09:00:00', 'Completed'),
(4, 4, '2026-02-07', '11:30:00', 'Booked'),
(5, 5, '2026-02-10', '15:00:00', 'Cancelled');
INSERT INTO appointmentPet (appointmentId, petId)
VALUES
(1, 2),
(2, 1),
(3, 3),
(4, 5),
(5, 6);
INSERT INTO sale (saleDate, totalAmount, paymentMethod, employeeId, storeId, customerId)
VALUES
('2026-01-05 09:15:00', 125.00, 'Card', 1, 1, 1),
('2026-01-08 11:30:00', 200.00, 'Card', 2, 1, 2),
('2026-01-12 14:20:00', 60.00, 'Cash', 3, 2, 3),
('2026-01-15 10:45:00', 150.00, 'Debit', 1, 1, 1),
('2026-01-18 16:30:00', 80.00, 'Card', 4, 3, 2),
('2026-01-22 13:15:00', 95.00, 'Cash', 2, 2, NULL),
('2026-01-25 15:40:00', 240.00, 'Card', 5, 4, 4),
('2026-01-28 10:30:00', 80.00, 'Cash', 1, 1, NULL),
('2026-02-01 09:00:00', 175.00, 'Card', 3, 3, 1),
('2026-02-03 11:20:00', 120.00, 'Card', 2, 1, 3),
('2026-02-05 14:50:00', 45.00, 'Cash', 4, 2, NULL),
('2026-02-08 16:15:00', 160.00, 'Debit', 1, 1, 2),
('2026-02-10 10:25:00', 100.00, 'Card', 5, 4, NULL),
('2026-02-12 13:45:00', 50.00, 'Cash', 2, 2, 1),
('2026-02-15 15:30:00', 85.00, 'Card', 3, 3, NULL),
('2026-02-18 11:10:00', 200.00, 'Card', 1, 1, 4),
('2026-02-20 14:35:00', 155.00, 'Debit', 4, 3, NULL),
('2026-02-22 16:50:00', 75.00, 'Cash', 2, 1, 2),
('2026-02-24 10:15:00', 140.00, 'Card', 5, 4, NULL),
(NOW(), 95.00, 'Card', 1, 1, 1);
INSERT INTO saleItem (saleId, prodId, quantity, unitPrice)
VALUES
(1, 1, 2, 50.00),
(1, 6, 1, 25.00),
(2, 3, 1, 120.00),
(2, 4, 1, 80.00),
(3, 2, 3, 10.00),
(3, 5, 2, 15.00),
(4, 1, 3, 50.00),
(5, 4, 1, 80.00),
(6, 2, 4, 10.00),
(6, 5, 1, 15.00),
(6, 6, 1, 25.00),
(6, 1, 1, 50.00),
(7, 3, 2, 120.00),
(8, 1, 1, 50.00),
(8, 2, 3, 10.00),
(9, 1, 3, 50.00),
(9, 6, 1, 25.00),
(10, 3, 1, 120.00),
(11, 5, 1, 15.00),
(11, 2, 3, 10.00),
(12, 4, 2, 80.00),
(13, 6, 4, 25.00),
(14, 1, 1, 50.00),
(15, 2, 2, 10.00),
(15, 5, 1, 15.00),
(15, 6, 2, 25.00),
(16, 3, 1, 120.00),
(16, 4, 1, 80.00),
(17, 4, 1, 80.00),
(17, 1, 1, 50.00),
(17, 6, 1, 25.00),
(18, 6, 2, 25.00),
(18, 2, 2, 10.00),
(18, 5, 1, 15.00),
(19, 1, 2, 50.00),
(19, 6, 2, 25.00),
(20, 2, 5, 10.00),
(20, 5, 3, 15.00);
INSERT INTO purchaseOrder (supId, orderDate, status)
VALUES
(1, '2025-01-15', 'Delivered'),
(2, '2025-01-20', 'Pending'),
(3, '2025-02-01', 'Delivered'),
(4, '2025-02-10', 'In Transit'),
(1, '2025-02-15', 'Pending');
INSERT INTO activityLog (employeeId, activity)
VALUES
(1, 'Created new sale'),
(2, 'Booked appointment'),
(3, 'Completed grooming service'),
(4, 'Processed inventory order'),
(5, 'Conducted health checkup'),
(1, 'Updated customer information');

View File

@@ -0,0 +1,250 @@
-- Create Tables
CREATE TABLE IF NOT EXISTS storeLocation (
storeId BIGINT AUTO_INCREMENT PRIMARY KEY,
storeName VARCHAR(100) NOT NULL,
address VARCHAR(255) NOT NULL,
phone VARCHAR(20) NOT NULL,
email VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS employee (
employeeId BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NULL,
firstName VARCHAR(50) NOT NULL,
lastName VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
phone VARCHAR(20) NOT NULL,
role VARCHAR(50) NOT NULL,
isActive BOOLEAN DEFAULT TRUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT uk_employee_user_id UNIQUE (user_id)
);
CREATE TABLE IF NOT EXISTS employeeStore (
employeeId BIGINT NOT NULL,
storeId BIGINT NOT NULL,
PRIMARY KEY (employeeId, storeId),
FOREIGN KEY (employeeId) REFERENCES employee(employeeId),
FOREIGN KEY (storeId) REFERENCES storeLocation(storeId)
);
CREATE TABLE IF NOT EXISTS customer (
customerId BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NULL,
firstName VARCHAR(50) NOT NULL,
lastName VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
phone VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT uk_customer_user_id UNIQUE (user_id)
);
CREATE TABLE IF NOT EXISTS pet (
petId BIGINT AUTO_INCREMENT PRIMARY KEY,
petName VARCHAR(50) NOT NULL,
petSpecies VARCHAR(50) NOT NULL,
petBreed VARCHAR(50) NOT NULL,
petAge INT NOT NULL,
petStatus VARCHAR(20) NOT NULL,
petPrice DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS adoption (
adoptionId BIGINT AUTO_INCREMENT PRIMARY KEY,
petId BIGINT NOT NULL,
customerId BIGINT NOT NULL,
adoptionDate DATE NOT NULL,
adoptionStatus VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (petId) REFERENCES pet(petId),
FOREIGN KEY (customerId) REFERENCES customer(customerId)
);
CREATE TABLE IF NOT EXISTS supplier (
supId BIGINT AUTO_INCREMENT PRIMARY KEY,
supCompany VARCHAR(100) NOT NULL,
supContactFirstName VARCHAR(50) NOT NULL,
supContactLastName VARCHAR(50) NOT NULL,
supEmail VARCHAR(100) NOT NULL,
supPhone VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS category (
categoryId BIGINT AUTO_INCREMENT PRIMARY KEY,
categoryName VARCHAR(100) NOT NULL,
categoryType VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS product (
prodId BIGINT AUTO_INCREMENT PRIMARY KEY,
prodName VARCHAR(100) NOT NULL,
prodPrice DECIMAL(10, 2) NOT NULL,
categoryId BIGINT NOT NULL,
prodDesc TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (categoryId) REFERENCES category(categoryId)
);
CREATE TABLE IF NOT EXISTS productSupplier (
supId BIGINT NOT NULL,
prodId BIGINT NOT NULL,
cost DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (supId, prodId),
FOREIGN KEY (supId) REFERENCES supplier(supId),
FOREIGN KEY (prodId) REFERENCES product(prodId)
);
CREATE TABLE IF NOT EXISTS inventory (
inventoryId BIGINT AUTO_INCREMENT PRIMARY KEY,
prodId BIGINT NOT NULL,
quantity INT DEFAULT 0 NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (prodId) REFERENCES product(prodId)
);
CREATE TABLE IF NOT EXISTS service (
serviceId BIGINT AUTO_INCREMENT PRIMARY KEY,
serviceName VARCHAR(100) NOT NULL,
serviceDesc TEXT,
serviceDuration INT NOT NULL,
servicePrice DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS appointment (
appointmentId BIGINT AUTO_INCREMENT PRIMARY KEY,
serviceId BIGINT NOT NULL,
customerId BIGINT NOT NULL,
appointmentDate DATE NOT NULL,
appointmentTime TIME NOT NULL,
appointmentStatus VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (serviceId) REFERENCES service(serviceId),
FOREIGN KEY (customerId) REFERENCES customer(customerId)
);
CREATE TABLE IF NOT EXISTS appointmentPet (
appointmentId BIGINT NOT NULL,
petId BIGINT NOT NULL,
PRIMARY KEY (appointmentId, petId),
FOREIGN KEY (appointmentId) REFERENCES appointment(appointmentId),
FOREIGN KEY (petId) REFERENCES pet(petId)
);
CREATE TABLE IF NOT EXISTS sale (
saleId BIGINT AUTO_INCREMENT PRIMARY KEY,
saleDate DATETIME NOT NULL,
totalAmount DECIMAL(10, 2) NOT NULL,
paymentMethod VARCHAR(50) NOT NULL,
employeeId BIGINT NOT NULL,
storeId BIGINT NOT NULL,
customerId BIGINT NULL,
isRefund BOOLEAN DEFAULT FALSE NOT NULL,
originalSaleId BIGINT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (employeeId) REFERENCES employee(employeeId),
FOREIGN KEY (storeId) REFERENCES storeLocation(storeId),
FOREIGN KEY (customerId) REFERENCES customer(customerId),
FOREIGN KEY (originalSaleId) REFERENCES sale(saleId)
);
CREATE TABLE IF NOT EXISTS saleItem (
saleItemId BIGINT AUTO_INCREMENT PRIMARY KEY,
saleId BIGINT NOT NULL,
prodId BIGINT NOT NULL,
quantity INT NOT NULL,
unitPrice DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (saleId) REFERENCES sale(saleId),
FOREIGN KEY (prodId) REFERENCES product(prodId)
);
CREATE TABLE IF NOT EXISTS purchaseOrder (
purchaseOrderId BIGINT AUTO_INCREMENT PRIMARY KEY,
supId BIGINT NOT NULL,
orderDate DATE NOT NULL,
status VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (supId) REFERENCES supplier(supId)
);
CREATE TABLE IF NOT EXISTS activityLog (
logId BIGINT AUTO_INCREMENT PRIMARY KEY,
employeeId BIGINT NOT NULL,
activity TEXT NOT NULL,
logTimestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY (employeeId) REFERENCES employee(employeeId)
);
CREATE TABLE IF NOT EXISTS users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
email VARCHAR(100) UNIQUE,
fullName VARCHAR(100),
avatarUrl VARCHAR(255),
role VARCHAR(20) NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS refund (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
saleId BIGINT NOT NULL,
customerId BIGINT NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
reason VARCHAR(500) NOT NULL,
status VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (saleId) REFERENCES sale(saleId),
FOREIGN KEY (customerId) REFERENCES customer(customerId)
);
CREATE TABLE IF NOT EXISTS conversation (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
customerId BIGINT NOT NULL,
staffId BIGINT,
status VARCHAR(20) DEFAULT 'OPEN',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (customerId) REFERENCES customer(customerId),
FOREIGN KEY (staffId) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS message (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
conversationId BIGINT NOT NULL,
senderId BIGINT NOT NULL,
content TEXT NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
isRead BOOLEAN DEFAULT FALSE,
FOREIGN KEY (conversationId) REFERENCES conversation(id),
FOREIGN KEY (senderId) REFERENCES users(id)
);
-- Add foreign keys for user_id linkage
ALTER TABLE employee ADD CONSTRAINT fk_employee_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE customer ADD CONSTRAINT fk_customer_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;

View File

@@ -0,0 +1,13 @@
package com.petshop.backend.util;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class PasswordHashGenerator {
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
System.out.println("admin123: " + encoder.encode("admin123"));
System.out.println("staff123: " + encoder.encode("staff123"));
System.out.println("customer123: " + encoder.encode("customer123"));
}
}