add user phone

This commit is contained in:
2026-03-14 20:12:48 -06:00
parent bc9bd09d24
commit efc9836c11
13 changed files with 245 additions and 43 deletions

View File

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

View File

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

View File

@@ -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 + '\'' +
'}';
}

View File

@@ -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 + '\'' +
'}';
}
}

View File

@@ -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 + '\'' +
'}';

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,9 +14,13 @@ import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
Optional<User> 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<User> searchUsers(@Param("q") String query, Pageable pageable);
}

View File

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

View File

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

View File

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