Skip to content

Commit

Permalink
Merge pull request #6668 from ita-social-projects/habit-achievement
Browse files Browse the repository at this point in the history
Habit achievement
ospodaryk authored Nov 7, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents d7b3d03 + 669482a commit f7e8962
Showing 15 changed files with 467 additions and 63 deletions.
4 changes: 4 additions & 0 deletions dao/src/main/java/greencity/entity/UserAchievement.java
Original file line number Diff line number Diff line change
@@ -26,4 +26,8 @@ public class UserAchievement {

@Column
private boolean notified;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "habit_id", nullable = true)
private Habit habit;
}
4 changes: 4 additions & 0 deletions dao/src/main/java/greencity/entity/UserAction.java
Original file line number Diff line number Diff line change
@@ -26,4 +26,8 @@ public class UserAction {

@Column(name = "count")
private Integer count = 0;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "habit_id", nullable = true)
private Habit habit;
}
31 changes: 31 additions & 0 deletions dao/src/main/java/greencity/repository/AchievementRepo.java
Original file line number Diff line number Diff line change
@@ -12,6 +12,37 @@

@Repository
public interface AchievementRepo extends JpaRepository<Achievement, Long> {
/**
* Retrieves a list of achievements that a specific user hasn't achieved yet
* within a specified achievement category. The method identifies unachieved
* achievements by comparing user actions count with the conditions of
* achievements and by checking if a user already has the achievement in the
* user_achievements table.
*
* @param userId The unique identifier of the user.
* @param achievementCategoryId The unique identifier of the achievement
* category.
* @return A list of Achievement objects that the user hasn't achieved within
* the specified category.
*/
@Query(value = "SELECT ach.* "
+ "FROM achievements ach "
+ "WHERE ach.id IN ("
+ " SELECT achievement_id "
+ " FROM user_achievements uach "
+ " WHERE uach.user_id = :userId"
+ ") "
+ "AND ach.condition > ("
+ " SELECT ua.count "
+ " FROM user_actions ua "
+ " WHERE ua.user_id = :userId "
+ " AND ua.achievement_category_id = :achievementCategoryId"
+ ") "
+ "AND ach.achievement_category_id = :achievementCategoryId "
+ "AND ach.habit_id = :habitId",
nativeQuery = true)
List<Achievement> findUnAchieved(Long userId, Long achievementCategoryId, Long habitId);

/**
* Retrieves a list of achievements that a specific user hasn't achieved yet
* within a specified achievement category. The method identifies unachieved
17 changes: 17 additions & 0 deletions dao/src/main/java/greencity/repository/UserActionRepo.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package greencity.repository;

import greencity.entity.AchievementCategory;
import greencity.entity.Habit;
import greencity.entity.User;
import greencity.entity.UserAction;
import org.springframework.data.jpa.repository.JpaRepository;
@@ -21,4 +22,20 @@ public interface UserActionRepo extends JpaRepository<UserAction, Long> {
+ "WHERE ua.achievementCategory.id = :achievementCategoryId "
+ "AND ua.user.id = :userId")
UserAction findByUserIdAndAchievementCategoryId(Long userId, Long achievementCategoryId);

/**
* Method finds {@link UserAction} by userId, achievementCategoryId, and
* habitId.
*
* @param userId the ID of {@link User}
* @param achievementCategoryId the ID of {@link AchievementCategory}
* @param habitId the ID of {@link Habit}
* @return UserAction {@link UserAction}
* @author Oksana Spodaryk
*/
@Query(value = "SELECT ua FROM UserAction ua "
+ "WHERE ua.achievementCategory.id = :achievementCategoryId "
+ "AND ua.user.id = :userId "
+ "AND ua.habit.id = :habitId")
UserAction findByUserIdAndAchievementCategoryIdAndHabitId(Long userId, Long achievementCategoryId, Long habitId);
}
Original file line number Diff line number Diff line change
@@ -188,5 +188,6 @@
<include file="db/changelog/logs/ch-add-column-userLocation-Midianyi.xml"/>
<include file="db/changelog/logs/ch-drop-column-city-in-users-table-Midianyi.xml"/>
<include file="db/changelog/logs/ch-update-online-link-column-length-Sotnik.xml"/>
<include file="db/changelog/logs/ch-add-column-user_actions-Spodaryk.xml"/>
</databaseChangeLog>

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd">
<changeSet id="Spodaryk-28" author="Oksana Spodaryk">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="user_actions" columnName="habit_id"/>
</not>
</preConditions>
<addColumn tableName="user_actions">
<column name="habit_id" type="BIGINT">
<constraints nullable="true"/>
</column>
</addColumn>
<addForeignKeyConstraint baseColumnNames="habit_id"
baseTableName="user_actions"
constraintName="fk_user_actions_habit"
onDelete="RESTRICT"
onUpdate="RESTRICT"
referencedColumnNames="id"
referencedTableName="habits"/>
</changeSet>
<changeSet id="Spodaryk-29" author="Oksana Spodaryk">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="user_achievements" columnName="habit_id"/>
</not>
</preConditions>
<addColumn tableName="user_achievements">
<column name="habit_id" type="BIGINT">
<constraints nullable="true"/>
</column>
</addColumn>
<addForeignKeyConstraint baseColumnNames="habit_id"
baseTableName="user_achievements"
constraintName="fk_user_achievements_habit"
onDelete="RESTRICT"
onUpdate="RESTRICT"
referencedColumnNames="id"
referencedTableName="habits"/>
</changeSet>
</databaseChangeLog>
38 changes: 32 additions & 6 deletions service-api/src/main/java/greencity/service/UserActionService.java
Original file line number Diff line number Diff line change
@@ -3,8 +3,18 @@
import greencity.dto.achievementcategory.AchievementCategoryVO;
import greencity.dto.user.UserVO;
import greencity.dto.useraction.UserActionVO;
import greencity.dto.habit.HabitVO;

