fix(analytics): rebuild stale current period summaries on read
Signed-off-by: Marcus Fihlon <marcus@fihlon.swiss>
This commit is contained in:
parent
88ac213214
commit
6110deba21
3 changed files with 150 additions and 11 deletions
|
|
@ -284,6 +284,23 @@ public class AnalyticsController {
|
||||||
return ResponseEntity.ok(summary);
|
return ResponseEntity.ok(summary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current year summary.
|
||||||
|
*/
|
||||||
|
@GetMapping("/summaries/current-year")
|
||||||
|
public ResponseEntity<ActivitySummary> 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.
|
* Get form status description.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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(
|
private ActivitySummary getOrBuildCurrentSummary(
|
||||||
UUID userId,
|
UUID userId,
|
||||||
ActivitySummary.PeriodType periodType,
|
ActivitySummary.PeriodType periodType,
|
||||||
|
|
@ -275,16 +291,6 @@ public class ActivitySummaryService {
|
||||||
LocalDate periodEndExclusive,
|
LocalDate periodEndExclusive,
|
||||||
Runnable rebuildAction
|
Runnable rebuildAction
|
||||||
) {
|
) {
|
||||||
ActivitySummary existingSummary = activitySummaryRepository.findByUserIdAndPeriodTypeAndPeriodStart(
|
|
||||||
userId,
|
|
||||||
periodType,
|
|
||||||
periodStart
|
|
||||||
).orElse(null);
|
|
||||||
|
|
||||||
if (existingSummary != null) {
|
|
||||||
return existingSummary;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean hasActivitiesInPeriod = activityRepository.existsByUserIdAndStartedAtBetween(
|
boolean hasActivitiesInPeriod = activityRepository.existsByUserIdAndStartedAtBetween(
|
||||||
userId,
|
userId,
|
||||||
periodStart.atStartOfDay(),
|
periodStart.atStartOfDay(),
|
||||||
|
|
@ -292,7 +298,11 @@ public class ActivitySummaryService {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasActivitiesInPeriod) {
|
if (!hasActivitiesInPeriod) {
|
||||||
return null;
|
return activitySummaryRepository.findByUserIdAndPeriodTypeAndPeriodStart(
|
||||||
|
userId,
|
||||||
|
periodType,
|
||||||
|
periodStart
|
||||||
|
).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
rebuildAction.run();
|
rebuildAction.run();
|
||||||
|
|
|
||||||
|
|
@ -215,4 +215,116 @@ class ActivitySummaryServiceTest {
|
||||||
assertNull(result);
|
assertNull(result);
|
||||||
verify(activityRepository).existsByUserIdAndStartedAtBetween(userId, startDateTime, endDateTime);
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue