diff --git a/src/main/java/net/javahippie/fitpub/controller/AnalyticsController.java b/src/main/java/net/javahippie/fitpub/controller/AnalyticsController.java index e5c40eb..59d87e4 100644 --- a/src/main/java/net/javahippie/fitpub/controller/AnalyticsController.java +++ b/src/main/java/net/javahippie/fitpub/controller/AnalyticsController.java @@ -284,6 +284,23 @@ public class AnalyticsController { return ResponseEntity.ok(summary); } + /** + * Get current year summary. + */ + @GetMapping("/summaries/current-year") + public ResponseEntity getCurrentYearSummary( + @AuthenticationPrincipal UserDetails userDetails) { + + UUID userId = getUserId(userDetails); + ActivitySummary summary = activitySummaryService.getCurrentYearSummary(userId); + + if (summary == null) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok(summary); + } + /** * Get form status description. */ diff --git a/src/main/java/net/javahippie/fitpub/service/ActivitySummaryService.java b/src/main/java/net/javahippie/fitpub/service/ActivitySummaryService.java index c41439f..807335e 100644 --- a/src/main/java/net/javahippie/fitpub/service/ActivitySummaryService.java +++ b/src/main/java/net/javahippie/fitpub/service/ActivitySummaryService.java @@ -268,6 +268,22 @@ public class ActivitySummaryService { ); } + /** + * Get current year summary. + */ + @Transactional + public ActivitySummary getCurrentYearSummary(UUID userId) { + LocalDate yearStart = LocalDate.now().with(TemporalAdjusters.firstDayOfYear()); + LocalDate yearEndExclusive = yearStart.plusYears(1); + return getOrBuildCurrentSummary( + userId, + ActivitySummary.PeriodType.YEAR, + yearStart, + yearEndExclusive, + () -> updateYearlySummary(userId, yearStart) + ); + } + private ActivitySummary getOrBuildCurrentSummary( UUID userId, ActivitySummary.PeriodType periodType, @@ -275,16 +291,6 @@ public class ActivitySummaryService { LocalDate periodEndExclusive, Runnable rebuildAction ) { - ActivitySummary existingSummary = activitySummaryRepository.findByUserIdAndPeriodTypeAndPeriodStart( - userId, - periodType, - periodStart - ).orElse(null); - - if (existingSummary != null) { - return existingSummary; - } - boolean hasActivitiesInPeriod = activityRepository.existsByUserIdAndStartedAtBetween( userId, periodStart.atStartOfDay(), @@ -292,7 +298,11 @@ public class ActivitySummaryService { ); if (!hasActivitiesInPeriod) { - return null; + return activitySummaryRepository.findByUserIdAndPeriodTypeAndPeriodStart( + userId, + periodType, + periodStart + ).orElse(null); } rebuildAction.run(); diff --git a/src/test/java/net/javahippie/fitpub/service/ActivitySummaryServiceTest.java b/src/test/java/net/javahippie/fitpub/service/ActivitySummaryServiceTest.java index 64ea742..933fed5 100644 --- a/src/test/java/net/javahippie/fitpub/service/ActivitySummaryServiceTest.java +++ b/src/test/java/net/javahippie/fitpub/service/ActivitySummaryServiceTest.java @@ -215,4 +215,116 @@ class ActivitySummaryServiceTest { assertNull(result); verify(activityRepository).existsByUserIdAndStartedAtBetween(userId, startDateTime, endDateTime); } + + @Test + @DisplayName("Should rebuild stale current month summary when activities exist") + void getCurrentMonthSummary_RebuildsExistingStaleSummary() { + LocalDate monthStart = LocalDate.now().withDayOfMonth(1); + LocalDateTime startDateTime = monthStart.atStartOfDay(); + LocalDateTime endDateTime = monthStart.plusMonths(1).atStartOfDay(); + + Activity activity = Activity.builder() + .id(UUID.randomUUID()) + .userId(userId) + .activityType(Activity.ActivityType.RUN) + .startedAt(startDateTime.plusDays(3).plusHours(6)) + .endedAt(startDateTime.plusDays(3).plusHours(7)) + .totalDistance(BigDecimal.valueOf(12000)) + .totalDurationSeconds(4200L) + .elevationGain(BigDecimal.valueOf(200)) + .build(); + + ActivitySummary rebuiltSummary = ActivitySummary.builder() + .userId(userId) + .periodType(ActivitySummary.PeriodType.MONTH) + .periodStart(monthStart) + .periodEnd(monthStart.plusMonths(1).minusDays(1)) + .activityCount(1) + .build(); + + when(activityRepository.existsByUserIdAndStartedAtBetween(userId, startDateTime, endDateTime)) + .thenReturn(true); + when(activityRepository.findByUserIdAndStartedAtBetweenOrderByStartedAtDesc(userId, startDateTime, endDateTime)) + .thenReturn(List.of(activity)); + when(personalRecordRepository.countByUserIdAndDateRange(userId, startDateTime, endDateTime)).thenReturn(0L); + when(achievementRepository.countByUserIdAndActivityStartedDateRange(userId, startDateTime, endDateTime)).thenReturn(0L); + when(activitySummaryRepository.findByUserIdAndPeriodTypeAndPeriodStart( + userId, + ActivitySummary.PeriodType.MONTH, + monthStart + )).thenReturn( + Optional.of(ActivitySummary.builder() + .userId(userId) + .periodType(ActivitySummary.PeriodType.MONTH) + .periodStart(monthStart) + .periodEnd(monthStart.plusMonths(1).minusDays(1)) + .activityCount(0) + .build()), + Optional.of(rebuiltSummary) + ); + when(activitySummaryRepository.save(any(ActivitySummary.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + ActivitySummary result = activitySummaryService.getCurrentMonthSummary(userId); + + assertNotNull(result); + assertEquals(1, result.getActivityCount()); + verify(activitySummaryRepository).save(any(ActivitySummary.class)); + } + + @Test + @DisplayName("Should rebuild stale current year summary when activities exist") + void getCurrentYearSummary_RebuildsExistingStaleSummary() { + LocalDate yearStart = LocalDate.now().withDayOfYear(1); + LocalDateTime startDateTime = yearStart.atStartOfDay(); + LocalDateTime endDateTime = yearStart.plusYears(1).atStartOfDay(); + + Activity activity = Activity.builder() + .id(UUID.randomUUID()) + .userId(userId) + .activityType(Activity.ActivityType.RIDE) + .startedAt(startDateTime.plusDays(20).plusHours(9)) + .endedAt(startDateTime.plusDays(20).plusHours(11)) + .totalDistance(BigDecimal.valueOf(65000)) + .totalDurationSeconds(7200L) + .elevationGain(BigDecimal.valueOf(900)) + .build(); + + ActivitySummary rebuiltSummary = ActivitySummary.builder() + .userId(userId) + .periodType(ActivitySummary.PeriodType.YEAR) + .periodStart(yearStart) + .periodEnd(yearStart.plusYears(1).minusDays(1)) + .activityCount(1) + .build(); + + when(activityRepository.existsByUserIdAndStartedAtBetween(userId, startDateTime, endDateTime)) + .thenReturn(true); + when(activityRepository.findByUserIdAndStartedAtBetweenOrderByStartedAtDesc(userId, startDateTime, endDateTime)) + .thenReturn(List.of(activity)); + when(personalRecordRepository.countByUserIdAndDateRange(userId, startDateTime, endDateTime)).thenReturn(0L); + when(achievementRepository.countByUserIdAndActivityStartedDateRange(userId, startDateTime, endDateTime)).thenReturn(0L); + when(activitySummaryRepository.findByUserIdAndPeriodTypeAndPeriodStart( + userId, + ActivitySummary.PeriodType.YEAR, + yearStart + )).thenReturn( + Optional.of(ActivitySummary.builder() + .userId(userId) + .periodType(ActivitySummary.PeriodType.YEAR) + .periodStart(yearStart) + .periodEnd(yearStart.plusYears(1).minusDays(1)) + .activityCount(0) + .build()), + Optional.of(rebuiltSummary) + ); + when(activitySummaryRepository.save(any(ActivitySummary.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + ActivitySummary result = activitySummaryService.getCurrentYearSummary(userId); + + assertNotNull(result); + assertEquals(1, result.getActivityCount()); + verify(activitySummaryRepository).save(any(ActivitySummary.class)); + } }