UserServiceImpl.java

package com.nashtech.rookie.asset_management_0701.services.user;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Set;

import org.springframework.cache.CacheManager;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.nashtech.rookie.asset_management_0701.constants.DefaultSortOptions;
import com.nashtech.rookie.asset_management_0701.dtos.requests.user.ChangePasswordRequest;
import com.nashtech.rookie.asset_management_0701.dtos.requests.user.FirstChangePasswordRequest;
import com.nashtech.rookie.asset_management_0701.dtos.requests.user.UserRequest;
import com.nashtech.rookie.asset_management_0701.dtos.requests.user.UserSearchDto;
import com.nashtech.rookie.asset_management_0701.dtos.requests.user.UserUpdateRequest;
import com.nashtech.rookie.asset_management_0701.dtos.responses.PaginationResponse;
import com.nashtech.rookie.asset_management_0701.dtos.responses.user.UserResponse;
import com.nashtech.rookie.asset_management_0701.entities.User;
import com.nashtech.rookie.asset_management_0701.enums.EAssignmentState;
import com.nashtech.rookie.asset_management_0701.enums.ERole;
import com.nashtech.rookie.asset_management_0701.enums.EUserStatus;
import com.nashtech.rookie.asset_management_0701.exceptions.AppException;
import com.nashtech.rookie.asset_management_0701.exceptions.ErrorCode;
import com.nashtech.rookie.asset_management_0701.mappers.UserMapper;
import com.nashtech.rookie.asset_management_0701.repositories.AssignmentRepository;
import com.nashtech.rookie.asset_management_0701.repositories.LocationRepository;
import com.nashtech.rookie.asset_management_0701.repositories.UserRepository;
import com.nashtech.rookie.asset_management_0701.services.assignment.AssignmentSpecification;
import com.nashtech.rookie.asset_management_0701.utils.PageSortUtil;
import com.nashtech.rookie.asset_management_0701.utils.auth_util.AuthUtil;
import com.nashtech.rookie.asset_management_0701.utils.user.UserUtil;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserServiceImpl implements UserService {
    private final AssignmentRepository assignmentRepository;
    private final UserRepository userRepository;
    private final LocationRepository locationRepository;
    private final UserMapper userMapper;
    private final PasswordEncoder passwordEncoder;
    private final AuthUtil authUtil;
    private final UserUtil userUtil;
    private final CacheManager cacheManager;

    @Override
    @Transactional
    public UserResponse createUser (UserRequest userRequest) {
        if (ERole.ADMIN.equals(userRequest.getRole()) && userRequest.getLocationId() == null) {
            throw new AppException(ErrorCode.ADMIN_NULL_LOCATION);
        }
        validateJoinDate(userRequest.getDob(), userRequest.getJoinDate());
        DateTimeFormatter newFormatter = DateTimeFormatter.ofPattern("ddMMyyyy");
        String username = userUtil.generateUsername(userRequest);

        User user = userMapper.toUser(userRequest);

        if (userRequest.getRole().equals(ERole.USER)) {
            User admin = authUtil.getCurrentUser();
            userRequest.setLocationId(admin.getLocation().getId());
            user.setLocation(admin.getLocation());
        }
        else {
            user.setLocation(locationRepository.findById(userRequest.getLocationId()).orElseThrow());
        }

        user.setUsername(username);
        user.setHashPassword(passwordEncoder.encode(username + "@" + newFormatter.format(userRequest.getDob())));
        user.setStatus(EUserStatus.FIRST_LOGIN);
        userRepository.save(user);
        user.generateStaffCode();
        return userMapper.toUserResponse(user);
    }

    @Override
    public String generateUsername (String firstName, String lastName) {
        return userUtil.generateUsernameFromWeb(firstName, lastName);
    }

    private void validateJoinDate (LocalDate dob, LocalDate joinDate) {
        if (joinDate.isBefore(dob)) {
            throw new AppException(ErrorCode.JOIN_DATE_BEFORE_DOB);
        }

        DayOfWeek dayOfWeek = joinDate.getDayOfWeek();
        if (dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY) {
            throw new AppException(ErrorCode.JOIN_DATE_WEEKEND);
        }
    }

    @Override
    public PaginationResponse<UserResponse> getAllUser (UserSearchDto dto) {
        return getUserPaginationResponse(dto, true);
    }

    @Override
    public PaginationResponse<UserResponse> getAllUserAssignment (UserSearchDto dto) {
        return getUserPaginationResponse(dto, false);
    }

    private PaginationResponse<UserResponse> getUserPaginationResponse (UserSearchDto dto, boolean excludeCurrentUser) {
        var pageRequest = PageSortUtil.createPageRequest(
                dto.getPageNumber() - 1,
                dto.getPageSize(),
                dto.getOrderBy().equals("type") ? "role" : dto.getOrderBy(),
                PageSortUtil.parseSortDirection(dto.getSortDir()),
                DefaultSortOptions.DEFAULT_USER_SORT_BY);

        var searchString = dto.getSearchString();
        var currentUser = authUtil.getCurrentUser();
        var currentLocation = currentUser.getLocation();

        var specification = Specification.where(UserSpecification.hasNameContains(searchString))
                .or(UserSpecification.hasStaffCodeContains(searchString))
                .and(UserSpecification.hasRole(dto.getType()))
                .and(UserSpecification.hasLocation(currentLocation))
                .and(UserSpecification.isNotDisabled());

        if (excludeCurrentUser) {
            specification = specification.and(UserSpecification.excludeUser(currentUser));
        }

        var users = userRepository.findAll(specification, pageRequest);

        return PaginationResponse.<UserResponse>builder()
                .page(pageRequest.getPageNumber() + 1)
                .total(users.getTotalElements())
                .itemsPerPage(pageRequest.getPageSize())
                .data(users.map(userMapper::toUserResponse).toList())
                .build();
    }

    @Override
    @Transactional
    public void firstChangePassword (FirstChangePasswordRequest firstChangePasswordRequest) {
        User user = authUtil.getCurrentUser();

        if (!user.getStatus().equals(EUserStatus.FIRST_LOGIN)){
            throw new AppException(ErrorCode.PASSWORD_CHANGED);
        }

        user.setHashPassword(passwordEncoder.encode(firstChangePasswordRequest.getPassword()));

        user.setStatus(EUserStatus.ACTIVE);
        userRepository.save(user);
    }

    @Override
    @Transactional
    public void changePassword (ChangePasswordRequest changePasswordRequest) {
        if (changePasswordRequest.getNewPassword().equals(changePasswordRequest.getPassword())) {
            throw new AppException(ErrorCode.PASSWORD_SAME);
        }
        User user = authUtil.getCurrentUser();
        if (!passwordEncoder.matches(changePasswordRequest.getPassword(), user.getHashPassword())) {
            throw new AppException(ErrorCode.WRONG_PASSWORD);
        }

        user.setHashPassword(passwordEncoder.encode(changePasswordRequest.getNewPassword()));
        userRepository.save(user);
    }

    @Override
    @Transactional
    public void disableUser (Long id) {
        User userToDisable = userRepository.findById(id)
            .orElseThrow(()-> new AppException(ErrorCode.USER_NOT_FOUND));

        if (userToDisable.getStatus().equals(EUserStatus.DISABLED)){
            throw new AppException(ErrorCode.USER_NOT_FOUND);
        }

        User currentAdmin = authUtil.getCurrentUser();
        if (!currentAdmin.getLocation().equals(userToDisable.getLocation())){
            throw new AppException(ErrorCode.USER_NOT_FOUND);
        }

        if (existsCurrentAssignment(id)){
            throw new AppException(ErrorCode.USER_STILL_OWNS_VALID_ASSIGNMENTS);
        }

        userToDisable.setStatus(EUserStatus.DISABLED);
        userRepository.save(userToDisable);
        cacheManager.getCache("userDisable").evict(userToDisable.getUsername());
    }

    public Boolean existsCurrentAssignment (Long userId) {
        User assignee = userRepository.findById(userId)
            .orElseThrow(()-> new AppException(ErrorCode.USER_NOT_FOUND));
        if (assignee.getStatus().equals(EUserStatus.DISABLED)){
            throw new AppException(ErrorCode.USER_NOT_FOUND);
        }
        String assigneeUsername = assignee.getUsername();
        return assignmentRepository.exists(Specification.where(
            AssignmentSpecification.hasAssigneeUsername(assigneeUsername))
            .and(AssignmentSpecification.hasStates(
                Set.of(EAssignmentState.ACCEPTED, EAssignmentState.WAITING))));
    }

    @Override
    public UserResponse getUserById (Long id) {
        return userMapper.toUserResponse(userRepository.findById(id).orElseThrow(
                ()-> new AppException(ErrorCode.USER_NOT_FOUND)));
    }

    @Override
    @Transactional
    public UserResponse editUser (Long id, UserUpdateRequest userUpdateRequest) {
        User existUser = userRepository.findByIdAndLocation(id, authUtil.getCurrentUser().getLocation())
                .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND));

        if (!existUser.getVersion().equals(userUpdateRequest.getVersion())) {
            throw new AppException(ErrorCode.DATA_IS_OLD);
        }
        validateJoinDate(userUpdateRequest.getDob(), userUpdateRequest.getJoinDate());
        existUser.setDob(userUpdateRequest.getDob());
        existUser.setGender(userUpdateRequest.getGender());
        existUser.setJoinDate(userUpdateRequest.getJoinDate());
        existUser.setRole(userUpdateRequest.getType());
        try {
            userRepository.save(existUser);
        }
        catch (OptimisticLockingFailureException e) {
            throw new AppException(ErrorCode.DATA_IS_OLD);
        }
        return userMapper.toUserResponse(existUser);
    }
}