public interface UserActionService {
/**
* Method saves {@link UserActionVO}.
*
* @param userActionVO {@link UserActionVO}
* @return {@link UserActionVO}
* @author Orest Mamchuk
*/
UserActionVO save(UserActionVO userActionVO);

/**
* Method updates {@link UserActionVO}.
*
@@ -22,14 +32,30 @@ public interface UserActionService {
* @return {@link UserActionVO}
* @author Orest Mamchuk
*/
UserActionVO findUserActionByUserIdAndAchievementCategory(Long userId, Long categoryId);
UserActionVO findUserAction(Long userId, Long categoryId);

/**
* Method saves {@link UserActionVO}.
* Method finds {@link UserActionVO} by userId, achievementCategoryId, and
* habitId.
*
* @param userActionVO {@link UserActionVO}
* @return {@link UserActionVO}
* @author Orest Mamchuk
* @param userId the ID of {@link UserVO}
* @param categoryId the ID of {@link AchievementCategoryVO}
* @param habitId the ID of {@link HabitVO}
* @return UserAction {@link UserActionVO}
* @author Oksana Spodaryk
*/
UserActionVO save(UserActionVO userActionVO);
UserActionVO findUserAction(Long userId, Long categoryId, Long habitId);

/**
* Creates a new user action record in the system. This method is responsible
* for persisting a new user action associated with the specified user,
* category, and habit. It encapsulates the creation logic and ensures that the
* action is valid and properly recorded in the database.
*
* @param userId The ID of the user performing the action.
* @param categoryId The ID of the category associated with the action.
* @param habitId The ID of the habit related to the action.
* @return UserActionVO An object representing the newly created user action.
*/
UserActionVO createUserAction(Long userId, Long categoryId, Long habitId);
}
Original file line number Diff line number Diff line change
@@ -11,8 +11,10 @@
import greencity.enums.AchievementCategoryType;
import greencity.enums.AchievementAction;
import greencity.enums.RatingCalculationEnum;
import greencity.exception.exceptions.NotFoundException;
import greencity.rating.RatingCalculation;
import greencity.repository.AchievementRepo;
import greencity.repository.HabitRepo;
import greencity.repository.UserAchievementRepo;
import greencity.service.AchievementCategoryService;
import greencity.service.AchievementService;
@@ -24,7 +26,6 @@

import javax.transaction.Transactional;
import java.util.List;
import java.util.NoSuchElementException;

