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
|
@Transactional
|
||||||
public List<Achievement> checkAndAwardAchievements(Activity activity) {
|
public List<Achievement> checkAndAwardAchievements(Activity activity) {
|
||||||
List<Achievement> newAchievements = new ArrayList<>();
|
|
||||||
|
|
||||||
if (activity.getUserId() == null || activity.getStartedAt() == null || activity.getEndedAt() == null) {
|
if (activity.getUserId() == null || activity.getStartedAt() == null || activity.getEndedAt() == null) {
|
||||||
return newAchievements;
|
return List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
UUID userId = activity.getUserId();
|
UUID userId = activity.getUserId();
|
||||||
|
|
@ -59,6 +57,15 @@ public class AchievementService {
|
||||||
existing.add(a.getAchievementType());
|
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
|
// Check first activity achievements
|
||||||
newAchievements.addAll(checkFirstActivityAchievements(userId, activity, progress, existing));
|
newAchievements.addAll(checkFirstActivityAchievements(userId, activity, progress, existing));
|
||||||
|
|
||||||
|
|
@ -83,6 +90,10 @@ public class AchievementService {
|
||||||
// Check speed achievements
|
// Check speed achievements
|
||||||
newAchievements.addAll(checkSpeedAchievements(userId, activity, progress, existing));
|
newAchievements.addAll(checkSpeedAchievements(userId, activity, progress, existing));
|
||||||
|
|
||||||
|
for (Achievement achievement : newAchievements) {
|
||||||
|
existing.add(achievement.getAchievementType());
|
||||||
|
}
|
||||||
|
|
||||||
return newAchievements;
|
return newAchievements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,9 +114,15 @@ public class AchievementService {
|
||||||
|
|
||||||
achievementRepository.deleteByUserId(userId);
|
achievementRepository.deleteByUserId(userId);
|
||||||
|
|
||||||
|
Set<Achievement.AchievementType> existing = EnumSet.noneOf(Achievement.AchievementType.class);
|
||||||
List<Achievement> rebuiltAchievements = new ArrayList<>();
|
List<Achievement> rebuiltAchievements = new ArrayList<>();
|
||||||
for (Activity activity : activityHistory) {
|
for (int i = 0; i < activityHistory.size(); i++) {
|
||||||
rebuiltAchievements.addAll(checkAndAwardAchievements(activity));
|
Activity activity = activityHistory.get(i);
|
||||||
|
rebuiltAchievements.addAll(checkAndAwardAchievements(
|
||||||
|
activity,
|
||||||
|
ActivityProgress.fromHistory(activityHistory, i),
|
||||||
|
existing
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return rebuiltAchievements;
|
return rebuiltAchievements;
|
||||||
|
|
@ -560,6 +577,10 @@ public class AchievementService {
|
||||||
throw new IllegalStateException("Current activity missing from chronological history: " + currentActivity.getId());
|
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(
|
return new ActivityProgress(
|
||||||
List.copyOf(activityHistory.subList(0, currentIndex)),
|
List.copyOf(activityHistory.subList(0, currentIndex)),
|
||||||
List.copyOf(activityHistory.subList(0, currentIndex + 1))
|
List.copyOf(activityHistory.subList(0, currentIndex + 1))
|
||||||
|
|
|
||||||
|
|
@ -414,6 +414,27 @@ class AchievementServiceTest {
|
||||||
inOrder.verify(achievementRepository, atLeastOnce()).save(any(Achievement.class));
|
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
|
@Test
|
||||||
@DisplayName("Should get user achievements")
|
@DisplayName("Should get user achievements")
|
||||||
void testGetUserAchievements() {
|
void testGetUserAchievements() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue