diff --git a/src/main/java/school/faang/user_service/controller/goal/GoalController.java b/src/main/java/school/faang/user_service/controller/goal/GoalController.java new file mode 100644 index 0000000000..650ccb28d3 --- /dev/null +++ b/src/main/java/school/faang/user_service/controller/goal/GoalController.java @@ -0,0 +1,78 @@ +package school.faang.user_service.controller.goal; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import school.faang.user_service.entity.filter.GoalFilterDto; +import school.faang.user_service.entity.goal.Goal; +import school.faang.user_service.entity.goal.dto.request.CreateGoalDto; +import school.faang.user_service.entity.goal.dto.request.UpdateGoalDto; +import school.faang.user_service.entity.goal.dto.response.GoalDto; +import school.faang.user_service.entity.goal.mapper.GoalMapper; +import school.faang.user_service.service.goal.GoalService; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/goals") +@RequiredArgsConstructor +public class GoalController { + + private final GoalService goalService; + private final GoalMapper goalMapper; + + @GetMapping("/{goalId}") + public ResponseEntity getGoal(@PathVariable long goalId) { + Goal goal = goalService.getGoalById(goalId); + return ResponseEntity.ok(goalMapper.toGoalDto(goal)); + } + + @PostMapping("/filter") + public ResponseEntity> getGoals(@RequestBody GoalFilterDto goalFilterDto) { + List filteredGoals = goalService.getGoalsByFilter(goalFilterDto); + return ResponseEntity.ok(goalMapper.toGoalDtoList(filteredGoals)); + } + + @PostMapping("/{parentId}/subGoals/filter") + public ResponseEntity> getSubGoals(@PathVariable long parentId, @RequestBody GoalFilterDto goalFilterDto) { + List filteredSubGoals = goalService.getSubGoalsByFilter(parentId, goalFilterDto); + return ResponseEntity.ok(goalMapper.toGoalDtoList(filteredSubGoals)); + } + + @PostMapping + public ResponseEntity createGoal(@RequestBody @Valid CreateGoalDto goalDto) { + Goal createdGoal = goalService.createGoal( + goalMapper.toGoal(goalDto), + goalDto.skillsId(), + goalDto.parentId() + ); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(goalMapper.toGoalDto(createdGoal)); + } + + @PatchMapping("/{goalId}") + public ResponseEntity updateGoal(@PathVariable long goalId, @RequestBody @Valid UpdateGoalDto goalDto) { + Goal createdGoal = goalService.update( + goalId, + goalMapper.toGoal(goalDto), + goalDto.skillsId() + ); + return ResponseEntity.ok(goalMapper.toGoalDto(createdGoal)); + } + + @DeleteMapping("/{goalId}") + public ResponseEntity deleteGoal(@PathVariable long goalId) { + goalService.delete(goalId); + return ResponseEntity.noContent().build(); + } +} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/entity/filter/GoalFilterDto.java b/src/main/java/school/faang/user_service/entity/filter/GoalFilterDto.java new file mode 100644 index 0000000000..af46e063e7 --- /dev/null +++ b/src/main/java/school/faang/user_service/entity/filter/GoalFilterDto.java @@ -0,0 +1,17 @@ +package school.faang.user_service.entity.filter; + +import com.fasterxml.jackson.annotation.JsonInclude; +import school.faang.user_service.entity.goal.GoalStatus; + +import java.time.LocalDateTime; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public record GoalFilterDto( + List usersId, + String title, + GoalStatus status, + List skillsId, + LocalDateTime deadline +) { +} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/entity/goal/dto/request/CreateGoalDto.java b/src/main/java/school/faang/user_service/entity/goal/dto/request/CreateGoalDto.java new file mode 100644 index 0000000000..6e5d6d6679 --- /dev/null +++ b/src/main/java/school/faang/user_service/entity/goal/dto/request/CreateGoalDto.java @@ -0,0 +1,18 @@ +package school.faang.user_service.entity.goal.dto.request; + +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDateTime; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record CreateGoalDto( + @NotBlank(message = "Empty goal title not allowed!") String title, + String description, + Long parentId, + @NotNull List skillsId, + @Future(message = "Deadline must be in future!") LocalDateTime deadline +) {} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/entity/goal/dto/request/UpdateGoalDto.java b/src/main/java/school/faang/user_service/entity/goal/dto/request/UpdateGoalDto.java new file mode 100644 index 0000000000..51851431a1 --- /dev/null +++ b/src/main/java/school/faang/user_service/entity/goal/dto/request/UpdateGoalDto.java @@ -0,0 +1,19 @@ +package school.faang.user_service.entity.goal.dto.request; + +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import school.faang.user_service.entity.goal.GoalStatus; + +import java.time.LocalDateTime; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record UpdateGoalDto( + @NotBlank(message = "Empty goal title not allowed!") String title, + String description, + GoalStatus status, + @NotNull List skillsId, + @Future(message = "Dead line must be in future!") LocalDateTime deadline +) {} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/entity/goal/dto/response/GoalDto.java b/src/main/java/school/faang/user_service/entity/goal/dto/response/GoalDto.java new file mode 100644 index 0000000000..162180c7ef --- /dev/null +++ b/src/main/java/school/faang/user_service/entity/goal/dto/response/GoalDto.java @@ -0,0 +1,18 @@ +package school.faang.user_service.entity.goal.dto.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import school.faang.user_service.entity.goal.GoalStatus; + +import java.time.LocalDateTime; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record GoalDto( + Long id, + String title, + String description, + Long parentId, + GoalStatus status, + List skillsId, + LocalDateTime deadline +) {} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/entity/goal/mapper/GoalMapper.java b/src/main/java/school/faang/user_service/entity/goal/mapper/GoalMapper.java new file mode 100644 index 0000000000..058ef19ced --- /dev/null +++ b/src/main/java/school/faang/user_service/entity/goal/mapper/GoalMapper.java @@ -0,0 +1,49 @@ +package school.faang.user_service.entity.goal.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import school.faang.user_service.entity.Skill; +import school.faang.user_service.entity.goal.Goal; +import school.faang.user_service.entity.goal.dto.request.CreateGoalDto; +import school.faang.user_service.entity.goal.dto.request.UpdateGoalDto; +import school.faang.user_service.entity.goal.dto.response.GoalDto; + +import java.util.List; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface GoalMapper { + + @Mapping(source = "parent.id", target = "parentId") + @Mapping(target = "skillsId", expression = "java(mapSkillsToIds(goal.getSkillsToAchieve()))") + GoalDto toGoalDto(Goal goal); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "status", ignore = true) + @Mapping(target = "parent", ignore = true) + @Mapping(target = "skillsToAchieve", ignore = true) + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + @Mapping(target = "mentor", ignore = true) + @Mapping(target = "invitations", ignore = true) + @Mapping(target = "users", ignore = true) + Goal toGoal(CreateGoalDto goalDto); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "parent", ignore = true) + @Mapping(target = "skillsToAchieve", ignore = true) + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + @Mapping(target = "mentor", ignore = true) + @Mapping(target = "invitations", ignore = true) + @Mapping(target = "users", ignore = true) + Goal toGoal(UpdateGoalDto goalDto); + + default List mapSkillsToIds(List skills) { + return skills.stream() + .map(Skill::getId) + .toList(); + } + + List toGoalDtoList(List goals); +} diff --git a/src/main/java/school/faang/user_service/exception/goal/GoalNotExistException.java b/src/main/java/school/faang/user_service/exception/goal/GoalNotExistException.java new file mode 100644 index 0000000000..2d90edcce8 --- /dev/null +++ b/src/main/java/school/faang/user_service/exception/goal/GoalNotExistException.java @@ -0,0 +1,8 @@ +package school.faang.user_service.exception.goal; + +public class GoalNotExistException extends RuntimeException { + + public GoalNotExistException(long goalId) { + super("Goal with id %d not exist".formatted(goalId)); + } +} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/exception/goal/MaxActiveGoalPerUserException.java b/src/main/java/school/faang/user_service/exception/goal/MaxActiveGoalPerUserException.java new file mode 100644 index 0000000000..7d4f86dd67 --- /dev/null +++ b/src/main/java/school/faang/user_service/exception/goal/MaxActiveGoalPerUserException.java @@ -0,0 +1,8 @@ +package school.faang.user_service.exception.goal; + +public class MaxActiveGoalPerUserException extends RuntimeException { + + public MaxActiveGoalPerUserException(Long userId, int goalLimit) { + super("User with id %s reach max goals active limit of %d".formatted(userId, goalLimit)); + } +} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/exception/goal/UpdateComleteGoalException.java b/src/main/java/school/faang/user_service/exception/goal/UpdateComleteGoalException.java new file mode 100644 index 0000000000..dedd8c9c31 --- /dev/null +++ b/src/main/java/school/faang/user_service/exception/goal/UpdateComleteGoalException.java @@ -0,0 +1,8 @@ +package school.faang.user_service.exception.goal; + +public class UpdateComleteGoalException extends RuntimeException { + + public UpdateComleteGoalException(long goalId) { + super("Goal with id %d is complete. Update not allowed.".formatted(goalId)); + } +} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/exception/goal/UpdateGoalWithActiveSubGoalsException.java b/src/main/java/school/faang/user_service/exception/goal/UpdateGoalWithActiveSubGoalsException.java new file mode 100644 index 0000000000..dde72e1ef1 --- /dev/null +++ b/src/main/java/school/faang/user_service/exception/goal/UpdateGoalWithActiveSubGoalsException.java @@ -0,0 +1,8 @@ +package school.faang.user_service.exception.goal; + +public class UpdateGoalWithActiveSubGoalsException extends RuntimeException { + + public UpdateGoalWithActiveSubGoalsException(long goalId, String activeSubGoalsId) { + super("Goal with id %d have active sub goals [%s]. Update not allowed.".formatted(goalId, activeSubGoalsId)); + } +} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/exception/goal/UserNotGoalOwnerException.java b/src/main/java/school/faang/user_service/exception/goal/UserNotGoalOwnerException.java new file mode 100644 index 0000000000..a5231b249c --- /dev/null +++ b/src/main/java/school/faang/user_service/exception/goal/UserNotGoalOwnerException.java @@ -0,0 +1,8 @@ +package school.faang.user_service.exception.goal; + +public class UserNotGoalOwnerException extends RuntimeException { + + public UserNotGoalOwnerException(long userId, long goalId) { + super("User with id %d not the owner of goal with id %d".formatted(userId, goalId)); + } +} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/exception/skill/SkillNotExistException.java b/src/main/java/school/faang/user_service/exception/skill/SkillNotExistException.java new file mode 100644 index 0000000000..49294d476e --- /dev/null +++ b/src/main/java/school/faang/user_service/exception/skill/SkillNotExistException.java @@ -0,0 +1,12 @@ +package school.faang.user_service.exception.skill; + +public class SkillNotExistException extends RuntimeException { + + public SkillNotExistException(String skillsId) { + super("Contains skills [%s] that not exist!!".formatted(skillsId)); + } + + public SkillNotExistException(Long skillId) { + super("Skill with id - '%d' not exist!!".formatted(skillId)); + } +} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/filter/Filter.java b/src/main/java/school/faang/user_service/filter/Filter.java new file mode 100644 index 0000000000..d93c1a827b --- /dev/null +++ b/src/main/java/school/faang/user_service/filter/Filter.java @@ -0,0 +1,8 @@ +package school.faang.user_service.filter; + +public interface Filter { + + boolean isApplicable(T filterDto); + + P apply(P filteredData, T filterDto); +} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/filter/goal/GoalFilter.java b/src/main/java/school/faang/user_service/filter/goal/GoalFilter.java new file mode 100644 index 0000000000..7dabb82707 --- /dev/null +++ b/src/main/java/school/faang/user_service/filter/goal/GoalFilter.java @@ -0,0 +1,9 @@ +package school.faang.user_service.filter.goal; + +import school.faang.user_service.entity.filter.GoalFilterDto; +import school.faang.user_service.entity.goal.Goal; +import school.faang.user_service.filter.Filter; + +import java.util.stream.Stream; + +public interface GoalFilter extends Filter> {} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/filter/goal/GoalFilterBySkills.java b/src/main/java/school/faang/user_service/filter/goal/GoalFilterBySkills.java new file mode 100644 index 0000000000..7177c103a1 --- /dev/null +++ b/src/main/java/school/faang/user_service/filter/goal/GoalFilterBySkills.java @@ -0,0 +1,26 @@ +package school.faang.user_service.filter.goal; + +import org.springframework.stereotype.Component; +import school.faang.user_service.entity.filter.GoalFilterDto; +import school.faang.user_service.entity.goal.Goal; + +import java.util.Objects; +import java.util.stream.Stream; + +@Component +public class GoalFilterBySkills implements GoalFilter { + + @Override + public boolean isApplicable(GoalFilterDto filterDto) { + return Objects.nonNull(filterDto.skillsId()) && !filterDto.skillsId().isEmpty(); + } + + @Override + public Stream apply(Stream filteredData, GoalFilterDto filterDto) { + return filteredData.filter(goal -> Objects.nonNull(goal.getSkillsToAchieve()) + && goal.getSkillsToAchieve() + .stream() + .anyMatch(skill -> filterDto.skillsId().contains(skill.getId())) + ); + } +} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/filter/goal/GoalFilterByStatus.java b/src/main/java/school/faang/user_service/filter/goal/GoalFilterByStatus.java new file mode 100644 index 0000000000..73d15875d2 --- /dev/null +++ b/src/main/java/school/faang/user_service/filter/goal/GoalFilterByStatus.java @@ -0,0 +1,23 @@ +package school.faang.user_service.filter.goal; + +import org.springframework.stereotype.Component; +import school.faang.user_service.entity.filter.GoalFilterDto; +import school.faang.user_service.entity.goal.Goal; + +import java.util.Objects; +import java.util.stream.Stream; + +@Component +public class GoalFilterByStatus implements GoalFilter { + + @Override + public boolean isApplicable(GoalFilterDto filterDto) { + return Objects.nonNull(filterDto.status()); + } + + @Override + public Stream apply(Stream filteredData, GoalFilterDto filterDto) { + return filteredData.filter(goal -> Objects.nonNull(goal.getStatus()) + && goal.getStatus() == filterDto.status()); + } +} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/filter/goal/GoalFilterByTitle.java b/src/main/java/school/faang/user_service/filter/goal/GoalFilterByTitle.java new file mode 100644 index 0000000000..cc7831a526 --- /dev/null +++ b/src/main/java/school/faang/user_service/filter/goal/GoalFilterByTitle.java @@ -0,0 +1,25 @@ +package school.faang.user_service.filter.goal; + +import org.springframework.stereotype.Component; +import school.faang.user_service.entity.filter.GoalFilterDto; +import school.faang.user_service.entity.goal.Goal; + +import java.util.Objects; +import java.util.stream.Stream; + +@Component +public class GoalFilterByTitle implements GoalFilter { + + @Override + public boolean isApplicable(GoalFilterDto filterDto) { + return Objects.nonNull(filterDto.title()); + } + + @Override + public Stream apply(Stream filteredData, GoalFilterDto filterDto) { + return filteredData.filter(goal -> Objects.nonNull(goal.getTitle()) + && goal.getTitle() + .toLowerCase() + .contains(filterDto.title().toLowerCase())); + } +} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/filter/goal/GoalFilterByUsers.java b/src/main/java/school/faang/user_service/filter/goal/GoalFilterByUsers.java new file mode 100644 index 0000000000..7b47cc457d --- /dev/null +++ b/src/main/java/school/faang/user_service/filter/goal/GoalFilterByUsers.java @@ -0,0 +1,26 @@ +package school.faang.user_service.filter.goal; + +import org.springframework.stereotype.Component; +import school.faang.user_service.entity.filter.GoalFilterDto; +import school.faang.user_service.entity.goal.Goal; + +import java.util.Objects; +import java.util.stream.Stream; + +@Component +public class GoalFilterByUsers implements GoalFilter { + + @Override + public boolean isApplicable(GoalFilterDto filterDto) { + return Objects.nonNull(filterDto.usersId()) && !filterDto.usersId().isEmpty(); + } + + @Override + public Stream apply(Stream filteredData, GoalFilterDto filterDto) { + return filteredData.filter(goal -> Objects.nonNull(goal.getUsers()) + && goal.getUsers() + .stream() + .anyMatch(user -> filterDto.usersId().contains(user.getId())) + ); + } +} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/repository/SkillRepository.java b/src/main/java/school/faang/user_service/repository/SkillRepository.java index b231162217..6ac8884744 100644 --- a/src/main/java/school/faang/user_service/repository/SkillRepository.java +++ b/src/main/java/school/faang/user_service/repository/SkillRepository.java @@ -40,6 +40,16 @@ public interface SkillRepository extends JpaRepository { @Modifying void assignSkillToUser(long skillId, long userId); + @Query(nativeQuery = true, value = "INSERT INTO goal_skill (goal_id, skill_id) VALUES (:goalId, :skillId)") + @Modifying + void assignSkillToGoal(long goalId, long skillId); + + @Query(nativeQuery = true, value = """ + DELETE FROM goal_skill + WHERE goal_id = :goalId""") + @Modifying + void removeSkillsFromGoal(Long goalId); + @Query(nativeQuery = true, value = """ SELECT s.* FROM skill s WHERE s.id IN (SELECT gs.skill_id FROM goal_skill gs diff --git a/src/main/java/school/faang/user_service/repository/goal/GoalRepository.java b/src/main/java/school/faang/user_service/repository/goal/GoalRepository.java index 5bb9213945..acbc7e22d3 100644 --- a/src/main/java/school/faang/user_service/repository/goal/GoalRepository.java +++ b/src/main/java/school/faang/user_service/repository/goal/GoalRepository.java @@ -1,8 +1,8 @@ package school.faang.user_service.repository.goal; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; -import school.faang.user_service.entity.User; import school.faang.user_service.entity.goal.Goal; import java.util.List; @@ -46,5 +46,12 @@ WITH RECURSIVE subtasks AS ( JOIN user_goal ug ON u.id = ug.user_id WHERE ug.goal_id = :goalId """) - List findUsersByGoalId(long goalId); + List findUsersByGoalId(long goalId); + + @Modifying + @Query(nativeQuery = true, value = """ + DELETE FROM user_goal ug + WHERE ug.user_id = :userId AND ug.goal_id = :goalId + """) + void removeGoalFromUser(long userId, long goalId); } diff --git a/src/main/java/school/faang/user_service/service/SkillService.java b/src/main/java/school/faang/user_service/service/SkillService.java new file mode 100644 index 0000000000..a40d4f65ad --- /dev/null +++ b/src/main/java/school/faang/user_service/service/SkillService.java @@ -0,0 +1,45 @@ +package school.faang.user_service.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import school.faang.user_service.entity.Skill; +import school.faang.user_service.exception.skill.SkillNotExistException; +import school.faang.user_service.repository.SkillRepository; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class SkillService { + + private final SkillRepository skillRepository; + + @Transactional(readOnly = true) + public Skill getSkillById(long skillId) { + return skillRepository.findById(skillId) + .orElseThrow(() -> new SkillNotExistException(skillId)); + } + + @Transactional + public void removeSkillForGoal(long goalId) { + skillRepository.removeSkillsFromGoal(goalId); + } + + @Transactional + public void assignSkillsToUser(long userId, List skills) { + List ownedSkills = skillRepository.findAllByUserId(userId); + skills + .stream() + .filter(skill -> !ownedSkills.contains(skill)) + .forEach(skill -> skillRepository.assignSkillToUser(skill.getId(), userId)); + } + + @Transactional(readOnly = true) + public List getSkillsById(List skillsId) { + return skillsId.stream() + .map(this::getSkillById) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/service/goal/GoalService.java b/src/main/java/school/faang/user_service/service/goal/GoalService.java new file mode 100644 index 0000000000..a3d192a543 --- /dev/null +++ b/src/main/java/school/faang/user_service/service/goal/GoalService.java @@ -0,0 +1,144 @@ +package school.faang.user_service.service.goal; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import school.faang.user_service.config.context.UserContext; +import school.faang.user_service.entity.Skill; +import school.faang.user_service.entity.filter.GoalFilterDto; +import school.faang.user_service.entity.goal.Goal; +import school.faang.user_service.entity.goal.GoalStatus; +import school.faang.user_service.exception.goal.GoalNotExistException; +import school.faang.user_service.exception.goal.UpdateComleteGoalException; +import school.faang.user_service.exception.goal.UserNotGoalOwnerException; +import school.faang.user_service.filter.goal.GoalFilter; +import school.faang.user_service.repository.goal.GoalRepository; +import school.faang.user_service.service.SkillService; +import school.faang.user_service.service.user.UserService; +import school.faang.user_service.validator.goal.GoalValidator; +import school.faang.user_service.validator.goal.SkillValidator; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import static school.faang.user_service.validation.ValidationUtils.setIfNotNull; + +@Service +@RequiredArgsConstructor +public class GoalService { + + private final UserContext userContext; + private final GoalRepository goalRepository; + private final GoalValidator goalValidator; + + private final SkillService skillService; + private final SkillValidator skillValidator; + + private final UserService userService; + + private final List filters; + + @Transactional + public Goal createGoal(Goal newGoalData, List skillsId, Long parentId) { + long userId = userContext.getUserId(); + + goalValidator.validateMaxActiveGoalLimitPerUser(userId); + skillValidator.validateExistingSkills(skillsId); + + newGoalData.setUsers(userService.getUsersById(List.of(userContext.getUserId()))); + newGoalData.setStatus(GoalStatus.ACTIVE); + newGoalData.setSkillsToAchieve(skillsId.isEmpty() ? new ArrayList<>() : skillService.getSkillsById(skillsId)); + setIfNotNull(parentId, id -> newGoalData.setParent(getGoalById(id))); + + return goalRepository.save(newGoalData); + } + + @Transactional + public Goal update(long goalId, Goal newGoalData, List skillsId) { + Goal dbGoal = getGoalById(goalId); + + if (dbGoal.getStatus() == GoalStatus.COMPLETED) { + throw new UpdateComleteGoalException(goalId); + } + + skillValidator.validateExistingSkills(skillsId); + + updateGoalEntity(dbGoal, newGoalData, skillsId); + + if (dbGoal.getStatus() == GoalStatus.COMPLETED) { + List involvedUsersId = goalRepository.findUsersByGoalId(dbGoal.getId()); + + goalValidator.validateAllSubGoalsCompleted(goalId, goalRepository.findByParent(goalId)); + + involvedUsersId.forEach(userId -> + skillService.assignSkillsToUser(userId, dbGoal.getSkillsToAchieve())); + } + + return goalRepository.save(dbGoal); + } + + @Transactional + public void delete(long goalId) { + Goal goal = getGoalById(goalId); + + boolean userNotOwner = goal.getUsers() + .stream() + .noneMatch(user -> Objects.equals(user.getId(), userContext.getUserId())); + + if (userNotOwner) { + throw new UserNotGoalOwnerException(userContext.getUserId(), goalId); + } + + goalRepository.removeGoalFromUser(userContext.getUserId(), goalId); + + if (goalRepository.findUsersByGoalId(goalId).isEmpty()) { + goalRepository.findByParent(goalId).forEach(goalRepository::delete); + goalRepository.delete(goal); + } + } + + @Transactional(readOnly = true) + public Goal getGoalById(long goalId) { + return goalRepository.findById(goalId).orElseThrow(() -> new GoalNotExistException(goalId)); + } + + @Transactional(readOnly = true) + public List getGoalsByFilter(GoalFilterDto goalFilterDto) { + return filterGoals(goalRepository.findAll().stream(), goalFilterDto).toList(); + } + + @Transactional(readOnly = true) + public List getSubGoalsByFilter(long parentId, GoalFilterDto goalFilterDto) { + return filterGoals(goalRepository.findByParent(parentId), goalFilterDto).toList(); + } + + private Stream filterGoals(Stream goalStream, GoalFilterDto goalFilterDto) { + return filters.stream() + .filter(filter -> filter.isApplicable(goalFilterDto)) + .reduce(goalStream, + (currentStream, filter) -> filter.apply(currentStream, goalFilterDto), + Stream::concat); + } + + private void updateGoalEntity(Goal targetGoal, Goal newGoalData, List skillsId) { + setIfNotNull(newGoalData.getTitle(), targetGoal::setTitle); + setIfNotNull(newGoalData.getDescription(), targetGoal::setDescription); + setIfNotNull(newGoalData.getStatus(), targetGoal::setStatus); + setIfNotNull(newGoalData.getDeadline(), targetGoal::setDeadline); + + if (isSkillsListUpdated(targetGoal, skillsId)) { + skillService.removeSkillForGoal(targetGoal.getId()); + targetGoal.setSkillsToAchieve(skillService.getSkillsById(skillsId)); + } + } + + private boolean isSkillsListUpdated(Goal targetGoal, List skillsId) { + return targetGoal.getSkillsToAchieve().size() != skillsId.size() + || !targetGoal.getSkillsToAchieve() + .stream() + .map(Skill::getId) + .allMatch(skillsId::contains); + } +} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/service/user/UserService.java b/src/main/java/school/faang/user_service/service/user/UserService.java new file mode 100644 index 0000000000..953b3268dc --- /dev/null +++ b/src/main/java/school/faang/user_service/service/user/UserService.java @@ -0,0 +1,32 @@ +package school.faang.user_service.service.user; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import school.faang.user_service.entity.User; +import school.faang.user_service.exception.users.UserNotFoundException; +import school.faang.user_service.repository.UserRepository; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + @Transactional(readOnly = true) + public User getUserById(long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(String.format("User with id %d not Found", userId))); + } + + @Transactional(readOnly = true) + public List getUsersById(List usersId) { + return usersId.stream() + .map(this::getUserById) + .collect(Collectors.toList()); + } + +} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/validation/ValidationUtils.java b/src/main/java/school/faang/user_service/validation/ValidationUtils.java index 175bde23bb..fcd843b1c1 100644 --- a/src/main/java/school/faang/user_service/validation/ValidationUtils.java +++ b/src/main/java/school/faang/user_service/validation/ValidationUtils.java @@ -3,6 +3,8 @@ import lombok.experimental.UtilityClass; import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; @UtilityClass public class ValidationUtils { @@ -12,4 +14,16 @@ public static void executeIfNotNull(Object field, Runnable runnable) { runnable.run(); } } + + public static void setIfNotNull(T fieldValue, Consumer setter) { + if (Objects.nonNull(fieldValue)) { + setter.accept(fieldValue); + } + } + + public static void setIfNotNullAndTrue(T fieldValue, Predicate predicate, Consumer setter) { + if (Objects.nonNull(fieldValue) && predicate.test(fieldValue)) { + setter.accept(fieldValue); + } + } } diff --git a/src/main/java/school/faang/user_service/validator/goal/GoalValidator.java b/src/main/java/school/faang/user_service/validator/goal/GoalValidator.java new file mode 100644 index 0000000000..e51d8f3791 --- /dev/null +++ b/src/main/java/school/faang/user_service/validator/goal/GoalValidator.java @@ -0,0 +1,40 @@ +package school.faang.user_service.validator.goal; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import school.faang.user_service.config.context.UserContext; +import school.faang.user_service.entity.goal.Goal; +import school.faang.user_service.entity.goal.GoalStatus; +import school.faang.user_service.exception.goal.MaxActiveGoalPerUserException; +import school.faang.user_service.exception.goal.UpdateGoalWithActiveSubGoalsException; +import school.faang.user_service.repository.goal.GoalRepository; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Component +@RequiredArgsConstructor +public class GoalValidator { + + @Value("${goal.maxLimit}") + private Integer goalLimit; + private final UserContext userContext; + private final GoalRepository goalRepository; + + public void validateMaxActiveGoalLimitPerUser(long userId) { + boolean isUserReachActiveGoalLimit = goalRepository.countActiveGoalsPerUser(userId) >= goalLimit; + if (isUserReachActiveGoalLimit) { + throw new MaxActiveGoalPerUserException(userContext.getUserId(), goalLimit); + } + } + + public void validateAllSubGoalsCompleted(long goalId, Stream subGoals) { + String activeSubGoalIds = subGoals.filter(goal -> !goal.getStatus().equals(GoalStatus.COMPLETED)) + .map(subGoal -> String.valueOf(subGoal.getId())) + .collect(Collectors.joining(", ")); + if (!activeSubGoalIds.isEmpty()) { + throw new UpdateGoalWithActiveSubGoalsException(goalId, activeSubGoalIds); + } + } +} \ No newline at end of file diff --git a/src/main/java/school/faang/user_service/validator/goal/SkillValidator.java b/src/main/java/school/faang/user_service/validator/goal/SkillValidator.java new file mode 100644 index 0000000000..28117aa114 --- /dev/null +++ b/src/main/java/school/faang/user_service/validator/goal/SkillValidator.java @@ -0,0 +1,32 @@ +package school.faang.user_service.validator.goal; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import school.faang.user_service.exception.skill.SkillNotExistException; +import school.faang.user_service.repository.SkillRepository; + +import java.util.List; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class SkillValidator { + + private final SkillRepository skillRepository; + + public void validateExistingSkills(List skillsId) { + if (skillsId.isEmpty()) { + return; + } + List absentSkillsId = skillsId.stream() + .filter(skillId -> !skillRepository.existsById(skillId)) + .toList(); + + if (!absentSkillsId.isEmpty()) { + String notExistingSkillsId = absentSkillsId.stream() + .map(String::valueOf) + .collect(Collectors.joining(", ")); + throw new SkillNotExistException(notExistingSkillsId); + } + } +} \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 52f30d6cb0..7514982b6f 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -34,4 +34,7 @@ project-service: payment-service: host: localhost - port: 9080 \ No newline at end of file + port: 9080 + +goal: + maxLimit: 3 \ No newline at end of file