Skip to content

Commit e5aa23a

Browse files
committed
feat: add replaceChangedWorkouts option to CopyFromCalendarToCalendarRequest and refactor workout reconciliation logic
1 parent abeb53d commit e5aa23a

3 files changed

Lines changed: 7 additions & 187 deletions

File tree

boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyC2CRequest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ data class CopyFromCalendarToCalendarRequest(
1010
val types: List<TrainingType>,
1111
val skipSynced: Boolean,
1212
val sourcePlatform: Platform,
13-
val targetPlatform: Platform
13+
val targetPlatform: Platform,
14+
val replaceChangedWorkouts: Boolean = false
1415
)

boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/WorkoutService.kt

Lines changed: 2 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ class WorkoutService(
289289
return response
290290
}
291291

292-
fun copyWorkoutsNormally(
292+
private fun copyWorkoutsNormally(
293293
request: CopyFromCalendarToCalendarRequest
294294
): CopyWorkoutsResponse {
295295

@@ -428,177 +428,7 @@ class WorkoutService(
428428
val workoutRepository = workoutRepositoryMap[request.platform]!!
429429
workoutRepository.deleteWorkoutsFromCalendar(request.startDate, request.endDate)
430430
}
431-
432-
fun reconcileScheduledTrainerRoadToTrainingPeaks(
433-
request: CopyFromCalendarToCalendarRequest
434-
): CopyWorkoutsResponse {
435-
436-
require(request.sourcePlatform == Platform.TRAINER_ROAD) {
437-
"Scheduled reconciliation requires TrainerRoad as source"
438-
}
439-
440-
require(request.targetPlatform == Platform.TRAINING_PEAKS) {
441-
"Scheduled reconciliation requires TrainingPeaks as target"
442-
}
443-
444-
log.info(
445-
"Starting scheduled TrainerRoad to TrainingPeaks reconciliation: {}",
446-
request
447-
)
448-
449-
val sourceRepository =
450-
workoutRepositoryMap[request.sourcePlatform]!!
451-
452-
val targetRepository =
453-
workoutRepositoryMap[request.targetPlatform]!!
454-
455-
val allSourceWorkouts =
456-
sourceRepository.getWorkoutsFromCalendar(
457-
request.startDate,
458-
request.endDate
459-
)
460-
461-
val sourceWorkouts = allSourceWorkouts.filter {
462-
request.types.contains(it.details.type)
463-
}
464-
465-
val skippedByType =
466-
allSourceWorkouts.size - sourceWorkouts.size
467-
468-
/*
469-
* Important safeguard:
470-
* an empty response from TrainerRoad must never cause existing
471-
* TrainingPeaks workouts to be deleted.
472-
*/
473-
if (sourceWorkouts.isEmpty()) {
474-
log.info(
475-
"TrainerRoad returned no matching workouts. " +
476-
"No TrainingPeaks workouts will be removed."
477-
)
478-
479-
return CopyWorkoutsResponse(
480-
copied = 0,
481-
filteredOut = skippedByType,
482-
skippedByType = skippedByType,
483-
skippedAlreadySynced = 0,
484-
startDate = request.startDate,
485-
endDate = request.endDate,
486-
externalData = ExternalData.empty()
487-
)
488-
}
489-
490-
val sourceWithoutTrainerRoadId = sourceWorkouts.filter {
491-
it.details.externalData.trainerRoadId.isNullOrBlank()
492-
}
493-
494-
val validSourceWorkouts = sourceWorkouts.filter {
495-
!it.details.externalData.trainerRoadId.isNullOrBlank()
496-
}
497-
498-
val invalidSourceFailures =
499-
sourceWithoutTrainerRoadId.map { workout ->
500-
WorkoutSyncFailure(
501-
workoutName = workout.details.name,
502-
workoutDate = workout.date,
503-
message = "TrainerRoad workout ID is missing"
504-
)
505-
}
506-
507-
val targetWorkouts =
508-
targetRepository.getWorkoutsFromCalendar(
509-
request.startDate,
510-
request.endDate
511-
)
512-
513-
val managedTargetWorkouts = targetWorkouts.filter {
514-
isApplicationManagedTrainerRoadWorkout(it)
515-
}
516-
517-
val sourceTrainerRoadIds = validSourceWorkouts
518-
.mapNotNull {
519-
it.details.externalData.trainerRoadId
520-
}
521-
.toSet()
522-
523-
val targetTrainerRoadIds = managedTargetWorkouts
524-
.mapNotNull {
525-
it.details.externalData.trainerRoadId
526-
}
527-
.toSet()
528-
529-
val workoutsToCreate = validSourceWorkouts.filter {
530-
it.details.externalData.trainerRoadId !in targetTrainerRoadIds
531-
}
532-
533-
val alreadySynced =
534-
validSourceWorkouts.size - workoutsToCreate.size
535-
536-
val workoutsToRemove = managedTargetWorkouts.filter {
537-
it.details.externalData.trainerRoadId !in sourceTrainerRoadIds
538-
}
539-
540-
/*
541-
* Create the replacement first.
542-
*/
543-
val saveResult = saveWorkoutsIndividually(
544-
repository = targetRepository,
545-
workouts = workoutsToCreate
546-
)
547-
548-
val copyFailures =
549-
invalidSourceFailures + saveResult.failures
550-
551-
/*
552-
* Do not remove the previous workout if any new workout failed
553-
* to be created.
554-
*/
555-
val deleteResult = if (copyFailures.isEmpty()) {
556-
deleteWorkoutsIndividually(
557-
repository = targetRepository,
558-
workouts = workoutsToRemove
559-
)
560-
} else {
561-
log.warn(
562-
"Skipping removal of {} replaced workouts because " +
563-
"{} source workouts failed to sync",
564-
workoutsToRemove.size,
565-
copyFailures.size
566-
)
567-
568-
DeleteWorkoutsResult(
569-
removed = 0,
570-
failures = emptyList()
571-
)
572-
}
573-
574-
val response = CopyWorkoutsResponse(
575-
copied = saveResult.copied,
576-
filteredOut = skippedByType + alreadySynced,
577-
skippedByType = skippedByType,
578-
skippedAlreadySynced = alreadySynced,
579-
startDate = request.startDate,
580-
endDate = request.endDate,
581-
externalData = ExternalData.empty(),
582-
failed = copyFailures.size,
583-
failedWorkouts = copyFailures,
584-
removed = deleteResult.removed,
585-
failedToRemove = deleteResult.failures.size,
586-
failedRemovals = deleteResult.failures
587-
)
588-
589-
log.info(
590-
"Scheduled reconciliation completed. copied={}, " +
591-
"alreadySynced={}, removed={}, failed={}, failedToRemove={}",
592-
response.copied,
593-
response.skippedAlreadySynced,
594-
response.removed,
595-
response.failed,
596-
response.failedToRemove
597-
)
598-
599-
return response
600-
}
601-
431+
602432
private fun isApplicationManagedTrainerRoadWorkout(
603433
workout: Workout
604434
): Boolean {

boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/WorkoutScheduledJob.kt

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import org.slf4j.LoggerFactory
88
import org.springframework.scheduling.annotation.Scheduled
99
import org.springframework.stereotype.Service
1010
import java.util.concurrent.TimeUnit
11-
import org.freekode.tp2intervals.domain.Platform
1211

1312
@Service
1413
class WorkoutScheduledJob(
@@ -46,19 +45,9 @@ class WorkoutScheduledJob(
4645
private fun handleCopyCalendarToCalendarRequest(
4746
request: C2CTodayScheduledRequest
4847
) {
49-
val todayRequest = request.forToday()
50-
51-
if (
52-
request.sourcePlatform == Platform.TRAINER_ROAD &&
53-
request.targetPlatform == Platform.TRAINING_PEAKS
54-
) {
55-
workoutService
56-
.reconcileScheduledTrainerRoadToTrainingPeaks(
57-
todayRequest
58-
)
59-
} else {
60-
workoutService.copyWorkoutsC2C(todayRequest)
61-
}
48+
workoutService.copyWorkoutsC2C(
49+
request.forToday()
50+
)
6251
}
6352

6453
private fun ScheduleRequestEntity.toSchedulable(): C2CTodayScheduledRequest {

0 commit comments

Comments
 (0)