diff --git a/src/main/java/net/javahippie/fitpub/repository/AchievementRepository.java b/src/main/java/net/javahippie/fitpub/repository/AchievementRepository.java index d06471b..130869c 100644 --- a/src/main/java/net/javahippie/fitpub/repository/AchievementRepository.java +++ b/src/main/java/net/javahippie/fitpub/repository/AchievementRepository.java @@ -2,6 +2,7 @@ package net.javahippie.fitpub.repository; import net.javahippie.fitpub.model.entity.Achievement; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @@ -40,6 +41,12 @@ public interface AchievementRepository extends JpaRepository */ long countByUserId(UUID userId); + /** + * Delete all achievements for a user. + */ + @Modifying + void deleteByUserId(UUID userId); + /** * Get count of achievements earned by a user in a date range. */ diff --git a/src/main/java/net/javahippie/fitpub/service/AchievementService.java b/src/main/java/net/javahippie/fitpub/service/AchievementService.java index d52ed2e..702a581 100644 --- a/src/main/java/net/javahippie/fitpub/service/AchievementService.java +++ b/src/main/java/net/javahippie/fitpub/service/AchievementService.java @@ -86,6 +86,31 @@ public class AchievementService { return newAchievements; } + /** + * Rebuild all achievements for a user from chronological activity history. + */ + @Transactional + public List rebuildAchievementsForUser(UUID userId) { + if (userId == null) { + return List.of(); + } + + List activityHistory = activityRepository.findByUserIdOrderByStartedAtAsc(userId); + if (activityHistory.isEmpty()) { + achievementRepository.deleteByUserId(userId); + return List.of(); + } + + achievementRepository.deleteByUserId(userId); + + List rebuiltAchievements = new ArrayList<>(); + for (Activity activity : activityHistory) { + rebuiltAchievements.addAll(checkAndAwardAchievements(activity)); + } + + return rebuiltAchievements; + } + /** * Check first activity achievements. */ diff --git a/src/main/java/net/javahippie/fitpub/service/BatchImportService.java b/src/main/java/net/javahippie/fitpub/service/BatchImportService.java index ea477cd..b33b10a 100644 --- a/src/main/java/net/javahippie/fitpub/service/BatchImportService.java +++ b/src/main/java/net/javahippie/fitpub/service/BatchImportService.java @@ -344,11 +344,9 @@ public class BatchImportService { personalRecordService.checkAndUpdatePersonalRecords(activity); } - // Recalculate achievements for each activity + // Recalculate achievements from the full chronological activity history log.debug("Recalculating achievements..."); - for (Activity activity : activities) { - achievementService.checkAndAwardAchievements(activity); - } + achievementService.rebuildAchievementsForUser(job.getUserId()); // Recalculate training load for each activity log.debug("Recalculating training load..."); diff --git a/src/test/java/net/javahippie/fitpub/service/AchievementServiceTest.java b/src/test/java/net/javahippie/fitpub/service/AchievementServiceTest.java index 27d6ac4..a38e05b 100644 --- a/src/test/java/net/javahippie/fitpub/service/AchievementServiceTest.java +++ b/src/test/java/net/javahippie/fitpub/service/AchievementServiceTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InOrder; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -391,6 +392,28 @@ class AchievementServiceTest { assertEquals(activity.getId(), distanceAchievement.getActivityId()); } + @Test + @DisplayName("Should rebuild achievements by deleting existing rows and replaying history") + void testRebuildAchievementsForUser() { + 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)); + + stubHistory(previous, activity); + 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)); + + InOrder inOrder = inOrder(achievementRepository); + inOrder.verify(achievementRepository).deleteByUserId(userId); + inOrder.verify(achievementRepository, atLeastOnce()).save(any(Achievement.class)); + } + @Test @DisplayName("Should get user achievements") void testGetUserAchievements() {