Coverage Summary for Class: UserServiceImpl (com.github.malyshevhen.services.impl)

Class Method, % Branch, % Line, %
UserServiceImpl 92.3% (12/13) 81.2% (13/16) 90.6% (48/53)
UserServiceImpl$$SpringCGLIB$$0
Total 92.3% (12/13) 81.2% (13/16) 90.6% (48/53)


 package com.github.malyshevhen.services.impl;
 
 import java.time.LocalDate;
 import java.time.temporal.ChronoUnit;
 import java.util.Objects;
 
 import com.github.malyshevhen.domain.dto.DateRange;
 import com.github.malyshevhen.configs.UserConstraints;
 import com.github.malyshevhen.dto.Phone;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.jpa.domain.Specification;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import com.github.malyshevhen.exceptions.EntityAlreadyExistsException;
 import com.github.malyshevhen.exceptions.EntityNotFoundException;
 import com.github.malyshevhen.exceptions.UserValidationException;
 import com.github.malyshevhen.domain.models.Address;
 import com.github.malyshevhen.domain.models.User;
 import com.github.malyshevhen.repositories.UserRepository;
 import com.github.malyshevhen.services.UserService;
 
 import com.github.malyshevhen.configs.ApplicationConfig;
 import lombok.RequiredArgsConstructor;
 
 /**
  * Provides an implementation of the {@link UserService} interface, handling
  * user-related operations such as saving, retrieving, updating, and deleting
  * users.
  * <p>
  * This service implementation uses a {@link UserRepository} to interact with
  * the underlying data storage and a {@link ApplicationConfig} to access
  * application-specific configuration.
  * <p>
  * The service methods perform various validation checks, such as ensuring the
  * user's age is legal and the email is not already taken, before performing the
  * requested operations.
  * Also operations are performed in a transactional way.
  *
  * @author Evhen Malysh
  */
 @Service
 @RequiredArgsConstructor
 public class UserServiceImpl implements UserService {
 
     private final UserRepository userRepository;
     private final UserConstraints userConstraints;
 
     /**
      * Saves a new user to the system.
      *
      * @param userToRegister the user to be registered
      * @return the saved user
      * @throws EntityAlreadyExistsException if email is already taken
      *                                      in the database
      * @throws UserValidationException      if age validation fails
      */
     @Transactional
     @Override
     public User save(User userToRegister) {
         assertThatAgeIsLegal(userToRegister);
         assertThatEmailNotTaken(userToRegister.getEmail());
 
         return userRepository.save(userToRegister);
     }
 
     /**
      * Retrieves a paginated list of all users.
      * </p>
      * If {@code pageable} is null, using default values for sorting and paging.
      * If 0 or negative number passed as page size, then it will be set to 10.
      * If the result is empty, returns an empty page.
      *
      * @param pageable  the pagination parameters
      * @param dateRange date range filter for filtering users by age
      * @return a page of users
      */
     @Transactional(readOnly = true)
     @Override
     public Page<User> getAll(Pageable pageable, DateRange dateRange) {
         return userRepository.findAll(inRange(dateRange), pageable);
     }
 
     /**
      * Retrieves a user by their unique identifier.
      *
      * @param id the unique identifier of the user to retrieve
      * @return the user with the specified ID
      * @throws EntityNotFoundException if no user is found with the specified ID
      */
     @Transactional(readOnly = true)
     @Override
     public User getById(Long id) {
         var errorMessage = String.format("User with id %d was not found", id);
         return userRepository.findById(id)
                 .orElseThrow(() -> new EntityNotFoundException(errorMessage));
     }
 
     /**
      * Updates an existing user with the provided user data.
      * </p>
      * This method should not be used. Instead, use {@link #updateEmail} and
      * {@link #updateAddress} methods separately.
      * <p>
      * This method is kept for test assignment purposes only.
      *
      * @param id   the ID of the user to update
      * @param user the updated user data
      * @return the updated user
      * @throws EntityNotFoundException      if no user found with the specified ID
      * @throws EntityAlreadyExistsException if the new email is already registered
      * @throws UserValidationException      if the user's age is below the required
      *                                      age
      */
     @Transactional
     @Override
     public User updateById(Long id, User user) {
         var existingUser = getById(id);
         existingUser.setFirstName(user.getFirstName());
         existingUser.setLastName(user.getLastName());
         existingUser.setAddress(user.getAddress());
         existingUser.setPhone(user.getPhone());
 
         var existingBirthDate = existingUser.getBirthDate();
         var birthDateToUpdate = user.getBirthDate();
         if (!Objects.equals(existingBirthDate, birthDateToUpdate)) {
             assertThatAgeIsLegal(user);
             existingUser.setBirthDate(user.getBirthDate());
         }
 
         var existingEmail = existingUser.getEmail();
         var emailToUpdate = user.getEmail();
         if (!Objects.equals(existingEmail, emailToUpdate)) {
             assertThatEmailNotTaken(emailToUpdate);
             existingUser.setEmail(user.getEmail());
         }
 
         return existingUser;
     }
 
     /**
      * Updates the email of an existing user.
      *
      * @param id    The ID of the user to update.
      * @param email The new email address to set for the user.
      * @return The updated user entity.
      * @throws EntityAlreadyExistsException if the new email is already registered
      *                                      to another user.
      */
     @Transactional
     @Override
     public User updateEmail(Long id, String email) {
         assertThatEmailNotTaken(email);
 
         var existingUser = getById(id);
         existingUser.setEmail(email);
         return existingUser;
     }
 
     /**
      * Updates the address of the user with the specified ID.
      *
      * @param id      the ID of the user whose address should be updated
      * @param address the new address to set for the user
      * @return the updated user with the new address
      * @throws EntityNotFoundException if no user is found with the specified ID
      */
     @Transactional
     @Override
     public User updateAddress(Long id, Address address) {
         var existingUser = getById(id);
         existingUser.setAddress(address);
         return existingUser;
     }
 
     /**
      * Deletes the users address by the specified user ID.
      *
      * @param id the ID of the user
      * @throws EntityNotFoundException if no user is found with the specified ID
      */
     @Transactional
     @Override
     public void deleteUsersAddress(Long id) {
         var existingUser = getById(id);
         existingUser.setAddress(null);
     }
 
     @Transactional
     @Override
     public User updatePhone(Long id, Phone phone) {
        var user = getById(id);
        user.setPhone(user.getPhone());
        return user;
     }
 
     /**
      * Deletes the user with the specified ID.
      *
      * @param id the ID of the user to delete
      * @throws EntityNotFoundException if no user is found with the specified ID
      */
     @Transactional
     @Override
     public void deleteById(Long id) {
         var existingUser = getById(id);
         userRepository.delete(existingUser);
     }
 
     /**
      * Checks if the provided email is already taken in the database.
      *
      * @param email the email to check for existence
      * @throws EntityAlreadyExistsException if the email is already taken
      */
     private void assertThatEmailNotTaken(String email) {
         if (userRepository.existsByEmail(email)) {
             throw new EntityAlreadyExistsException("User with this email already registered");
         }
     }
 
     /**
      * Checks if the provided age is legal, i.e., greater than or equal to the
      * required minimum age.
      *
      * @param user the user whose age should be checked
      * @throws UserValidationException if the user's age is below the required
      *                                 age
      */
     private void assertThatAgeIsLegal(User user) {
         int requiredAge = userConstraints.getRequiredAge();
         long userAge = ChronoUnit.YEARS.between(user.getBirthDate(), LocalDate.now());
         if (userAge < requiredAge) {
             var message = String.format("Users age must be greater than or equal to %d", requiredAge);
             throw new UserValidationException(message);
         }
     }
 
     private Specification<User> inRange(DateRange dateRange) {
         if (dateRange == null || !dateRange.isSet()) return Specification.where(null);
 
         return (root, query, criteriaBuilder) -> {
             query.distinct(true);
             var birthDate = root.get("birthDate").as(LocalDate.class);
 
             if ((dateRange.getFrom() == null)) {
                 return criteriaBuilder.lessThanOrEqualTo(birthDate, dateRange.getTo());
             } else if (dateRange.getTo() == null) {
                 return criteriaBuilder.greaterThanOrEqualTo(birthDate, dateRange.getFrom());
             }
             return criteriaBuilder.between(birthDate, dateRange.from(), dateRange.to());
         };
     }
 }