diff --git a/backend/src/main/java/com/thughari/jobtrackerpro/JobTrackerProApplication.java b/backend/src/main/java/com/thughari/jobtrackerpro/JobTrackerProApplication.java index e4f3775..c0dfe77 100644 --- a/backend/src/main/java/com/thughari/jobtrackerpro/JobTrackerProApplication.java +++ b/backend/src/main/java/com/thughari/jobtrackerpro/JobTrackerProApplication.java @@ -7,10 +7,12 @@ import org.springframework.cache.annotation.EnableCaching; import org.springframework.data.web.config.EnableSpringDataWebSupport; import org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableCaching @EnableSpringDataWebSupport(pageSerializationMode = PageSerializationMode.VIA_DTO) +@EnableScheduling public class JobTrackerProApplication { public static void main(String[] args) { diff --git a/backend/src/main/java/com/thughari/jobtrackerpro/repo/JobRepository.java b/backend/src/main/java/com/thughari/jobtrackerpro/repo/JobRepository.java index 04b6f22..cd91c4e 100644 --- a/backend/src/main/java/com/thughari/jobtrackerpro/repo/JobRepository.java +++ b/backend/src/main/java/com/thughari/jobtrackerpro/repo/JobRepository.java @@ -3,12 +3,14 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import com.thughari.jobtrackerpro.dto.DashboardStatsDTO; import com.thughari.jobtrackerpro.entity.Job; +import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -40,5 +42,24 @@ Page findWithFilters( WHERE j.userEmail = :email """) DashboardStatsDTO getStatsByEmail(@Param("email") String email); + + @Query("SELECT DISTINCT j.userEmail FROM Job j WHERE j.updatedAt < :cutoff AND j.status NOT IN ('Rejected', 'Offer Received')") + List findUserEmailsWithStaleJobs(@Param("cutoff") LocalDateTime cutoff); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + UPDATE Job j + SET j.status = 'Rejected', + j.stageStatus = 'failed', + j.updatedAt = :now, + j.notes = CONCAT(COALESCE(j.notes, ''), :note) + WHERE j.updatedAt < :cutoff + AND j.status NOT IN ('Rejected', 'Offer Received') + """) + void markStaleJobsAsRejected( + @Param("cutoff") LocalDateTime cutoff, + @Param("now") LocalDateTime now, + @Param("note") String note + ); } \ No newline at end of file diff --git a/backend/src/main/java/com/thughari/jobtrackerpro/scheduler/JobScheduler.java b/backend/src/main/java/com/thughari/jobtrackerpro/scheduler/JobScheduler.java new file mode 100644 index 0000000..9a6ca50 --- /dev/null +++ b/backend/src/main/java/com/thughari/jobtrackerpro/scheduler/JobScheduler.java @@ -0,0 +1,41 @@ +package com.thughari.jobtrackerpro.scheduler; + +import com.thughari.jobtrackerpro.service.JobService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * This scheduler runs a maintenance task every day at midnight to process stale + * job applications that haven't been updated in over 90 days. + * + * Instead of physical deletion, the task updates these applications to a 'Rejected' + * status and adds a system note, helping keep the user dashboard relevant while + * preserving historical data. + */ + +@Component +@Slf4j +public class JobScheduler { + + private final JobService jobService; + + public JobScheduler(JobService jobService) { + this.jobService = jobService; + } + + /** + * Runs every day at midnight UTC (5:30 AM IST). + * Format: second, minute, hour, day of month, month, day(s) of week + */ + @Scheduled(cron = "0 0 0 * * *") + public void runStaleJobCleanup() { + log.info("Starting scheduled cleanup of stale applications..."); + try { + jobService.cleanupStaleApplications(); + log.info("Scheduled cleanup completed successfully."); + } catch (Exception e) { + log.error("Error during scheduled cleanup: {}", e.getMessage()); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/thughari/jobtrackerpro/service/CloudStorageService.java b/backend/src/main/java/com/thughari/jobtrackerpro/service/CloudStorageService.java index 0dda392..63bb05c 100644 --- a/backend/src/main/java/com/thughari/jobtrackerpro/service/CloudStorageService.java +++ b/backend/src/main/java/com/thughari/jobtrackerpro/service/CloudStorageService.java @@ -21,7 +21,6 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; -import java.util.UUID; @Service @Slf4j diff --git a/backend/src/main/java/com/thughari/jobtrackerpro/service/JobService.java b/backend/src/main/java/com/thughari/jobtrackerpro/service/JobService.java index 72581b2..e07e3d4 100644 --- a/backend/src/main/java/com/thughari/jobtrackerpro/service/JobService.java +++ b/backend/src/main/java/com/thughari/jobtrackerpro/service/JobService.java @@ -6,6 +6,8 @@ import com.thughari.jobtrackerpro.repo.JobRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; @@ -17,6 +19,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; @@ -27,9 +30,12 @@ public class JobService { private final JobRepository jobRepository; + + private final CacheManager cacheManager; - public JobService(JobRepository jobRepository) { + public JobService(JobRepository jobRepository, CacheManager cacheManager) { this.jobRepository = jobRepository; + this.cacheManager = cacheManager; } private final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm 'UTC'"); @@ -157,6 +163,35 @@ public void createOrUpdateJob(JobDTO incomingJob, String userEmail) { } + public void cleanupStaleApplications() { + LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC); + LocalDateTime threeMonthsAgo = now.minusMonths(3); + + List affectedEmails = jobRepository.findUserEmailsWithStaleJobs(threeMonthsAgo); + + if (affectedEmails.isEmpty()) { + log.info("System Cleanup: No stale applications found."); + return; + } + + String autoNote = "\n[" + now.format(fmt) + "] Status auto-set to Rejected (3 months inactivity)."; + jobRepository.markStaleJobsAsRejected(threeMonthsAgo, now, autoNote); + + Cache jobList = cacheManager.getCache("jobList"); + Cache jobDashboard = cacheManager.getCache("jobDashboard"); + Cache jobPages = cacheManager.getCache("jobPages"); + + for (String email : affectedEmails) { + if (jobList != null) jobList.evict(email); + if (jobDashboard != null) jobDashboard.evict(email); + } + + // Clear all paged results once + if (jobPages != null) jobPages.clear(); + + log.info("System Cleanup: Successfully rejected stale jobs for {} users.", affectedEmails.size()); + } + private Job findBestMatch(List existingJobs, JobDTO incoming) { if (incoming.getCompany() == null) return null;