From 8ed22b70e541841f44d09bb1d5ea013b0e1671e7 Mon Sep 17 00:00:00 2001 From: mattknatt Date: Mon, 1 Jun 2026 16:55:33 +0200 Subject: [PATCH] Add rolling window for gym classes and daily top-up scheduling - Introduced `ensureUpcomingClasses()` to maintain a rolling 30-day window of gym classes. - Added a `@Scheduled` daily job to ensure the window is continuously updated. - Updated `GymClassRepository` to include `findByStartTimeBetween()` method. - Enabled scheduling in `BokaApplication`. - Refactored initial data seeding to delegate gym class creation to the rolling window logic. --- .../com/example/boka/BokaApplication.java | 2 + .../com/example/boka/config/DataSeeder.java | 89 ++++++++++++++----- .../gymclass/domain/GymClassRepository.java | 2 + 3 files changed, 73 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/example/boka/BokaApplication.java b/src/main/java/com/example/boka/BokaApplication.java index 8a36a7e..63e87a7 100644 --- a/src/main/java/com/example/boka/BokaApplication.java +++ b/src/main/java/com/example/boka/BokaApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling public class BokaApplication { public static void main(String[] args) { diff --git a/src/main/java/com/example/boka/config/DataSeeder.java b/src/main/java/com/example/boka/config/DataSeeder.java index dc83e60..2d8d837 100644 --- a/src/main/java/com/example/boka/config/DataSeeder.java +++ b/src/main/java/com/example/boka/config/DataSeeder.java @@ -21,15 +21,20 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Profile; import org.springframework.core.env.Environment; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; @Component @RequiredArgsConstructor @@ -57,13 +62,74 @@ public class DataSeeder implements CommandLineRunner { @Value("${ADMIN_PASSWORD_FORCE_SYNC:false}") private boolean adminPasswordForceSync; + private static final int WINDOW_DAYS = 30; + private static final int[] CLASS_HOURS = {8, 12, 16}; + @Override @Transactional public void run(String... args) { upsertAdminUser(); + seedBaseDataIfEmpty(); + ensureUpcomingClasses(); + } + + /** + * Maintains a rolling window of gym classes so there are always classes available + * from today through {@value #WINDOW_DAYS} days ahead. Idempotent: a day that already + * has classes (seeded, topped up, or admin-created) is left untouched, so existing + * classes and their bookings are never disturbed. Runs on every boot and daily. + */ + void ensureUpcomingClasses() { + List classTypes = classTypeRepository.findAll(); + List gyms = gymRepository.findAll(); + List instructors = userRepository.findByRole(UserRole.INSTRUCTOR); + if (classTypes.isEmpty() || gyms.isEmpty() || instructors.isEmpty()) { + log.warn("Skipping rolling class top-up — base data (class types, gyms, or instructors) missing."); + return; + } + + LocalDate today = LocalDate.now(); + Set daysWithClasses = gymClassRepository + .findByStartTimeBetween(today.atStartOfDay(), today.plusDays(WINDOW_DAYS).atTime(LocalTime.MAX)) + .stream() + .map(gc -> gc.getStartTime().toLocalDate()) + .collect(Collectors.toSet()); + List newClasses = new ArrayList<>(); + for (int day = 0; day <= WINDOW_DAYS; day++) { + LocalDate date = today.plusDays(day); + if (daysWithClasses.contains(date)) { + continue; + } + for (int hour : CLASS_HOURS) { + Gym gym = gyms.get(random.nextInt(gyms.size())); + ClassType type = classTypes.get(random.nextInt(classTypes.size())); + User instructor = instructors.get(random.nextInt(instructors.size())); + newClasses.add(createGymClass(type, instructor.getId(), gym.getId(), + date.atTime(hour, 0), 60, type.getDefaultCapacity())); + } + } + + if (newClasses.isEmpty()) { + log.info("Rolling class window already covers today → +{} days; nothing to add.", WINDOW_DAYS); + return; + } + gymClassRepository.saveAll(newClasses); + log.info("Added {} classes across {} day(s) to maintain the rolling {}-day window.", + newClasses.size(), newClasses.size() / CLASS_HOURS.length, WINDOW_DAYS); + } + + /** Daily top-up so the rolling window advances as time passes, surviving reboots. */ + @Scheduled(cron = "0 0 3 * * *") + @Transactional + public void dailyRollingClassTopUp() { + log.debug("Running daily rolling class top-up..."); + ensureUpcomingClasses(); + } + + private void seedBaseDataIfEmpty() { if (gymRepository.count() > 0 || classTypeRepository.count() > 0) { - log.info("Database already contains demo data — skipping seeding."); + log.info("Base demo data already present — skipping one-time seeding."); return; } @@ -109,25 +175,8 @@ public void run(String... args) { List classTypes = List.of(yoga, hiit, strength, spinning); classTypeRepository.saveAll(classTypes); - // ── Gym Classes ────────────────────────────────────────── - List allGymClasses = new ArrayList<>(); - LocalDateTime startBase = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0); - - List instructors = List.of(instructor1, instructor2, instructor3); - - for (int day = 0; day < 14; day++) { - LocalDateTime dayDate = startBase.plusDays(day); - for (int i = 0; i < 3; i++) { - Gym randomGym = gyms.get(random.nextInt(gyms.size())); - ClassType randomType = classTypes.get(random.nextInt(classTypes.size())); - User randomInstructor = instructors.get(random.nextInt(instructors.size())); - - allGymClasses.add(createGymClass(randomType, randomInstructor.getId(), randomGym.getId(), dayDate.withHour(8 + i * 4), 60, randomType.getDefaultCapacity())); - } - } - gymClassRepository.saveAll(allGymClasses); - - log.info("Database seeding complete!"); + // Gym classes are created by ensureUpcomingClasses() so they always start "today". + log.info("Base demo data seeding complete!"); } private static final String DEV_FALLBACK_PASSWORD = "password123"; diff --git a/src/main/java/com/example/boka/gymclass/domain/GymClassRepository.java b/src/main/java/com/example/boka/gymclass/domain/GymClassRepository.java index 4ec8e40..0b56e94 100644 --- a/src/main/java/com/example/boka/gymclass/domain/GymClassRepository.java +++ b/src/main/java/com/example/boka/gymclass/domain/GymClassRepository.java @@ -28,4 +28,6 @@ Page findByClassType_IdInAndStatusAndStartTimeAfter( ); Page findByStatus(ClassStatus status, Pageable pageable); + + List findByStartTimeBetween(LocalDateTime start, LocalDateTime end); }