diff --git a/src/main/java/net/javahippie/fitpub/service/AchievementService.java b/src/main/java/net/javahippie/fitpub/service/AchievementService.java index 702a581..0c21c5e 100644 --- a/src/main/java/net/javahippie/fitpub/service/AchievementService.java +++ b/src/main/java/net/javahippie/fitpub/service/AchievementService.java @@ -41,10 +41,8 @@ public class AchievementService { */ @Transactional public List checkAndAwardAchievements(Activity activity) { - List newAchievements = new ArrayList<>(); - if (activity.getUserId() == null || activity.getStartedAt() == null || activity.getEndedAt() == null) { - return newAchievements; + return List.of(); } UUID userId = activity.getUserId(); @@ -59,6 +57,15 @@ public class AchievementService { existing.add(a.getAchievementType()); } + return checkAndAwardAchievements(activity, progress, existing); + } + + private List checkAndAwardAchievements(Activity activity, + ActivityProgress progress, + Set existing) { + List newAchievements = new ArrayList<>(); + UUID userId = activity.getUserId(); + // Check first activity achievements newAchievements.addAll(checkFirstActivityAchievements(userId, activity, progress, existing)); @@ -83,6 +90,10 @@ public class AchievementService { // Check speed achievements newAchievements.addAll(checkSpeedAchievements(userId, activity, progress, existing)); + for (Achievement achievement : newAchievements) { + existing.add(achievement.getAchievementType()); + } + return newAchievements; } @@ -103,9 +114,15 @@ public class AchievementService { achievementRepository.deleteByUserId(userId); + Set existing = EnumSet.noneOf(Achievement.AchievementType.class); List rebuiltAchievements = new ArrayList<>(); - for (Activity activity : activityHistory) { - rebuiltAchievements.addAll(checkAndAwardAchievements(activity)); + for (int i = 0; i < activityHistory.size(); i++) { + Activity activity = activityHistory.get(i); + rebuiltAchievements.addAll(checkAndAwardAchievements( + activity, + ActivityProgress.fromHistory(activityHistory, i), + existing + )); } return rebuiltAchievements; @@ -560,6 +577,10 @@ public class AchievementService { throw new IllegalStateException("Current activity missing from chronological history: " + currentActivity.getId()); } + return fromHistory(activityHistory, currentIndex); + } + + private static ActivityProgress fromHistory(List activityHistory, int currentIndex) { return new ActivityProgress( List.copyOf(activityHistory.subList(0, currentIndex)), List.copyOf(activityHistory.subList(0, currentIndex + 1)) diff --git a/src/test/java/net/javahippie/fitpub/service/AchievementServiceTest.java b/src/test/java/net/javahippie/fitpub/service/AchievementServiceTest.java index a38e05b..f6f5665 100644 --- a/src/test/java/net/javahippie/fitpub/service/AchievementServiceTest.java +++ b/src/test/java/net/javahippie/fitpub/service/AchievementServiceTest.java @@ -414,6 +414,27 @@ class AchievementServiceTest { inOrder.verify(achievementRepository, atLeastOnce()).save(any(Achievement.class)); } + @Test + @DisplayName("Should rebuild achievements from a stable history snapshot") + void testRebuildAchievementsForUser_UsesStableHistorySnapshot() { + Activity previous = createActivity(Activity.ActivityType.RUN, 9000L, BigDecimal.ZERO); + previous.setStartedAt(LocalDateTime.of(2025, 11, 30, 10, 0)); + previous.setEndedAt(LocalDateTime.of(2025, 11, 30, 11, 0)); + Activity activity = createActivity(Activity.ActivityType.RUN, 2000L, BigDecimal.ZERO); + activity.setStartedAt(LocalDateTime.of(2025, 12, 1, 7, 15)); + activity.setEndedAt(LocalDateTime.of(2025, 12, 1, 8, 5)); + + when(activityRepository.findByUserIdOrderByStartedAtAsc(userId)) + .thenReturn(List.of(previous, activity)) + .thenReturn(List.of(previous)); + when(achievementRepository.save(any(Achievement.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + List rebuilt = achievementService.rebuildAchievementsForUser(userId); + + assertTrue(rebuilt.stream().anyMatch(a -> a.getAchievementType() == Achievement.AchievementType.DISTANCE_10K)); + verify(activityRepository, times(1)).findByUserIdOrderByStartedAtAsc(userId); + } + @Test @DisplayName("Should get user achievements") void testGetUserAchievements() {