fix(analytics): rebuild personal records after activity deletion

Signed-off-by: Marcus Fihlon <marcus@fihlon.swiss>
This commit is contained in:
Marcus Fihlon 2026-04-29 13:02:11 +02:00
parent f0e065600e
commit 86e276e735
Signed by: McPringle
GPG key ID: C6B7F469EE363E1F
5 changed files with 65 additions and 1 deletions

View file

@ -35,6 +35,11 @@ public interface PersonalRecordRepository extends JpaRepository<PersonalRecord,
PersonalRecord.RecordType recordType
);
/**
* Delete all personal records for a user.
*/
void deleteByUserId(UUID userId);
/**
* Get count of personal records set by a user.
*/

View file

@ -24,6 +24,7 @@ public class ActivityDeleteRecalculationService {
private final AchievementService achievementService;
private final ActivitySummaryService activitySummaryService;
private final PersonalRecordService personalRecordService;
private final ConcurrentMap<UUID, PendingUserRecalculation> pendingRecalculations = new ConcurrentHashMap<>();
@Async
@ -42,6 +43,7 @@ public class ActivityDeleteRecalculationService {
do {
Set<LocalDate> datesToRecalculate = pending.drainDates();
achievementService.rebuildAchievementsForUser(event.userId());
personalRecordService.rebuildPersonalRecordsForUser(event.userId());
for (LocalDate date : datesToRecalculate) {
activitySummaryService.updateWeeklySummary(event.userId(), date);
activitySummaryService.updateMonthlySummary(event.userId(), date);
@ -49,7 +51,7 @@ public class ActivityDeleteRecalculationService {
}
} while (pending.keepProcessing());
log.info("Recalculated achievements and summaries after deleting activities for user {}", event.userId());
log.info("Recalculated achievements, personal records, and summaries after deleting activities for user {}", event.userId());
}
private static final class PendingUserRecalculation {

View file

@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.javahippie.fitpub.model.entity.Activity;
import net.javahippie.fitpub.model.entity.PersonalRecord;
import net.javahippie.fitpub.repository.ActivityRepository;
import net.javahippie.fitpub.repository.PersonalRecordRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -23,6 +24,7 @@ import java.util.UUID;
@Slf4j
public class PersonalRecordService {
private final ActivityRepository activityRepository;
private final PersonalRecordRepository personalRecordRepository;
/**
@ -320,4 +322,19 @@ public class PersonalRecordService {
public long getPersonalRecordCount(UUID userId) {
return personalRecordRepository.countByUserId(userId);
}
/**
* Rebuild all personal records for a user from remaining activities.
*/
@Transactional
public void rebuildPersonalRecordsForUser(UUID userId) {
personalRecordRepository.deleteByUserId(userId);
List<Activity> activities = activityRepository.findByUserIdOrderByStartedAtAsc(userId);
for (Activity activity : activities) {
checkAndUpdatePersonalRecords(activity);
}
log.info("Rebuilt personal records for user {} from {} activities", userId, activities.size());
}
}

View file

@ -24,6 +24,9 @@ class ActivityDeleteRecalculationServiceTest {
@Mock
private ActivitySummaryService activitySummaryService;
@Mock
private PersonalRecordService personalRecordService;
@InjectMocks
private ActivityDeleteRecalculationService activityDeleteRecalculationService;
@ -36,6 +39,7 @@ class ActivityDeleteRecalculationServiceTest {
activityDeleteRecalculationService.handleActivityDeleted(new ActivityDeletedEvent(userId, activityDate));
verify(achievementService).rebuildAchievementsForUser(userId);
verify(personalRecordService).rebuildPersonalRecordsForUser(userId);
verify(activitySummaryService).updateWeeklySummary(userId, activityDate);
verify(activitySummaryService).updateMonthlySummary(userId, activityDate);
verify(activitySummaryService).updateYearlySummary(userId, activityDate);
@ -59,6 +63,7 @@ class ActivityDeleteRecalculationServiceTest {
activityDeleteRecalculationService.handleActivityDeleted(new ActivityDeletedEvent(userId, firstDate));
verify(achievementService, times(2)).rebuildAchievementsForUser(userId);
verify(personalRecordService, times(2)).rebuildPersonalRecordsForUser(userId);
verify(activitySummaryService).updateWeeklySummary(userId, firstDate);
verify(activitySummaryService).updateMonthlySummary(userId, firstDate);
verify(activitySummaryService).updateYearlySummary(userId, firstDate);

View file

@ -10,6 +10,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import net.javahippie.fitpub.model.entity.Activity;
import net.javahippie.fitpub.model.entity.ActivityMetrics;
import net.javahippie.fitpub.model.entity.PersonalRecord;
import net.javahippie.fitpub.repository.ActivityRepository;
import net.javahippie.fitpub.repository.PersonalRecordRepository;
import java.math.BigDecimal;
@ -33,6 +34,9 @@ class PersonalRecordServiceTest {
@Mock
private PersonalRecordRepository personalRecordRepository;
@Mock
private ActivityRepository activityRepository;
@InjectMocks
private PersonalRecordService personalRecordService;
@ -415,6 +419,37 @@ class PersonalRecordServiceTest {
));
}
@Test
@DisplayName("Should rebuild personal records from remaining activities")
void testRebuildPersonalRecordsForUser() {
Activity firstActivity = createActivity(
10000L,
3600L,
BigDecimal.valueOf(100)
);
firstActivity.setStartedAt(testTime.minusDays(2));
Activity secondActivity = createActivity(
15000L,
4500L,
BigDecimal.valueOf(200)
);
secondActivity.setStartedAt(testTime.minusDays(1));
when(activityRepository.findByUserIdOrderByStartedAtAsc(userId))
.thenReturn(List.of(firstActivity, secondActivity));
when(personalRecordRepository.findByUserIdAndActivityTypeAndRecordType(any(), any(), any()))
.thenReturn(Optional.empty());
when(personalRecordRepository.save(any(PersonalRecord.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
personalRecordService.rebuildPersonalRecordsForUser(userId);
verify(personalRecordRepository).deleteByUserId(userId);
verify(activityRepository).findByUserIdOrderByStartedAtAsc(userId);
verify(personalRecordRepository, atLeastOnce()).save(any(PersonalRecord.class));
}
// Helper methods
private Activity createActivity(long distanceMeters, long durationSeconds, BigDecimal elevationGain) {