fix(analytics): rebuild achievements from activity history

Signed-off-by: Marcus Fihlon <marcus@fihlon.swiss>
This commit is contained in:
Marcus Fihlon 2026-04-29 09:44:28 +02:00
parent 4d16e8c685
commit 6af484bcf7
Signed by: McPringle
GPG key ID: C6B7F469EE363E1F
4 changed files with 57 additions and 4 deletions

View file

@ -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<Achievement, UUID>
*/
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.
*/

View file

@ -86,6 +86,31 @@ public class AchievementService {
return newAchievements;
}
/**
* Rebuild all achievements for a user from chronological activity history.
*/
@Transactional
public List<Achievement> rebuildAchievementsForUser(UUID userId) {
if (userId == null) {
return List.of();
}
List<Activity> activityHistory = activityRepository.findByUserIdOrderByStartedAtAsc(userId);
if (activityHistory.isEmpty()) {
achievementRepository.deleteByUserId(userId);
return List.of();
}
achievementRepository.deleteByUserId(userId);
List<Achievement> rebuiltAchievements = new ArrayList<>();
for (Activity activity : activityHistory) {
rebuiltAchievements.addAll(checkAndAwardAchievements(activity));
}
return rebuiltAchievements;
}
/**
* Check first activity achievements.
*/

View file

@ -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...");

View file

@ -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<Achievement> 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() {