cut over employee phones

This commit is contained in:
2026-03-14 20:22:14 -06:00
parent 4fd2041fbb
commit 4b4f4b087e
13 changed files with 350 additions and 73 deletions

View File

@@ -0,0 +1,49 @@
package com.petshop.backend.controller;
import com.petshop.backend.dto.employee.EmployeeRequest;
import com.petshop.backend.dto.employee.EmployeeResponse;
import com.petshop.backend.service.EmployeeService;
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/employees")
@PreAuthorize("hasRole('ADMIN')")
public class EmployeeController {
private final EmployeeService employeeService;
public EmployeeController(EmployeeService employeeService) {
this.employeeService = employeeService;
}
@GetMapping
public ResponseEntity<Page<EmployeeResponse>> getAllEmployees(@RequestParam(required = false) String q, Pageable pageable) {
return ResponseEntity.ok(employeeService.getAllEmployees(q, pageable));
}
@GetMapping("/{id}")
public ResponseEntity<EmployeeResponse> getEmployeeById(@PathVariable Long id) {
return ResponseEntity.ok(employeeService.getEmployeeById(id));
}
@PostMapping
public ResponseEntity<EmployeeResponse> createEmployee(@Valid @RequestBody EmployeeRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(employeeService.createEmployee(request));
}
@PutMapping("/{id}")
public ResponseEntity<EmployeeResponse> updateEmployee(@PathVariable Long id, @Valid @RequestBody EmployeeRequest request) {
return ResponseEntity.ok(employeeService.updateEmployee(id, request));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteEmployee(@PathVariable Long id) {
employeeService.deleteEmployee(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -14,8 +14,6 @@ public class CustomerRequest {
@Email(message = "Invalid email format")
private String email;
private String phone;
public String getFirstName() {
return firstName;
}
@@ -40,14 +38,6 @@ public class CustomerRequest {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -55,13 +45,12 @@ public class CustomerRequest {
CustomerRequest that = (CustomerRequest) o;
return Objects.equals(firstName, that.firstName) &&
Objects.equals(lastName, that.lastName) &&
Objects.equals(email, that.email) &&
Objects.equals(phone, that.phone);
Objects.equals(email, that.email);
}
@Override
public int hashCode() {
return Objects.hash(firstName, lastName, email, phone);
return Objects.hash(firstName, lastName, email);
}
@Override
@@ -70,7 +59,6 @@ public class CustomerRequest {
"firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", phone='" + phone + '\'' +
'}';
}
}

View File

@@ -8,19 +8,17 @@ public class CustomerResponse {
private String firstName;
private String lastName;
private String email;
private String phone;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public CustomerResponse() {
}
public CustomerResponse(Long customerId, String firstName, String lastName, String email, String phone, LocalDateTime createdAt, LocalDateTime updatedAt) {
public CustomerResponse(Long customerId, String firstName, String lastName, String email, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.customerId = customerId;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.phone = phone;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
@@ -57,14 +55,6 @@ public class CustomerResponse {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
@@ -86,12 +76,12 @@ public class CustomerResponse {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CustomerResponse that = (CustomerResponse) o;
return Objects.equals(customerId, that.customerId) && Objects.equals(firstName, that.firstName) && Objects.equals(lastName, that.lastName) && Objects.equals(email, that.email) && Objects.equals(phone, that.phone) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt);
return Objects.equals(customerId, that.customerId) && Objects.equals(firstName, that.firstName) && Objects.equals(lastName, that.lastName) && Objects.equals(email, that.email) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt);
}
@Override
public int hashCode() {
return Objects.hash(customerId, firstName, lastName, email, phone, createdAt, updatedAt);
return Objects.hash(customerId, firstName, lastName, email, createdAt, updatedAt);
}
@Override
@@ -101,7 +91,6 @@ public class CustomerResponse {
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", phone='" + phone + '\'' +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
'}';

View File

@@ -0,0 +1,50 @@
package com.petshop.backend.dto.employee;
import com.petshop.backend.entity.User;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
public class EmployeeRequest {
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
private String username;
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;
@NotBlank(message = "First name is required")
private String firstName;
@NotBlank(message = "Last name is required")
private String lastName;
@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;
private Boolean active = true;
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 getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public User.Role getRole() { return role; }
public void setRole(User.Role role) { this.role = role; }
public Boolean getActive() { return active; }
public void setActive(Boolean active) { this.active = active; }
}

View File

@@ -0,0 +1,43 @@
package com.petshop.backend.dto.employee;
import java.time.LocalDateTime;
public class EmployeeResponse {
private Long employeeId;
private Long userId;
private String username;
private String firstName;
private String lastName;
private String fullName;
private String email;
private String phone;
private String role;
private Boolean active;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public Long getEmployeeId() { return employeeId; }
public void setEmployeeId(Long employeeId) { this.employeeId = employeeId; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public String getFullName() { return fullName; }
public void setFullName(String fullName) { this.fullName = fullName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public Boolean getActive() { return active; }
public void setActive(Boolean active) { this.active = active; }
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

@@ -27,9 +27,6 @@ public class Customer {
@Column(nullable = false, length = 100)
private String email;
@Column(nullable = false, length = 20)
private String phone;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@@ -41,13 +38,12 @@ public class Customer {
public Customer() {
}
public Customer(Long customerId, Long userId, String firstName, String lastName, String email, String phone, LocalDateTime createdAt, LocalDateTime updatedAt) {
public Customer(Long customerId, Long userId, String firstName, String lastName, String email, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.customerId = customerId;
this.userId = userId;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.phone = phone;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
@@ -92,14 +88,6 @@ public class Customer {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
@@ -137,7 +125,6 @@ public class Customer {
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", phone='" + phone + '\'' +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
'}';

View File

@@ -27,9 +27,6 @@ public class Employee {
@Column(nullable = false, length = 100)
private String email;
@Column(nullable = false, length = 20)
private String phone;
@Column(nullable = false, length = 50)
private String role;
@@ -47,13 +44,12 @@ public class Employee {
public Employee() {
}
public Employee(Long employeeId, Long userId, 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 role, Boolean isActive, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.employeeId = employeeId;
this.userId = userId;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.phone = phone;
this.role = role;
this.isActive = isActive;
this.createdAt = createdAt;
@@ -100,14 +96,6 @@ public class Employee {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getRole() {
return role;
}
@@ -161,7 +149,6 @@ public class Employee {
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", phone='" + phone + '\'' +
", role='" + role + '\'' +
", isActive=" + isActive +
", createdAt=" + createdAt +

View File

@@ -21,6 +21,6 @@ public interface CustomerRepository extends JpaRepository<Customer, Long> {
"LOWER(c.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(c.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(c.email) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(c.phone) LIKE LOWER(CONCAT('%', :q, '%'))")
"EXISTS (SELECT u FROM User u WHERE u.id = c.userId AND LOWER(COALESCE(u.phone, '')) LIKE LOWER(CONCAT('%', :q, '%')))")
Page<Customer> searchCustomers(@Param("q") String query, Pageable pageable);
}

View File

@@ -1,7 +1,11 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.Employee;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@@ -11,4 +15,14 @@ import java.util.Optional;
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
Optional<Employee> findByUserId(Long userId);
List<Employee> findAllByEmail(String email);
@Query("SELECT e FROM Employee e WHERE " +
"LOWER(e.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(e.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(e.email) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(e.role) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"EXISTS (SELECT u FROM User u WHERE u.id = e.userId AND (" +
"LOWER(u.username) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
"LOWER(COALESCE(u.phone, '')) LIKE LOWER(CONCAT('%', :q, '%'))))")
Page<Employee> searchEmployees(@Param("q") String query, Pageable pageable);
}

View File

@@ -45,7 +45,6 @@ public class CustomerService {
customer.setFirstName(request.getFirstName());
customer.setLastName(request.getLastName());
customer.setEmail(request.getEmail());
customer.setPhone(request.getPhone());
customer = customerRepository.save(customer);
syncLinkedUser(customer);
@@ -60,7 +59,6 @@ public class CustomerService {
customer.setFirstName(request.getFirstName());
customer.setLastName(request.getLastName());
customer.setEmail(request.getEmail());
customer.setPhone(request.getPhone());
customer = customerRepository.save(customer);
syncLinkedUser(customer);
@@ -86,7 +84,6 @@ public class CustomerService {
customer.getFirstName(),
customer.getLastName(),
customer.getEmail(),
customer.getPhone(),
customer.getCreatedAt(),
customer.getUpdatedAt()
);
@@ -98,7 +95,6 @@ public class CustomerService {
}
userRepository.findById(customer.getUserId()).ifPresent(user -> {
user.setEmail(customer.getEmail());
user.setPhone(customer.getPhone());
user.setFullName((customer.getFirstName() + " " + customer.getLastName()).trim());
userRepository.save(user);
});

View File

@@ -0,0 +1,175 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.employee.EmployeeRequest;
import com.petshop.backend.dto.employee.EmployeeResponse;
import com.petshop.backend.entity.Employee;
import com.petshop.backend.entity.User;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.EmployeeRepository;
import com.petshop.backend.repository.UserRepository;
import org.springframework.data.domain.Page;
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 EmployeeService {
private final EmployeeRepository employeeRepository;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final UserBusinessLinkageService userBusinessLinkageService;
public EmployeeService(EmployeeRepository employeeRepository, UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) {
this.employeeRepository = employeeRepository;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.userBusinessLinkageService = userBusinessLinkageService;
}
public Page<EmployeeResponse> getAllEmployees(String query, Pageable pageable) {
Page<Employee> employees;
if (query != null && !query.trim().isEmpty()) {
employees = employeeRepository.searchEmployees(query, pageable);
} else {
employees = employeeRepository.findAll(pageable);
}
return employees.map(this::mapToResponse);
}
public EmployeeResponse getEmployeeById(Long id) {
Employee employee = employeeRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + id));
return mapToResponse(employee);
}
@Transactional
public EmployeeResponse createEmployee(EmployeeRequest request) {
validateRole(request.getRole());
if (request.getPassword() == null || request.getPassword().trim().length() < 6) {
throw new IllegalArgumentException("Password must be at least 6 characters");
}
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
throw new ResponseStatusException(CONFLICT, "Username already exists");
}
if (request.getEmail() != null && userRepository.findByEmail(request.getEmail()).isPresent()) {
throw new ResponseStatusException(CONFLICT, "Email already exists");
}
String phone = trimToNull(request.getPhone());
if (phone != null && userRepository.findByPhone(phone).isPresent()) {
throw new ResponseStatusException(CONFLICT, "Phone already exists");
}
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setFullName(fullName(request));
user.setEmail(request.getEmail());
user.setPhone(phone);
user.setRole(request.getRole());
user.setActive(request.getActive() != null ? request.getActive() : true);
user = userRepository.save(user);
Employee employee = userBusinessLinkageService.ensureLinkedEmployee(user);
return mapToResponse(employee, user);
}
@Transactional
public EmployeeResponse updateEmployee(Long id, EmployeeRequest request) {
Employee employee = employeeRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + id));
User user = requireLinkedUser(employee);
validateRole(request.getRole());
if (!user.getUsername().equals(request.getUsername()) && userRepository.findByUsername(request.getUsername()).isPresent()) {
throw new ResponseStatusException(CONFLICT, "Username already exists");
}
if (!java.util.Objects.equals(user.getEmail(), request.getEmail()) && request.getEmail() != null && userRepository.findByEmail(request.getEmail()).isPresent()) {
throw new ResponseStatusException(CONFLICT, "Email already exists");
}
String phone = trimToNull(request.getPhone());
Long currentUserId = user.getId();
if (!java.util.Objects.equals(user.getPhone(), phone)) {
userRepository.findByPhone(phone)
.filter(existing -> !existing.getId().equals(currentUserId))
.ifPresent(existing -> { throw new ResponseStatusException(CONFLICT, "Phone already exists"); });
}
user.setUsername(request.getUsername());
if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) {
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setTokenVersion(user.getTokenVersion() + 1);
}
user.setEmail(request.getEmail());
user.setPhone(phone);
user.setFullName(fullName(request));
user.setRole(request.getRole());
user.setActive(request.getActive() != null ? request.getActive() : true);
user = userRepository.save(user);
employee = userBusinessLinkageService.ensureLinkedEmployee(user);
return mapToResponse(employee, user);
}
@Transactional
public void deleteEmployee(Long id) {
Employee employee = employeeRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + id));
if (employee.getUserId() != null && userRepository.existsById(employee.getUserId())) {
userRepository.deleteById(employee.getUserId());
return;
}
employeeRepository.deleteById(id);
}
private EmployeeResponse mapToResponse(Employee employee) {
User user = requireLinkedUser(employee);
return mapToResponse(employee, user);
}
private EmployeeResponse mapToResponse(Employee employee, User user) {
EmployeeResponse response = new EmployeeResponse();
response.setEmployeeId(employee.getEmployeeId());
response.setUserId(user.getId());
response.setUsername(user.getUsername());
response.setFirstName(employee.getFirstName());
response.setLastName(employee.getLastName());
response.setFullName(user.getFullName());
response.setEmail(user.getEmail());
response.setPhone(user.getPhone());
response.setRole(user.getRole().name());
response.setActive(user.getActive());
response.setCreatedAt(employee.getCreatedAt());
response.setUpdatedAt(employee.getUpdatedAt());
return response;
}
private User requireLinkedUser(Employee employee) {
if (employee.getUserId() == null) {
throw new ResourceNotFoundException("Employee user account not found");
}
return userRepository.findById(employee.getUserId())
.orElseThrow(() -> new ResourceNotFoundException("Employee user account not found"));
}
private void validateRole(User.Role role) {
if (role != User.Role.STAFF && role != User.Role.ADMIN) {
throw new IllegalArgumentException("Employee role must be STAFF or ADMIN");
}
}
private String fullName(EmployeeRequest request) {
return (request.getFirstName().trim() + " " + request.getLastName().trim()).trim();
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
}

View File

@@ -50,7 +50,6 @@ public class UserBusinessLinkageService {
newEmployee.setFirstName(nameParts[0]);
newEmployee.setLastName(nameParts[1]);
newEmployee.setPhone(normalizePhone(user.getPhone(), "000-000-0000"));
newEmployee.setIsActive(true);
if (user.getRole() == User.Role.ADMIN) {
@@ -91,8 +90,6 @@ public class UserBusinessLinkageService {
newCustomer.setFirstName(nameParts[0]);
newCustomer.setLastName(nameParts[1]);
newCustomer.setPhone(normalizePhone(user.getPhone(), "000-000-0001"));
return syncCustomer(newCustomer, user);
}
@@ -111,7 +108,6 @@ public class UserBusinessLinkageService {
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 {
@@ -126,17 +122,9 @@ public class UserBusinessLinkageService {
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) {
if (fullName == null || fullName.trim().isEmpty()) {
return new String[]{"System", "User"};

View File

@@ -0,0 +1,11 @@
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(u.phone, ''), NULLIF(c.phone, ''), NULLIF(e.phone, ''))
WHERE u.phone IS NULL OR u.phone = '';
ALTER TABLE customer
DROP COLUMN phone;
ALTER TABLE employee
DROP COLUMN phone;