@Component
public class AchievementCalculation {
@@ -35,6 +36,7 @@ public class AchievementCalculation {
private final AchievementRepo achievementRepo;
private final RatingCalculation ratingCalculation;
private final ModelMapper modelMapper;
private final HabitRepo habitRepo;

/**
* Constructor for initializing the required services and repositories.
@@ -44,14 +46,16 @@ public AchievementCalculation(
@Lazy AchievementService achievementService,
AchievementCategoryService achievementCategoryService,
UserAchievementRepo userAchievementRepo,
AchievementRepo achievementRepo, RatingCalculation ratingCalculation, ModelMapper modelMapper) {
AchievementRepo achievementRepo, RatingCalculation ratingCalculation, ModelMapper modelMapper,
HabitRepo habitRepo) {
this.userActionService = userActionService;
this.achievementService = achievementService;
this.achievementCategoryService = achievementCategoryService;
this.userAchievementRepo = userAchievementRepo;
this.achievementRepo = achievementRepo;
this.ratingCalculation = ratingCalculation;
this.modelMapper = modelMapper;
this.habitRepo = habitRepo;
}

/**
@@ -66,39 +70,65 @@ public AchievementCalculation(
public void calculateAchievement(UserVO user, AchievementCategoryType category,
AchievementAction achievementAction) {
AchievementCategoryVO achievementCategoryVO = achievementCategoryService.findByName(category.name());
UserActionVO userActionVO =
userActionService.findUserActionByUserIdAndAchievementCategory(user.getId(), achievementCategoryVO.getId());
int count = userActionVO.getCount() + (AchievementAction.ASSIGN.equals(achievementAction) ? 1 : -1);
userActionVO.setCount(count > 0 ? count : 0);
userActionService.updateUserActions(userActionVO);
if (AchievementAction.ASSIGN.equals(achievementAction)) {
saveAchievementToUser(user, achievementCategoryVO.getId(), count);
} else if (AchievementAction.DELETE.equals(achievementAction)) {
deleteAchievementFromUser(user, achievementCategoryVO.getId());
int count = updateUserActionCount(user, achievementCategoryVO.getId(), achievementAction, null);
if (AchievementAction.ASSIGN == achievementAction) {
saveAchievementToUser(user, achievementCategoryVO.getId(), count, null);
} else if (AchievementAction.DELETE == achievementAction) {
deleteAchievementFromUser(user, achievementCategoryVO.getId(), null);
}
}

/**
* Calculates the achievement based on the user's action.
*
* @param user The user for whom the achievement needs to be
* calculated.
* @param category The category of the achievement.
* @param achievementAction The type of action (e.g., ASSIGN, DELETE).
* @param habitId The ID of the habit related to the achievement.
*/
@Transactional
public void calculateAchievement(UserVO user, AchievementCategoryType category,
AchievementAction achievementAction, Long habitId) {
AchievementCategoryVO achievementCategoryVO = achievementCategoryService.findByName(category.name());
int count = updateUserActionCount(user, achievementCategoryVO.getId(), achievementAction, habitId);
if (AchievementAction.ASSIGN == achievementAction) {
saveAchievementToUser(user, achievementCategoryVO.getId(), count, habitId);
} else if (AchievementAction.DELETE == achievementAction) {
deleteAchievementFromUser(user, achievementCategoryVO.getId(), habitId);
}
}

private void saveAchievementToUser(UserVO userVO, Long achievementCategoryId, int count) {
private void saveAchievementToUser(UserVO userVO, Long achievementCategoryId, int count, Long habitId) {
AchievementVO achievementVO = achievementService.findByCategoryIdAndCondition(achievementCategoryId, count);
if (achievementVO != null) {
Achievement achievement =
achievementRepo.findByAchievementCategoryIdAndCondition(achievementCategoryId, count)
.orElseThrow(() -> new NoSuchElementException(
.orElseThrow(() -> new NotFoundException(
ErrorMessage.ACHIEVEMENT_CATEGORY_NOT_FOUND_BY_ID + achievementCategoryId));
UserAchievement userAchievement = UserAchievement.builder()
.achievement(achievement)
.user(modelMapper.map(userVO, User.class))
.build();
if (habitId != null) {
userAchievement.setHabit(habitRepo.findById(habitId).orElseThrow(() -> new NotFoundException(
ErrorMessage.HABIT_NOT_FOUND_BY_ID + habitId)));
}
RatingCalculationEnum reason = RatingCalculationEnum.findByName(achievement.getTitle());
ratingCalculation.ratingCalculation(reason, userVO);
userAchievementRepo.save(userAchievement);
calculateAchievement(userVO, AchievementCategoryType.ACHIEVEMENT, AchievementAction.ASSIGN);
}
}

private void deleteAchievementFromUser(UserVO user, Long achievementCategoryId) {
List<Achievement> achievements =
achievementRepo.findUnAchieved(user.getId(), achievementCategoryId);
private void deleteAchievementFromUser(UserVO user, Long achievementCategoryId, Long habitId) {
List<Achievement> achievements;
if (habitId != null) {
achievements =
achievementRepo.findUnAchieved(user.getId(), achievementCategoryId, habitId);
} else {
achievements = achievementRepo.findUnAchieved(user.getId(), achievementCategoryId);
}
if (!achievements.isEmpty()) {
achievements.forEach(achievement -> {
RatingCalculationEnum reason = RatingCalculationEnum.findByName("UNDO_" + achievement.getTitle());
@@ -108,4 +138,19 @@ private void deleteAchievementFromUser(UserVO user, Long achievementCategoryId)
calculateAchievement(user, AchievementCategoryType.ACHIEVEMENT, AchievementAction.DELETE);
}
}

private int updateUserActionCount(UserVO user, Long achievementCategoryVOId,
AchievementAction achievementAction, Long habitId) {
UserActionVO userActionVO =
habitId == null ? (userActionService.findUserAction(user.getId(), achievementCategoryVOId))
: (userActionService.findUserAction(user.getId(), achievementCategoryVOId, habitId));
if (userActionVO == null) {
userActionVO = userActionService.createUserAction(user.getId(), achievementCategoryVOId, habitId);
}
int count = userActionVO.getCount() + ((AchievementAction.ASSIGN == achievementAction) ? 1 : -1);
count = Math.max(count, 0);
userActionVO.setCount(count);
userActionService.updateUserActions(userActionVO);
return count;
}
}
Original file line number Diff line number Diff line change
@@ -72,7 +72,7 @@ public AchievementVO save(AchievementPostDto achievementPostDto) {
List<UserVO> all = restClient.findAll();
all.forEach(userVO -> {
UserActionVO userActionByUserIdAndAchievementCategory =
userActionService.findUserActionByUserIdAndAchievementCategory(userVO.getId(),
userActionService.findUserAction(userVO.getId(),
achievementCategoryVO.getId());
if (userActionByUserIdAndAchievementCategory == null) {
userActionVO.setAchievementCategory(achievementCategoryVO);
Original file line number Diff line number Diff line change
@@ -709,7 +709,7 @@ public HabitAssignDto enrollHabit(Long habitAssignId, Long userId, LocalDate dat
updateHabitAssignAfterEnroll(habitAssign, habitCalendar);
UserVO userVO = userService.findById(userId);
achievementCalculation.calculateAchievement(userVO,
AchievementCategoryType.HABIT, AchievementAction.ASSIGN);
AchievementCategoryType.HABIT, AchievementAction.ASSIGN, habitAssign.getHabit().getId());
ratingCalculation.ratingCalculation(RatingCalculationEnum.DAYS_OF_HABIT_IN_PROGRESS, userVO);

return buildHabitAssignDto(habitAssign, language);
@@ -799,7 +799,7 @@ public HabitAssignDto unenrollHabit(Long habitAssignId, Long userId, LocalDate d
UserVO userVO = userService.findById(userId);
ratingCalculation.ratingCalculation(RatingCalculationEnum.UNDO_DAYS_OF_HABIT_IN_PROGRESS, userVO);
achievementCalculation.calculateAchievement(userVO,
AchievementCategoryType.HABIT, AchievementAction.DELETE);
AchievementCategoryType.HABIT, AchievementAction.DELETE, habitAssign.getHabit().getId());
return modelMapper.map(habitAssign, HabitAssignDto.class);
}

Loading

0 comments on commit f7e8962

Please sign in to comment.