diff --git a/src/main/java/com/petshop/backend/config/DataInitializer.java b/src/main/java/com/petshop/backend/config/DataInitializer.java index cff00f6c..4a8c7470 100644 --- a/src/main/java/com/petshop/backend/config/DataInitializer.java +++ b/src/main/java/com/petshop/backend/config/DataInitializer.java @@ -35,6 +35,7 @@ public class DataInitializer implements CommandLineRunner { admin.setPassword(passwordEncoder.encode("admin123")); admin.setEmail("admin@petshop.com"); admin.setFullName("Admin User"); + admin.setPhone("000-000-1000"); admin.setRole(User.Role.ADMIN); admin.setActive(true); admin = userRepository.save(admin); @@ -51,6 +52,10 @@ public class DataInitializer implements CommandLineRunner { admin.setEmail("admin@petshop.com"); updated = true; } + if (admin.getPhone() == null || admin.getPhone().isEmpty()) { + admin.setPhone("000-000-1000"); + updated = true; + } if (admin.getActive() == null) { admin.setActive(true); updated = true; @@ -75,6 +80,7 @@ public class DataInitializer implements CommandLineRunner { staff.setPassword(passwordEncoder.encode("staff123")); staff.setEmail("staff@petshop.com"); staff.setFullName("Staff User"); + staff.setPhone("000-000-1001"); staff.setRole(User.Role.STAFF); staff.setActive(true); staff = userRepository.save(staff); @@ -91,6 +97,10 @@ public class DataInitializer implements CommandLineRunner { staff.setEmail("staff@petshop.com"); updated = true; } + if (staff.getPhone() == null || staff.getPhone().isEmpty()) { + staff.setPhone("000-000-1001"); + updated = true; + } if (staff.getActive() == null) { staff.setActive(true); updated = true; @@ -115,6 +125,7 @@ public class DataInitializer implements CommandLineRunner { customer.setPassword(passwordEncoder.encode("customer123")); customer.setEmail("customer@petshop.com"); customer.setFullName("Test Customer"); + customer.setPhone("000-000-1002"); customer.setRole(User.Role.CUSTOMER); customer.setActive(true); customer = userRepository.save(customer); @@ -131,6 +142,10 @@ public class DataInitializer implements CommandLineRunner { customer.setEmail("customer@petshop.com"); updated = true; } + if (customer.getPhone() == null || customer.getPhone().isEmpty()) { + customer.setPhone("000-000-1002"); + updated = true; + } if (customer.getActive() == null) { customer.setActive(true); updated = true; diff --git a/src/main/java/com/petshop/backend/controller/AuthController.java b/src/main/java/com/petshop/backend/controller/AuthController.java index 717ca7fa..2bd2b47d 100644 --- a/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/src/main/java/com/petshop/backend/controller/AuthController.java @@ -74,11 +74,19 @@ public class AuthController { return ResponseEntity.status(HttpStatus.CONFLICT).body(error); } + String phone = trimToNull(request.getPhone()); + if (phone != null && userRepository.findByPhone(phone).isPresent()) { + Map error = new HashMap<>(); + error.put("message", "Phone 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.setPhone(phone); user.setRole(User.Role.CUSTOMER); user.setActive(true); @@ -93,6 +101,7 @@ public class AuthController { savedUser.getId(), savedUser.getUsername(), savedUser.getEmail(), + savedUser.getPhone(), savedUser.getRole().name(), token )); @@ -145,6 +154,7 @@ public class AuthController { user.getUsername(), user.getEmail(), user.getFullName(), + user.getPhone(), user.getAvatarUrl(), user.getRole().name(), employeeStore != null ? employeeStore.getStore().getStoreId() : null, @@ -180,6 +190,20 @@ public class AuthController { user.setFullName(request.getFullName()); } + if (request.getPhone() != null) { + String phone = trimToNull(request.getPhone()); + if (!java.util.Objects.equals(phone, user.getPhone())) { + if (phone != null && userRepository.findByPhone(phone) + .filter(existing -> !existing.getId().equals(user.getId())) + .isPresent()) { + Map error = new HashMap<>(); + error.put("message", "Phone already exists"); + return ResponseEntity.status(HttpStatus.CONFLICT).body(error); + } + user.setPhone(phone); + } + } + if (request.getPassword() != null && !request.getPassword().isEmpty()) { user.setPassword(passwordEncoder.encode(request.getPassword())); invalidateToken = true; @@ -190,6 +214,7 @@ public class AuthController { } User updatedUser = userRepository.save(user); + userBusinessLinkageService.syncLinkedRecords(updatedUser); EmployeeStore employeeStore = resolveEmployeeStore(updatedUser); @@ -198,6 +223,7 @@ public class AuthController { updatedUser.getUsername(), updatedUser.getEmail(), updatedUser.getFullName(), + updatedUser.getPhone(), updatedUser.getAvatarUrl(), updatedUser.getRole().name(), employeeStore != null ? employeeStore.getStore().getStoreId() : null, @@ -215,6 +241,14 @@ public class AuthController { .orElse(null); } + private String trimToNull(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + @PostMapping("/me/avatar") public ResponseEntity uploadAvatar(@RequestParam("avatar") MultipartFile file) { User user = getAuthenticatedUser(); diff --git a/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java b/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java index ae7d6270..58959678 100644 --- a/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java +++ b/src/main/java/com/petshop/backend/dto/auth/ProfileUpdateRequest.java @@ -14,6 +14,9 @@ public class ProfileUpdateRequest { @Size(max = 100, message = "Full name must not exceed 100 characters") private String fullName; + @Size(max = 20, message = "Phone must not exceed 20 characters") + private String phone; + @Size(min = 6, message = "Password must be at least 6 characters") private String password; @@ -41,6 +44,14 @@ public class ProfileUpdateRequest { this.fullName = fullName; } + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + public String getPassword() { return password; } @@ -57,12 +68,13 @@ public class ProfileUpdateRequest { return Objects.equals(username, that.username) && Objects.equals(email, that.email) && Objects.equals(fullName, that.fullName) && + Objects.equals(phone, that.phone) && Objects.equals(password, that.password); } @Override public int hashCode() { - return Objects.hash(username, email, fullName, password); + return Objects.hash(username, email, fullName, phone, password); } @Override @@ -71,6 +83,7 @@ public class ProfileUpdateRequest { "username='" + username + '\'' + ", email='" + email + '\'' + ", fullName='" + fullName + '\'' + + ", phone='" + phone + '\'' + ", password='" + password + '\'' + '}'; } diff --git a/src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java b/src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java index 07775bad..2791746c 100644 --- a/src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java +++ b/src/main/java/com/petshop/backend/dto/auth/RegisterRequest.java @@ -22,6 +22,10 @@ public class RegisterRequest { @Size(max = 100, message = "Full name must not exceed 100 characters") private String fullName; + @NotBlank(message = "Phone is required") + @Size(max = 20, message = "Phone must not exceed 20 characters") + private String phone; + public String getUsername() { return username; } @@ -54,6 +58,14 @@ public class RegisterRequest { this.fullName = fullName; } + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -62,12 +74,13 @@ public class RegisterRequest { return Objects.equals(username, that.username) && Objects.equals(password, that.password) && Objects.equals(email, that.email) && - Objects.equals(fullName, that.fullName); + Objects.equals(fullName, that.fullName) && + Objects.equals(phone, that.phone); } @Override public int hashCode() { - return Objects.hash(username, password, email, fullName); + return Objects.hash(username, password, email, fullName, phone); } @Override @@ -77,6 +90,7 @@ public class RegisterRequest { ", password='" + password + '\'' + ", email='" + email + '\'' + ", fullName='" + fullName + '\'' + + ", phone='" + phone + '\'' + '}'; } } diff --git a/src/main/java/com/petshop/backend/dto/auth/RegisterResponse.java b/src/main/java/com/petshop/backend/dto/auth/RegisterResponse.java index b31370cb..7e016985 100644 --- a/src/main/java/com/petshop/backend/dto/auth/RegisterResponse.java +++ b/src/main/java/com/petshop/backend/dto/auth/RegisterResponse.java @@ -6,16 +6,18 @@ public class RegisterResponse { private Long id; private String username; private String email; + private String phone; private String role; private String token; public RegisterResponse() { } - public RegisterResponse(Long id, String username, String email, String role, String token) { + public RegisterResponse(Long id, String username, String email, String phone, String role, String token) { this.id = id; this.username = username; this.email = email; + this.phone = phone; this.role = role; this.token = token; } @@ -44,6 +46,14 @@ public class RegisterResponse { this.email = email; } + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + public String getRole() { return role; } @@ -68,13 +78,14 @@ public class RegisterResponse { return Objects.equals(id, that.id) && Objects.equals(username, that.username) && Objects.equals(email, that.email) && + Objects.equals(phone, that.phone) && Objects.equals(role, that.role) && Objects.equals(token, that.token); } @Override public int hashCode() { - return Objects.hash(id, username, email, role, token); + return Objects.hash(id, username, email, phone, role, token); } @Override @@ -83,6 +94,7 @@ public class RegisterResponse { "id=" + id + ", username='" + username + '\'' + ", email='" + email + '\'' + + ", phone='" + phone + '\'' + ", role='" + role + '\'' + ", token='" + token + '\'' + '}'; diff --git a/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java b/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java index 1f15daf8..ba714a49 100644 --- a/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java +++ b/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java @@ -7,6 +7,7 @@ public class UserInfoResponse { private String username; private String email; private String fullName; + private String phone; private String avatarUrl; private String role; private Long storeId; @@ -15,11 +16,12 @@ public class UserInfoResponse { public UserInfoResponse() { } - public UserInfoResponse(Long id, String username, String email, String fullName, String avatarUrl, String role, Long storeId, String storeName) { + public UserInfoResponse(Long id, String username, String email, String fullName, String phone, String avatarUrl, String role, Long storeId, String storeName) { this.id = id; this.username = username; this.email = email; this.fullName = fullName; + this.phone = phone; this.avatarUrl = avatarUrl; this.role = role; this.storeId = storeId; @@ -58,6 +60,14 @@ public class UserInfoResponse { this.fullName = fullName; } + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + public String getAvatarUrl() { return avatarUrl; } @@ -99,6 +109,7 @@ public class UserInfoResponse { Objects.equals(username, that.username) && Objects.equals(email, that.email) && Objects.equals(fullName, that.fullName) && + Objects.equals(phone, that.phone) && Objects.equals(avatarUrl, that.avatarUrl) && Objects.equals(role, that.role) && Objects.equals(storeId, that.storeId) && @@ -107,7 +118,7 @@ public class UserInfoResponse { @Override public int hashCode() { - return Objects.hash(id, username, email, fullName, avatarUrl, role, storeId, storeName); + return Objects.hash(id, username, email, fullName, phone, avatarUrl, role, storeId, storeName); } @Override @@ -117,6 +128,7 @@ public class UserInfoResponse { ", username='" + username + '\'' + ", email='" + email + '\'' + ", fullName='" + fullName + '\'' + + ", phone='" + phone + '\'' + ", avatarUrl='" + avatarUrl + '\'' + ", role='" + role + '\'' + ", storeId=" + storeId + diff --git a/src/main/java/com/petshop/backend/dto/user/UserRequest.java b/src/main/java/com/petshop/backend/dto/user/UserRequest.java index 72b1dbfb..09a9036d 100644 --- a/src/main/java/com/petshop/backend/dto/user/UserRequest.java +++ b/src/main/java/com/petshop/backend/dto/user/UserRequest.java @@ -21,6 +21,9 @@ public class UserRequest { @Email(message = "Invalid email format") private String email; + @Size(max = 20, message = "Phone must not exceed 20 characters") + private String phone; + @NotNull(message = "Role is required") private User.Role role; @@ -58,6 +61,14 @@ public class UserRequest { this.email = email; } + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + public User.Role getRole() { return role; } @@ -83,13 +94,14 @@ public class UserRequest { Objects.equals(password, that.password) && Objects.equals(fullName, that.fullName) && Objects.equals(email, that.email) && + Objects.equals(phone, that.phone) && role == that.role && Objects.equals(active, that.active); } @Override public int hashCode() { - return Objects.hash(username, password, fullName, email, role, active); + return Objects.hash(username, password, fullName, email, phone, role, active); } @Override @@ -99,6 +111,7 @@ public class UserRequest { ", password='" + password + '\'' + ", fullName='" + fullName + '\'' + ", email='" + email + '\'' + + ", phone='" + phone + '\'' + ", role=" + role + ", active=" + active + '}'; diff --git a/src/main/java/com/petshop/backend/dto/user/UserResponse.java b/src/main/java/com/petshop/backend/dto/user/UserResponse.java index 8b383366..9d7167c2 100644 --- a/src/main/java/com/petshop/backend/dto/user/UserResponse.java +++ b/src/main/java/com/petshop/backend/dto/user/UserResponse.java @@ -8,6 +8,7 @@ public class UserResponse { private String username; private String fullName; private String email; + private String phone; private String role; private Boolean active; private LocalDateTime createdAt; @@ -16,11 +17,12 @@ public class UserResponse { public UserResponse() { } - public UserResponse(Long id, String username, String fullName, String email, String role, Boolean active, LocalDateTime createdAt, LocalDateTime updatedAt) { + public UserResponse(Long id, String username, String fullName, String email, String phone, String role, Boolean active, LocalDateTime createdAt, LocalDateTime updatedAt) { this.id = id; this.username = username; this.fullName = fullName; this.email = email; + this.phone = phone; this.role = role; this.active = active; this.createdAt = createdAt; @@ -59,6 +61,14 @@ public class UserResponse { this.email = email; } + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + public String getRole() { return role; } @@ -96,12 +106,12 @@ public class UserResponse { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UserResponse that = (UserResponse) o; - return Objects.equals(id, that.id) && Objects.equals(username, that.username) && Objects.equals(fullName, that.fullName) && Objects.equals(email, that.email) && Objects.equals(role, that.role) && Objects.equals(active, that.active) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + return Objects.equals(id, that.id) && Objects.equals(username, that.username) && Objects.equals(fullName, that.fullName) && Objects.equals(email, that.email) && Objects.equals(phone, that.phone) && Objects.equals(role, that.role) && Objects.equals(active, that.active) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); } @Override public int hashCode() { - return Objects.hash(id, username, fullName, email, role, active, createdAt, updatedAt); + return Objects.hash(id, username, fullName, email, phone, role, active, createdAt, updatedAt); } @Override @@ -111,6 +121,7 @@ public class UserResponse { ", username='" + username + '\'' + ", fullName='" + fullName + '\'' + ", email='" + email + '\'' + + ", phone='" + phone + '\'' + ", role='" + role + '\'' + ", active=" + active + ", createdAt=" + createdAt + diff --git a/src/main/java/com/petshop/backend/entity/User.java b/src/main/java/com/petshop/backend/entity/User.java index 54bd202e..cdec2754 100644 --- a/src/main/java/com/petshop/backend/entity/User.java +++ b/src/main/java/com/petshop/backend/entity/User.java @@ -27,6 +27,9 @@ public class User { @Column(length = 100) private String fullName; + @Column(length = 20) + private String phone; + @Column(length = 255) private String avatarUrl; @@ -55,12 +58,13 @@ public class User { public User() { } - public User(Long id, String username, String password, String email, String fullName, String avatarUrl, Role role, Boolean active, Integer tokenVersion, LocalDateTime createdAt, LocalDateTime updatedAt) { + public User(Long id, String username, String password, String email, String fullName, String phone, String avatarUrl, Role role, Boolean active, Integer tokenVersion, LocalDateTime createdAt, LocalDateTime updatedAt) { this.id = id; this.username = username; this.password = password; this.email = email; this.fullName = fullName; + this.phone = phone; this.avatarUrl = avatarUrl; this.role = role; this.active = active; @@ -109,6 +113,14 @@ public class User { this.fullName = fullName; } + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + public String getAvatarUrl() { return avatarUrl; } @@ -178,6 +190,7 @@ public class User { ", password='" + password + '\'' + ", email='" + email + '\'' + ", fullName='" + fullName + '\'' + + ", phone='" + phone + '\'' + ", avatarUrl='" + avatarUrl + '\'' + ", role=" + role + ", active=" + active + diff --git a/src/main/java/com/petshop/backend/repository/UserRepository.java b/src/main/java/com/petshop/backend/repository/UserRepository.java index 17e4356a..775c1b18 100644 --- a/src/main/java/com/petshop/backend/repository/UserRepository.java +++ b/src/main/java/com/petshop/backend/repository/UserRepository.java @@ -14,9 +14,13 @@ import java.util.Optional; public interface UserRepository extends JpaRepository { Optional findByUsername(String username); Optional findByEmail(String email); + Optional findByPhone(String phone); boolean existsByUsername(String username); @Query("SELECT u FROM User u WHERE " + - "LOWER(u.username) LIKE LOWER(CONCAT('%', :q, '%'))") + "LOWER(u.username) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(COALESCE(u.fullName, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(COALESCE(u.email, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(COALESCE(u.phone, '')) LIKE LOWER(CONCAT('%', :q, '%'))") Page searchUsers(@Param("q") String query, Pageable pageable); } diff --git a/src/main/java/com/petshop/backend/service/UserBusinessLinkageService.java b/src/main/java/com/petshop/backend/service/UserBusinessLinkageService.java index 81b4738f..d0a18d3e 100644 --- a/src/main/java/com/petshop/backend/service/UserBusinessLinkageService.java +++ b/src/main/java/com/petshop/backend/service/UserBusinessLinkageService.java @@ -25,88 +25,116 @@ public class UserBusinessLinkageService { @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(); + return syncEmployee(existing.get(), user); } } - // Check for email matches List 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); + return syncEmployee(employee, user); } } - // 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.setPhone(normalizePhone(user.getPhone(), "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 + newEmployee.setRole("Staff"); } - return employeeRepository.save(newEmployee); + return syncEmployee(newEmployee, user); } @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(); + return syncCustomer(existing.get(), user); } } - // Check for email matches List 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); + return syncCustomer(customer, user); } } - // 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"); + newCustomer.setPhone(normalizePhone(user.getPhone(), "000-000-0001")); - return customerRepository.save(newCustomer); + return syncCustomer(newCustomer, user); + } + + @Transactional + public void syncLinkedRecords(User user) { + if (user.getRole() == User.Role.CUSTOMER) { + ensureLinkedCustomer(user); + return; + } + ensureLinkedEmployee(user); + } + + private Employee syncEmployee(Employee employee, User user) { + employee.setUserId(user.getId()); + employee.setEmail(user.getEmail()); + String[] nameParts = splitFullName(user.getFullName()); + employee.setFirstName(nameParts[0]); + employee.setLastName(nameParts[1]); + employee.setPhone(normalizePhone(user.getPhone(), employee.getPhone())); + if (user.getRole() == User.Role.ADMIN) { + employee.setRole("Manager"); + } else { + employee.setRole("Staff"); + } + return employeeRepository.save(employee); + } + + private Customer syncCustomer(Customer customer, User user) { + customer.setUserId(user.getId()); + customer.setEmail(user.getEmail()); + String[] nameParts = splitFullName(user.getFullName()); + customer.setFirstName(nameParts[0]); + customer.setLastName(nameParts[1]); + customer.setPhone(normalizePhone(user.getPhone(), customer.getPhone())); + return customerRepository.save(customer); + } + + private String normalizePhone(String phone, String fallback) { + if (phone == null || phone.trim().isEmpty()) { + return fallback; + } + return phone.trim(); } private String[] splitFullName(String fullName) { @@ -118,11 +146,9 @@ public class UserBusinessLinkageService { 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(); diff --git a/src/main/java/com/petshop/backend/service/UserService.java b/src/main/java/com/petshop/backend/service/UserService.java index ee705a8a..a54aecd3 100644 --- a/src/main/java/com/petshop/backend/service/UserService.java +++ b/src/main/java/com/petshop/backend/service/UserService.java @@ -11,6 +11,9 @@ import org.springframework.data.domain.Pageable; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import static org.springframework.http.HttpStatus.CONFLICT; @Service public class UserService { @@ -48,17 +51,15 @@ public class UserService { user.setPassword(passwordEncoder.encode(request.getPassword())); user.setFullName(request.getFullName()); user.setEmail(request.getEmail()); + user.setPhone(trimToNull(request.getPhone())); user.setRole(request.getRole()); user.setActive(request.getActive() != null ? request.getActive() : true); + validateUniquePhone(user.getPhone(), null); + 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); - } + userBusinessLinkageService.syncLinkedRecords(user); return mapToResponse(user); } @@ -80,6 +81,11 @@ public class UserService { } user.setFullName(request.getFullName()); user.setEmail(request.getEmail()); + String phone = trimToNull(request.getPhone()); + if (!java.util.Objects.equals(user.getPhone(), phone)) { + validateUniquePhone(phone, user.getId()); + } + user.setPhone(phone); user.setRole(request.getRole()); user.setActive(request.getActive() != null ? request.getActive() : true); if (invalidateToken) { @@ -87,6 +93,7 @@ public class UserService { } user = userRepository.save(user); + userBusinessLinkageService.syncLinkedRecords(user); return mapToResponse(user); } @@ -109,10 +116,30 @@ public class UserService { response.setUsername(user.getUsername()); response.setFullName(user.getFullName()); response.setEmail(user.getEmail()); + response.setPhone(user.getPhone()); response.setRole(user.getRole().toString()); response.setActive(user.getActive()); response.setCreatedAt(user.getCreatedAt()); response.setUpdatedAt(user.getUpdatedAt()); return response; } + + private void validateUniquePhone(String phone, Long currentUserId) { + if (phone == null || phone.isBlank()) { + return; + } + userRepository.findByPhone(phone) + .filter(existing -> !existing.getId().equals(currentUserId)) + .ifPresent(existing -> { + throw new ResponseStatusException(CONFLICT, "Phone already exists"); + }); + } + + private String trimToNull(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } } diff --git a/src/main/resources/db/migration/V6__user_phone.sql b/src/main/resources/db/migration/V6__user_phone.sql new file mode 100644 index 00000000..95478fdc --- /dev/null +++ b/src/main/resources/db/migration/V6__user_phone.sql @@ -0,0 +1,8 @@ +ALTER TABLE users + ADD COLUMN phone VARCHAR(20) NULL AFTER fullName; + +UPDATE users u +LEFT JOIN customer c ON c.user_id = u.id +LEFT JOIN employee e ON e.user_id = u.id +SET u.phone = COALESCE(NULLIF(c.phone, ''), NULLIF(e.phone, ''), u.phone) +WHERE u.phone IS NULL OR u.phone = '';