diff --git a/src/main/java/net/javahippie/fitpub/service/ActivityDeleteRecalculationService.java b/src/main/java/net/javahippie/fitpub/service/ActivityDeleteRecalculationService.java new file mode 100644 index 0000000..61d64f1 --- /dev/null +++ b/src/main/java/net/javahippie/fitpub/service/ActivityDeleteRecalculationService.java @@ -0,0 +1,30 @@ +package net.javahippie.fitpub.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +/** + * Recalculates derived analytics after an activity has been deleted. + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class ActivityDeleteRecalculationService { + + private final AchievementService achievementService; + private final ActivitySummaryService activitySummaryService; + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleActivityDeleted(ActivityDeletedEvent event) { + achievementService.rebuildAchievementsForUser(event.userId()); + activitySummaryService.updateWeeklySummary(event.userId(), event.activityDate()); + activitySummaryService.updateMonthlySummary(event.userId(), event.activityDate()); + activitySummaryService.updateYearlySummary(event.userId(), event.activityDate()); + log.info("Recalculated achievements and summaries after deleting activity for user {}", event.userId()); + } +} diff --git a/src/main/java/net/javahippie/fitpub/service/ActivityDeletedEvent.java b/src/main/java/net/javahippie/fitpub/service/ActivityDeletedEvent.java new file mode 100644 index 0000000..9eb069c --- /dev/null +++ b/src/main/java/net/javahippie/fitpub/service/ActivityDeletedEvent.java @@ -0,0 +1,7 @@ +package net.javahippie.fitpub.service; + +import java.time.LocalDate; +import java.util.UUID; + +public record ActivityDeletedEvent(UUID userId, LocalDate activityDate) { +} diff --git a/src/main/java/net/javahippie/fitpub/service/FitFileService.java b/src/main/java/net/javahippie/fitpub/service/FitFileService.java index 39c8b76..6e3b146 100644 --- a/src/main/java/net/javahippie/fitpub/service/FitFileService.java +++ b/src/main/java/net/javahippie/fitpub/service/FitFileService.java @@ -18,6 +18,7 @@ import net.javahippie.fitpub.util.FitFileValidator; import net.javahippie.fitpub.util.FitParser; import net.javahippie.fitpub.util.ParsedActivityData; import net.javahippie.fitpub.util.TrackSimplifier; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -52,6 +53,7 @@ public class FitFileService { private final ActivitySummaryService activitySummaryService; private final WeatherService weatherService; private final HeatmapGridService heatmapGridService; + private final ApplicationEventPublisher applicationEventPublisher; /** * Processes an uploaded FIT file and creates an activity. @@ -319,12 +321,10 @@ public class FitFileService { return activityRepository.findByIdAndUserId(activityId, userId) .map(activity -> { activityRepository.delete(activity); - achievementService.rebuildAchievementsForUser(userId); if (activity.getStartedAt() != null) { - java.time.LocalDate activityDate = activity.getStartedAt().toLocalDate(); - activitySummaryService.updateWeeklySummary(userId, activityDate); - activitySummaryService.updateMonthlySummary(userId, activityDate); - activitySummaryService.updateYearlySummary(userId, activityDate); + applicationEventPublisher.publishEvent( + new ActivityDeletedEvent(userId, activity.getStartedAt().toLocalDate()) + ); } log.info("Deleted activity {} for user {}", activityId, userId); return true; diff --git a/src/test/java/net/javahippie/fitpub/service/ActivityDeleteRecalculationServiceTest.java b/src/test/java/net/javahippie/fitpub/service/ActivityDeleteRecalculationServiceTest.java new file mode 100644 index 0000000..eecb5d8 --- /dev/null +++ b/src/test/java/net/javahippie/fitpub/service/ActivityDeleteRecalculationServiceTest.java @@ -0,0 +1,40 @@ +package net.javahippie.fitpub.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.util.UUID; + +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ActivityDeleteRecalculationServiceTest { + + @Mock + private AchievementService achievementService; + + @Mock + private ActivitySummaryService activitySummaryService; + + @InjectMocks + private ActivityDeleteRecalculationService activityDeleteRecalculationService; + + @Test + @DisplayName("Should rebuild achievements and summaries after activity deletion") + void shouldRebuildAchievementsAndSummariesAfterActivityDeletion() { + UUID userId = UUID.randomUUID(); + LocalDate activityDate = LocalDate.of(2025, 12, 3); + + activityDeleteRecalculationService.handleActivityDeleted(new ActivityDeletedEvent(userId, activityDate)); + + verify(achievementService).rebuildAchievementsForUser(userId); + verify(activitySummaryService).updateWeeklySummary(userId, activityDate); + verify(activitySummaryService).updateMonthlySummary(userId, activityDate); + verify(activitySummaryService).updateYearlySummary(userId, activityDate); + } +} diff --git a/src/test/java/net/javahippie/fitpub/service/FitFileServiceTest.java b/src/test/java/net/javahippie/fitpub/service/FitFileServiceTest.java index 0ab3ac1..1665c11 100644 --- a/src/test/java/net/javahippie/fitpub/service/FitFileServiceTest.java +++ b/src/test/java/net/javahippie/fitpub/service/FitFileServiceTest.java @@ -21,6 +21,7 @@ import net.javahippie.fitpub.util.FitFileValidator; import net.javahippie.fitpub.util.FitParser; import net.javahippie.fitpub.util.ParsedActivityData; import net.javahippie.fitpub.util.TrackSimplifier; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.mock.web.MockMultipartFile; import java.math.BigDecimal; @@ -74,6 +75,9 @@ class FitFileServiceTest { @Mock private HeatmapGridService heatmapGridService; + @Mock + private ApplicationEventPublisher applicationEventPublisher; + @Spy private ObjectMapper objectMapper; @@ -240,10 +244,10 @@ class FitFileServiceTest { // Assert assertTrue(result); verify(activityRepository).delete(activity); - verify(achievementService).rebuildAchievementsForUser(testUserId); - verify(activitySummaryService).updateWeeklySummary(testUserId, startedAt.toLocalDate()); - verify(activitySummaryService).updateMonthlySummary(testUserId, startedAt.toLocalDate()); - verify(activitySummaryService).updateYearlySummary(testUserId, startedAt.toLocalDate()); + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(ActivityDeletedEvent.class); + verify(applicationEventPublisher).publishEvent(eventCaptor.capture()); + assertEquals(testUserId, eventCaptor.getValue().userId()); + assertEquals(startedAt.toLocalDate(), eventCaptor.getValue().activityDate()); } @Test @@ -260,10 +264,7 @@ class FitFileServiceTest { // Assert assertFalse(result); verify(activityRepository, never()).delete(any()); - verify(achievementService, never()).rebuildAchievementsForUser(any()); - verify(activitySummaryService, never()).updateWeeklySummary(any(), any()); - verify(activitySummaryService, never()).updateMonthlySummary(any(), any()); - verify(activitySummaryService, never()).updateYearlySummary(any(), any()); + verify(applicationEventPublisher, never()).publishEvent(any()); } @Test