Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -40,5 +42,24 @@ Page<Job> 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<String> 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
);

}
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.UUID;

@Service
@Slf4j
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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'");
Expand Down Expand Up @@ -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<String> 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<Job> existingJobs, JobDTO incoming) {
if (incoming.getCompany() == null) return null;

Expand Down