fix(analytics): rebuild achievements from stable history snapshot
Prevent delete-triggered achievement rebuilds from failing when activity history changes again while an async recalculation is already running. Signed-off-by: Marcus Fihlon <marcus@fihlon.swiss>
This commit is contained in:
parent
714007aabe
commit
251beaae0f
2 changed files with 47 additions and 5 deletions
|
|
@ -41,10 +41,8 @@ public class AchievementService {
|
|||
*/
|
||||
@Transactional
|
||||
public List<Achievement> checkAndAwardAchievements(Activity activity) {
|
||||
List<Achievement> 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<Achievement> checkAndAwardAchievements(Activity activity,
|
||||
ActivityProgress progress,
|
||||
Set<Achievement.AchievementType> existing) {
|
||||
List<Achievement> 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<Achievement.AchievementType> existing = EnumSet.noneOf(Achievement.AchievementType.class);
|
||||
List<Achievement> 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<Activity> activityHistory, int currentIndex) {
|
||||
return new ActivityProgress(
|
||||
List.copyOf(activityHistory.subList(0, currentIndex)),
|
||||
List.copyOf(activityHistory.subList(0, currentIndex + 1))
|
||||
|
|
|
|||
|
|
@ -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<Achievement> 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() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue