diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index c279998d..de1eb115 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -227,6 +227,56 @@ "users.item.post", "users.item.put" ] + }, + { + "methods": [ + "GET" + ], + "pathPattern": "/data-export-spring/job-deletion-intervals", + "permissionsRequired": [ + "data-export.job-deletion-intervals.collection.get" + ], + "modulePermissions": [] + }, + { + "methods": [ + "GET" + ], + "pathPattern": "/data-export-spring/job-deletion-intervals/{exportType}", + "permissionsRequired": [ + "data-export.job-deletion-intervals.item.get" + ], + "modulePermissions": [] + }, + { + "methods": [ + "POST" + ], + "pathPattern": "/data-export-spring/job-deletion-intervals", + "permissionsRequired": [ + "data-export.job-deletion-intervals.item.post" + ], + "modulePermissions": [] + }, + { + "methods": [ + "PUT" + ], + "pathPattern": "/data-export-spring/job-deletion-intervals/{exportType}", + "permissionsRequired": [ + "data-export.job-deletion-intervals.item.put" + ], + "modulePermissions": [] + }, + { + "methods": [ + "DELETE" + ], + "pathPattern": "/data-export-spring/job-deletion-intervals/{exportType}", + "permissionsRequired": [ + "data-export.job-deletion-intervals.item.delete" + ], + "modulePermissions": [] } ] }, @@ -377,6 +427,43 @@ "orders-storage.purchase-orders.collection.get", "orders-storage.pieces.collection.get" ] + }, + { + "permissionName": "data-export.job-deletion-intervals.collection.get", + "displayName": "Get job deletion intervals", + "description": "Get job deletion intervals" + }, + { + "permissionName": "data-export.job-deletion-intervals.item.get", + "displayName": "Get job deletion interval", + "description": "Get job deletion interval by export type" + }, + { + "permissionName": "data-export.job-deletion-intervals.item.post", + "displayName": "Create/update job deletion interval", + "description": "Create/update job deletion interval" + }, + { + "permissionName": "data-export.job-deletion-intervals.item.put", + "displayName": "Update job deletion interval", + "description": "Update job deletion interval" + }, + { + "permissionName": "data-export.job-deletion-intervals.item.delete", + "displayName": "Delete job deletion interval", + "description": "Delete job deletion interval" + }, + { + "permissionName": "data-export.job-deletion-intervals.all", + "displayName": "Job deletion intervals - all permissions", + "description": "All permissions for job deletion intervals", + "subPermissions": [ + "data-export.job-deletion-intervals.collection.get", + "data-export.job-deletion-intervals.item.get", + "data-export.job-deletion-intervals.item.post", + "data-export.job-deletion-intervals.item.put", + "data-export.job-deletion-intervals.item.delete" + ] } ], "metadata": { diff --git a/folio-export-common b/folio-export-common index 70934167..b1e667c8 160000 --- a/folio-export-common +++ b/folio-export-common @@ -1 +1 @@ -Subproject commit 7093416702cf2d97513fdea5cb80cfda157416fa +Subproject commit b1e667c83f0f1746740900a31db52f6d1d806a7c diff --git a/pom.xml b/pom.xml index 52d5d9c6..496dbdc0 100644 --- a/pom.xml +++ b/pom.xml @@ -31,8 +31,8 @@ UTF-8 ${project.basedir}/src/main/resources/swagger.api/jobs.yaml - ${project.basedir}/src/main/resources/swagger.api/export-configs.yaml - + ${project.basedir}/src/main/resources/swagger.api/export-configs.yaml + ${project.basedir}/src/main/resources/swagger.api/job-deletion-intervals.yaml 9.0.0 4.1.1 @@ -397,6 +397,36 @@ + + job-deletion-intervals + + generate + + + ${job-deletion-intervals.yaml.file} + ${project.build.directory}/generated-sources + spring + ${project.groupId}.des.domain.dto.delete_interval + ${project.groupId}.des.rest.resource + true + true + true + true + false + true + ApiUtil.java + true + + ExportType=org.folio.des.domain.dto.ExportType + + + true + java8-localdatetime + true + true + + + diff --git a/src/main/java/org/folio/de/entity/JobDeletionIntervalEntity.java b/src/main/java/org/folio/de/entity/JobDeletionIntervalEntity.java new file mode 100644 index 00000000..e6d75133 --- /dev/null +++ b/src/main/java/org/folio/de/entity/JobDeletionIntervalEntity.java @@ -0,0 +1,38 @@ +package org.folio.de.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; +import org.folio.des.domain.dto.ExportType; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "job_deletion_interval") +@Data +public class JobDeletionIntervalEntity { + @Id + @Enumerated(EnumType.STRING) + @Column(name = "export_type") + private ExportType exportType; + + @Column(name = "retention_days", nullable = false) + private Integer retentionDays; + + @Column(name = "created_date", nullable = false) + private LocalDateTime createdDate; + + @Column(name = "created_by", nullable = false) + private UUID createdBy; + + @Column(name = "updated_date") + private LocalDateTime updatedDate; + + @Column(name = "updated_by") + private UUID updatedBy; +} diff --git a/src/main/java/org/folio/des/controller/JobDeletionIntervalController.java b/src/main/java/org/folio/des/controller/JobDeletionIntervalController.java new file mode 100644 index 00000000..18308ceb --- /dev/null +++ b/src/main/java/org/folio/des/controller/JobDeletionIntervalController.java @@ -0,0 +1,49 @@ +package org.folio.des.controller; + +import lombok.RequiredArgsConstructor; +import org.folio.des.domain.dto.ExportType; +import org.folio.des.domain.dto.delete_interval.JobDeletionInterval; +import org.folio.des.domain.dto.delete_interval.JobDeletionIntervalCollection; +import org.folio.des.rest.resource.JobDeletionIntervalsApi; +import org.folio.des.service.JobDeletionIntervalService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/data-export-spring") +@RequiredArgsConstructor +public class JobDeletionIntervalController implements JobDeletionIntervalsApi { + private final JobDeletionIntervalService service; + + @Override + public ResponseEntity getAllJobDeletionIntervals() { + var intervals = service.getAll(); + return ResponseEntity.ok(intervals); + } + + @Override + public ResponseEntity getJobDeletionIntervalByExportType(ExportType exportType) { + return ResponseEntity.ok(service.get(exportType)); + } + + @Override + public ResponseEntity createJobDeletionInterval(JobDeletionInterval interval) { + return ResponseEntity + .status(HttpStatus.CREATED) + .body(service.create(interval)); + } + + @Override + public ResponseEntity updateJobDeletionInterval(ExportType exportType, JobDeletionInterval interval) { + interval.setExportType(exportType); + return ResponseEntity.ok(service.update(interval)); + } + + @Override + public ResponseEntity deleteJobDeletionInterval(ExportType exportType) { + service.delete(exportType); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/org/folio/des/repository/JobDataExportRepository.java b/src/main/java/org/folio/des/repository/JobDataExportRepository.java index 67143373..6652bf02 100644 --- a/src/main/java/org/folio/des/repository/JobDataExportRepository.java +++ b/src/main/java/org/folio/des/repository/JobDataExportRepository.java @@ -1,6 +1,7 @@ package org.folio.des.repository; import org.folio.de.entity.Job; +import org.folio.des.domain.dto.ExportType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -13,6 +14,6 @@ public interface JobDataExportRepository extends JpaRepository { @Query(value = "SELECT nextval('job-number')", nativeQuery = true) Integer getNextJobNumber(); - List findByUpdatedDateBefore(Date updatedDate); + List findByTypeAndUpdatedDateBefore(ExportType type, Date updatedDate); } diff --git a/src/main/java/org/folio/des/repository/JobDeletionIntervalRepository.java b/src/main/java/org/folio/des/repository/JobDeletionIntervalRepository.java new file mode 100644 index 00000000..3770d9e6 --- /dev/null +++ b/src/main/java/org/folio/des/repository/JobDeletionIntervalRepository.java @@ -0,0 +1,10 @@ +package org.folio.des.repository; + +import org.folio.de.entity.JobDeletionIntervalEntity; +import org.folio.des.domain.dto.ExportType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface JobDeletionIntervalRepository extends JpaRepository { +} diff --git a/src/main/java/org/folio/des/service/FolioTenantService.java b/src/main/java/org/folio/des/service/FolioTenantService.java index 09284206..ca743fa0 100644 --- a/src/main/java/org/folio/des/service/FolioTenantService.java +++ b/src/main/java/org/folio/des/service/FolioTenantService.java @@ -7,7 +7,6 @@ import org.folio.des.scheduling.quartz.ScheduledJobsRemover; import org.folio.des.service.bursarlegacy.BursarExportLegacyJobService; import org.folio.des.service.bursarlegacy.BursarMigrationService; -import org.folio.des.service.config.BulkEditConfigService; import org.folio.des.service.config.impl.BursarFeesFinesExportConfigService; import org.folio.spring.FolioExecutionContext; import org.folio.spring.liquibase.FolioSpringLiquibase; @@ -26,7 +25,6 @@ public class FolioTenantService extends TenantService { private final KafkaService kafka; - private final BulkEditConfigService bulkEditConfigService; private final EdifactScheduledJobInitializer edifactScheduledJobInitializer; private final ScheduledJobsRemover scheduledJobsRemover; private final BursarScheduledJobInitializer bursarScheduledJobInitializer; @@ -38,7 +36,7 @@ public class FolioTenantService extends TenantService { private final BursarFeesFinesExportConfigService bursarFeesFinesExportConfigService; public FolioTenantService(JdbcTemplate jdbcTemplate, FolioExecutionContext context, FolioSpringLiquibase folioSpringLiquibase, - PrepareSystemUserService prepareSystemUserService, KafkaService kafka, BulkEditConfigService bulkEditConfigService, + PrepareSystemUserService prepareSystemUserService, KafkaService kafka, EdifactScheduledJobInitializer edifactScheduledJobInitializer, ScheduledJobsRemover scheduledJobsRemover, BursarScheduledJobInitializer bursarScheduledJobInitializer, OldJobDeleteScheduler oldJobDeleteScheduler, BursarExportLegacyJobService bursarExportLegacyJobService, JobService jobService, @@ -46,7 +44,6 @@ public FolioTenantService(JdbcTemplate jdbcTemplate, FolioExecutionContext conte super(jdbcTemplate, context, folioSpringLiquibase); this.prepareSystemUserService = prepareSystemUserService; this.kafka = kafka; - this.bulkEditConfigService = bulkEditConfigService; this.edifactScheduledJobInitializer = edifactScheduledJobInitializer; this.scheduledJobsRemover = scheduledJobsRemover; this.bursarScheduledJobInitializer = bursarScheduledJobInitializer; @@ -64,7 +61,6 @@ protected void afterTenantUpdate(TenantAttributes tenantAttributes) { bursarMigrationService.updateLegacyBursarIfNeeded(tenantAttributes, bursarFeesFinesExportConfigService, bursarExportLegacyJobService, jobService); bursarScheduledJobInitializer.initAllScheduledJob(tenantAttributes); - bulkEditConfigService.checkBulkEditConfiguration(); edifactScheduledJobInitializer.initAllScheduledJob(tenantAttributes); oldJobDeleteScheduler.scheduleOldJobDeletion(context.getTenantId()); kafka.createKafkaTopics(); diff --git a/src/main/java/org/folio/des/service/JobDeletionIntervalService.java b/src/main/java/org/folio/des/service/JobDeletionIntervalService.java new file mode 100644 index 00000000..783b4a14 --- /dev/null +++ b/src/main/java/org/folio/des/service/JobDeletionIntervalService.java @@ -0,0 +1,45 @@ +package org.folio.des.service; + +import org.folio.des.domain.dto.ExportType; +import org.folio.des.domain.dto.delete_interval.JobDeletionInterval; +import org.folio.des.domain.dto.delete_interval.JobDeletionIntervalCollection; + +public interface JobDeletionIntervalService { + /** + * Get all job deletion intervals. + * + * @return collection of job deletion intervals + */ + JobDeletionIntervalCollection getAll(); + + /** + * Get interval by export type. + * + * @param exportType the export type to retrieve + * @return the found job deletion interval + */ + JobDeletionInterval get(ExportType exportType); + + /** + * Create a new job deletion interval. + * + * @param interval the interval to create + * @return the created interval + */ + JobDeletionInterval create(JobDeletionInterval interval); + + /** + * Update an existing job deletion interval. + * + * @param interval the interval to update + * @return the updated interval + */ + JobDeletionInterval update(JobDeletionInterval interval); + + /** + * Delete a job deletion interval. + * + * @param exportType the export type to delete + */ + void delete(ExportType exportType); +} diff --git a/src/main/java/org/folio/des/service/config/BulkEditConfigService.java b/src/main/java/org/folio/des/service/config/BulkEditConfigService.java deleted file mode 100644 index 8596e162..00000000 --- a/src/main/java/org/folio/des/service/config/BulkEditConfigService.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.folio.des.service.config; - -import static org.folio.des.util.BulkEditConstants.BULK_EDIT_CONFIGURATIONS_QUERY_TEMPLATE; -import static org.folio.des.util.BulkEditConstants.CONFIG_NAME; -import static org.folio.des.util.BulkEditConstants.DEFAULT_BULK_EDIT_JOB_EXPIRATION_PERIOD; -import static org.folio.des.util.BulkEditConstants.EXPIRATION_PERIOD_PARAMETER; -import static org.folio.des.util.BulkEditConstants.JOB_EXPIRATION_PERIOD; -import static org.folio.des.util.BulkEditConstants.MODULE_NAME; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.extern.log4j.Log4j2; -import org.folio.des.client.ConfigurationClient; -import org.folio.des.domain.dto.ConfigurationCollection; -import org.folio.des.domain.dto.ModelConfiguration; -import org.springframework.stereotype.Service; - -import java.util.Collections; -import java.util.Map; - -@Service -@Log4j2 -@RequiredArgsConstructor -public class BulkEditConfigService { - private final ConfigurationClient configurationClient; - - public ConfigurationCollection getBulkEditConfigurations(Integer limit) { - return configurationClient.getConfigurations(String.format(BULK_EDIT_CONFIGURATIONS_QUERY_TEMPLATE, MODULE_NAME, CONFIG_NAME), limit); - } - - @SneakyThrows - public int getBulkEditJobExpirationPeriod() { - var configs = getBulkEditConfigurations(1); - if (!configs.getConfigs().isEmpty()) { - var config = configs.getConfigs().get(0); - log.info("Found bulk-edit configuration: {}", config); - var bulkEditConfig = new ObjectMapper().readValue(config.getValue(), new TypeReference>(){}); - return Integer.parseInt(bulkEditConfig.getOrDefault(JOB_EXPIRATION_PERIOD, DEFAULT_BULK_EDIT_JOB_EXPIRATION_PERIOD)); - } - return Integer.parseInt(DEFAULT_BULK_EDIT_JOB_EXPIRATION_PERIOD); - } - - public void checkBulkEditConfiguration() { - if (getBulkEditConfigurations(1).getConfigs().isEmpty()) { - log.info("Bulk-edit configuration was not found, uploading default"); - configurationClient.postConfiguration(buildDefaultConfig()); - } - } - - @SneakyThrows - public static ModelConfiguration buildDefaultConfig() { - var configMap = Collections.singletonMap(EXPIRATION_PERIOD_PARAMETER, DEFAULT_BULK_EDIT_JOB_EXPIRATION_PERIOD); - return new ModelConfiguration() - .module(MODULE_NAME) - .configName(CONFIG_NAME) - ._default(true) - .enabled(true) - .value(new ObjectMapper().writeValueAsString(configMap)); - } -} diff --git a/src/main/java/org/folio/des/service/impl/JobDeletionIntervalServiceImpl.java b/src/main/java/org/folio/des/service/impl/JobDeletionIntervalServiceImpl.java new file mode 100644 index 00000000..e5e3a6c3 --- /dev/null +++ b/src/main/java/org/folio/des/service/impl/JobDeletionIntervalServiceImpl.java @@ -0,0 +1,112 @@ +package org.folio.des.service.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.folio.de.entity.JobDeletionIntervalEntity; +import org.folio.des.domain.dto.ExportType; +import org.folio.des.domain.dto.delete_interval.JobDeletionInterval; +import org.folio.des.domain.dto.delete_interval.JobDeletionIntervalCollection; +import org.folio.des.domain.dto.delete_interval.Metadata; +import org.folio.des.repository.JobDeletionIntervalRepository; +import org.folio.des.service.JobDeletionIntervalService; +import org.folio.spring.FolioExecutionContext; +import org.folio.spring.exception.NotFoundException; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDateTime; +import java.util.stream.Collectors; + +@Service +@Log4j2 +@RequiredArgsConstructor +public class JobDeletionIntervalServiceImpl implements JobDeletionIntervalService { + public static final String CREATED_BY_SYSTEM = "1c49bbf9-abda-4c02-a7cc-0504f8301268"; + + private final JobDeletionIntervalRepository repository; + private final FolioExecutionContext context; + + @Override + public JobDeletionIntervalCollection getAll() { + var entities = repository.findAll(); + var intervals = entities.stream() + .map(this::mapToDto) + .toList(); + return new JobDeletionIntervalCollection() + .jobDeletionIntervals(intervals) + .totalRecords(intervals.size()); + } + + @Override + public JobDeletionInterval get(ExportType exportType) { + return repository.findById(exportType) + .map(this::mapToDto) + .orElseThrow(() -> new NotFoundException("Interval for export type " + exportType + " not found")); + } + + @Override + public JobDeletionInterval create(JobDeletionInterval interval) { + log.info("create:: Creating job deletion interval: {}", interval); + if (repository.existsById(interval.getExportType())) { + throw new ResponseStatusException(HttpStatus.CONFLICT, + "Interval for export type " + interval.getExportType() + " already exists"); + } + var entity = mapToEntityForCreate(interval); + return mapToDto(repository.save(entity)); + } + + @Override + public JobDeletionInterval update(JobDeletionInterval interval) { + log.info("update:: Updating job deletion interval: {}", interval); + if (!repository.existsById(interval.getExportType())) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, + "Interval for export type " + interval.getExportType() + " not found"); + } + var entity = mapToEntityForUpdate(interval); + return mapToDto(repository.save(entity)); + } + + @Override + public void delete(ExportType exportType) { + log.info("delete:: Deleting interval for export type: {}", exportType); + repository.deleteById(exportType); + } + + private JobDeletionInterval mapToDto(JobDeletionIntervalEntity entity) { + JobDeletionInterval interval = new JobDeletionInterval() + .exportType(entity.getExportType()) + .retentionDays(entity.getRetentionDays()); + + Metadata metadata = new Metadata(); + metadata.setCreatedDate(entity.getCreatedDate()); + metadata.setCreatedByUserId(entity.getCreatedBy()); + metadata.setUpdatedDate(entity.getUpdatedDate()); + metadata.setUpdatedByUserId(entity.getUpdatedBy()); + + interval.setMetadata(metadata); + return interval; + } + + private JobDeletionIntervalEntity mapToEntityForUpdate(JobDeletionInterval dto) { + var entity = new JobDeletionIntervalEntity(); + entity.setExportType(dto.getExportType()); + entity.setRetentionDays(dto.getRetentionDays()); + entity.setUpdatedBy(context.getUserId()); + entity.setUpdatedDate(LocalDateTime.now()); + if (dto.getMetadata() != null) { + entity.setCreatedDate(dto.getMetadata().getCreatedDate()); + entity.setCreatedBy(dto.getMetadata().getCreatedByUserId()); + } + return entity; + } + + private JobDeletionIntervalEntity mapToEntityForCreate(JobDeletionInterval dto) { + var entity = new JobDeletionIntervalEntity(); + entity.setExportType(dto.getExportType()); + entity.setRetentionDays(dto.getRetentionDays()); + entity.setCreatedBy(context.getUserId()); + entity.setCreatedDate(LocalDateTime.now()); + return entity; + } +} diff --git a/src/main/java/org/folio/des/service/impl/JobServiceImpl.java b/src/main/java/org/folio/des/service/impl/JobServiceImpl.java index e59116bc..fbaf0ab2 100644 --- a/src/main/java/org/folio/des/service/impl/JobServiceImpl.java +++ b/src/main/java/org/folio/des/service/impl/JobServiceImpl.java @@ -1,11 +1,6 @@ package org.folio.des.service.impl; import static java.util.Objects.nonNull; -import static org.folio.des.domain.dto.ExportType.BULK_EDIT_IDENTIFIERS; -import static org.folio.des.domain.dto.ExportType.BULK_EDIT_QUERY; -import static org.folio.des.domain.dto.ExportType.BULK_EDIT_UPDATE; -import static org.folio.des.domain.dto.ExportType.CLAIMS; -import static org.folio.des.domain.dto.ExportType.EDIFACT_ORDERS_EXPORT; import java.io.InputStream; import java.net.HttpURLConnection; @@ -18,12 +13,10 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.SetUtils; import org.apache.commons.lang3.StringUtils; import org.folio.de.entity.Job; import org.folio.des.client.ConfigurationClient; @@ -35,13 +28,14 @@ import org.folio.des.domain.dto.PresignedUrl; import org.folio.des.domain.dto.ScheduleParameters; import org.folio.des.domain.dto.VendorEdiOrdersExportConfig; +import org.folio.des.domain.dto.delete_interval.JobDeletionInterval; import org.folio.des.exceptions.FileDownloadException; import org.folio.des.repository.CQLService; import org.folio.des.repository.JobDataExportRepository; import org.folio.des.security.JWTokenUtils; +import org.folio.des.service.JobDeletionIntervalService; import org.folio.des.service.JobExecutionService; import org.folio.des.service.JobService; -import org.folio.des.service.config.BulkEditConfigService; import org.folio.des.service.util.JobMapperUtil; import org.folio.spring.FolioExecutionContext; import org.folio.spring.data.OffsetRequest; @@ -66,8 +60,6 @@ public class JobServiceImpl implements JobService { private int jobDownloadFileConnectionTimeout; private static final Map OUTPUT_FORMATS = new EnumMap<>(ExportType.class); - private static final Set BULK_EDIT_TYPES = Set.of(BULK_EDIT_IDENTIFIERS, BULK_EDIT_QUERY, BULK_EDIT_UPDATE); - private static final Set ACQUISITION_TYPES = Set.of(EDIFACT_ORDERS_EXPORT, CLAIMS); static { OUTPUT_FORMATS.put(ExportType.BURSAR_FEES_FINES, "Fees & Fines Bursar Report"); @@ -80,8 +72,8 @@ public class JobServiceImpl implements JobService { private final JobDataExportRepository repository; private final FolioExecutionContext context; private final CQLService cqlService; - private final BulkEditConfigService bulkEditConfigService; private final ConfigurationClient client; + private final JobDeletionIntervalService deletionIntervalService; @Transactional(readOnly = true) @Override @@ -201,14 +193,20 @@ public org.folio.des.domain.dto.Job upsertAndSendToKafka(org.folio.des.domain.dt @Transactional @Override public void deleteOldJobs() { - var expirationDate = createExpirationDate(jobExpirationPeriod); - log.info("Collecting old jobs with 'updatedDate' less than {}.", expirationDate); - Set excludedTypes = SetUtils.union(BULK_EDIT_TYPES, ACQUISITION_TYPES); - var jobsToDelete = filterJobsNotMatchingExportTypes(repository.findByUpdatedDateBefore(expirationDate), excludedTypes); + List jobDeletionIntervals = deletionIntervalService.getAll().getJobDeletionIntervals(); + if (CollectionUtils.isEmpty(jobDeletionIntervals)) { + log.info("deleteOldJobs:: No job deletion intervals found, skipping."); + return; + } + List jobsToDelete = new ArrayList<>(); - expirationDate = createExpirationDate(bulkEditConfigService.getBulkEditJobExpirationPeriod()); - log.info("Collecting old bulk-edit jobs with 'updatedDate' less than {}.", expirationDate); - jobsToDelete.addAll(filterJobsMatchingExportTypes(repository.findByUpdatedDateBefore(expirationDate), BULK_EDIT_TYPES)); + jobDeletionIntervals.forEach(interval -> { + log.info("deleteOldJobs:: Deleting old jobs for export type: {} with retention days: {}.", interval.getExportType(), interval.getRetentionDays()); + var expirationDate = createExpirationDate(interval.getRetentionDays()); + List expiredJobs = repository.findByTypeAndUpdatedDateBefore(interval.getExportType(), expirationDate); + log.info("deleteOldJobs:: Found {} old jobs for export type: {} for deletion.", expiredJobs.size(), interval.getExportType()); + jobsToDelete.addAll(expiredJobs); + }); deleteJobs(jobsToDelete); } @@ -230,18 +228,6 @@ private Date createExpirationDate(int days) { return Date.from(LocalDate.now().minusDays(days).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); } - private List filterJobsNotMatchingExportTypes(List jobs, Set exportTypes) { - return new ArrayList<>(jobs.stream() - .filter(j -> !exportTypes.contains(j.getType())) - .toList()); - } - - private List filterJobsMatchingExportTypes(List jobs, Set exportTypes) { - return jobs.stream() - .filter(j -> exportTypes.contains(j.getType())) - .toList(); - } - public void deleteJobs(List jobs) { if (CollectionUtils.isEmpty(jobs)) { log.info("No old jobs were found."); diff --git a/src/main/java/org/folio/des/util/BulkEditConstants.java b/src/main/java/org/folio/des/util/BulkEditConstants.java deleted file mode 100644 index da4572fd..00000000 --- a/src/main/java/org/folio/des/util/BulkEditConstants.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.folio.des.util; - -import lombok.experimental.UtilityClass; - -@UtilityClass -public class BulkEditConstants { - public static final String MODULE_NAME = "BULKEDIT"; - public static final String CONFIG_NAME = "general"; - public static final String EXPIRATION_PERIOD_PARAMETER = "jobExpirationPeriod"; - public static final String DEFAULT_BULK_EDIT_JOB_EXPIRATION_PERIOD = "14"; - public static final String BULK_EDIT_CONFIGURATIONS_QUERY_TEMPLATE = "module==%s and configName==%s"; - public static final String JOB_EXPIRATION_PERIOD = "jobExpirationPeriod"; -} diff --git a/src/main/resources/db/changelog/changelog-master.xml b/src/main/resources/db/changelog/changelog-master.xml index 5554df60..920fe660 100644 --- a/src/main/resources/db/changelog/changelog-master.xml +++ b/src/main/resources/db/changelog/changelog-master.xml @@ -14,5 +14,6 @@ + diff --git a/src/main/resources/db/changelog/changes/17_07_2025_create_job_deletion_interval.xml b/src/main/resources/db/changelog/changes/17_07_2025_create_job_deletion_interval.xml new file mode 100644 index 00000000..8760b3d9 --- /dev/null +++ b/src/main/resources/db/changelog/changes/17_07_2025_create_job_deletion_interval.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/swagger.api/job-deletion-intervals.yaml b/src/main/resources/swagger.api/job-deletion-intervals.yaml new file mode 100644 index 00000000..f0be41d8 --- /dev/null +++ b/src/main/resources/swagger.api/job-deletion-intervals.yaml @@ -0,0 +1,149 @@ +openapi: 3.0.0 +info: + title: Data Export Spring Job Deletion Intervals + version: v1 +servers: + - url: /data-export-spring/ +paths: + /job-deletion-intervals: + get: + description: Get all job deletion intervals + operationId: getAllJobDeletionIntervals + responses: + "200": + description: Job deletion intervals successfully retrieved + content: + application/json: + schema: + $ref: "#/components/schemas/jobDeletionIntervalCollection" + "500": + description: Internal server errors, e.g. due to misconfiguration + content: + application/json: + schema: + $ref: "#/components/schemas/errors" + post: + description: Create a new job deletion interval + operationId: createJobDeletionInterval + responses: + "201": + description: Job deletion interval created + content: + application/json: + schema: + $ref: "#/components/schemas/jobDeletionInterval" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/errors" + "409": + description: Interval for this export type already exists + content: + application/json: + schema: + $ref: "#/components/schemas/errors" + "500": + description: Internal server errors + content: + application/json: + schema: + $ref: "#/components/schemas/errors" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/jobDeletionInterval" + required: true + /job-deletion-intervals/{exportType}: + get: + description: Get job deletion interval by export type + operationId: getJobDeletionIntervalByExportType + parameters: + - $ref: '#/components/parameters/exportType' + responses: + '200': + description: Returns job deletion interval + content: + application/json: + schema: + $ref: '#/components/schemas/jobDeletionInterval' + '404': + description: Job deletion interval not found + content: + text/plain: + schema: + type: string + put: + description: Update an existing job deletion interval + operationId: updateJobDeletionInterval + parameters: + - $ref: '#/components/parameters/exportType' + responses: + "200": + description: Job deletion interval updated successfully + content: + application/json: + schema: + $ref: "#/components/schemas/jobDeletionInterval" + "400": + description: Bad request + content: + application/json: + schema: + $ref: "#/components/schemas/errors" + "404": + description: Job deletion interval not found + content: + application/json: + schema: + $ref: "#/components/schemas/errors" + "500": + description: Internal server errors + content: + application/json: + schema: + $ref: "#/components/schemas/errors" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/jobDeletionInterval" + required: true + delete: + description: Delete a job deletion interval by export type + operationId: deleteJobDeletionInterval + parameters: + - $ref: '#/components/parameters/exportType' + responses: + "204": + description: Job deletion interval successfully deleted + "404": + description: Job deletion interval not found + content: + application/json: + schema: + $ref: "#/components/schemas/errors" + "500": + description: Internal server errors + content: + application/json: + schema: + $ref: "#/components/schemas/errors" +components: + parameters: + exportType: + name: exportType + in: path + required: true + description: Export type of the interval + schema: + $ref: "./jobs.yaml#/components/schemas/exportType" + schemas: + errors: + $ref: '../../../../folio-export-common/schemas/common/errors.json#/Errors' + jobDeletionInterval: + $ref: '../../../../folio-export-common/schemas/jobDeletionInterval.json#/JobDeletionInterval' + jobDeletionIntervalCollection: + $ref: '../../../../folio-export-common/schemas/jobDeletionIntervalCollection.json#/JobDeletionIntervalCollection' diff --git a/src/test/java/org/folio/des/CopilotGenerated.java b/src/test/java/org/folio/des/CopilotGenerated.java index fb8f698c..c3851c82 100644 --- a/src/test/java/org/folio/des/CopilotGenerated.java +++ b/src/test/java/org/folio/des/CopilotGenerated.java @@ -13,7 +13,7 @@ * Set value or partiallyGenerated attribute to true * if the generated test(s) were significantly modified/altered by the developer. */ -@Target(ElementType.TYPE) +@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.SOURCE) public @interface CopilotGenerated { @@ -22,4 +22,7 @@ @AliasFor("value") boolean partiallyGenerated() default false; + + @AliasFor("model") + String model() default ""; } diff --git a/src/test/java/org/folio/des/controller/FolioTenantControllerTest.java b/src/test/java/org/folio/des/controller/FolioTenantControllerTest.java deleted file mode 100644 index f460fa0c..00000000 --- a/src/test/java/org/folio/des/controller/FolioTenantControllerTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.folio.des.controller; - -import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; -import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; - -import org.folio.des.support.BaseTest; -import org.junit.jupiter.api.Test; - -class FolioTenantControllerTest extends BaseTest { - - @Test - void shouldPostDefaultBulkEditConfigurationUponPostTenant() { - var expectedBody = "{" + - " \"module\" : \"BULKEDIT\"," + - " \"configName\" : \"general\"," + - " \"default\" : true," + - " \"enabled\" : true," + - " \"value\" : \"{\\\"jobExpirationPeriod\\\":\\\"14\\\"}\"" + - "}"; - - wireMockServer.verify( - postRequestedFor( - urlEqualTo("/configurations/entries")) - .withRequestBody(equalToJson(expectedBody))); - } -} diff --git a/src/test/java/org/folio/des/controller/JobDeletionIntervalsControllerTest.java b/src/test/java/org/folio/des/controller/JobDeletionIntervalsControllerTest.java new file mode 100644 index 00000000..4bcb6a56 --- /dev/null +++ b/src/test/java/org/folio/des/controller/JobDeletionIntervalsControllerTest.java @@ -0,0 +1,145 @@ +package org.folio.des.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.folio.des.domain.dto.ExportType; +import org.folio.des.domain.dto.delete_interval.JobDeletionInterval; +import org.folio.des.support.BaseTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import static org.folio.des.service.impl.JobDeletionIntervalServiceImpl.CREATED_BY_SYSTEM; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class JobDeletionIntervalsControllerTest extends BaseTest { + @Autowired + private MockMvc mockMvc; + + @Test + void getAllJobDeletionIntervalsReturnsCollection() throws Exception { + mockMvc.perform(get("/data-export-spring/job-deletion-intervals") + .contentType(APPLICATION_JSON) + .headers(defaultHeaders()) + .accept(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("$.totalRecords", is(6))) + .andExpect(jsonPath("$.jobDeletionIntervals", hasSize(6))) + .andExpect(jsonPath("$.jobDeletionIntervals[0].exportType", is("CIRCULATION_LOG"))) + .andExpect(jsonPath("$.jobDeletionIntervals[0].retentionDays", is(7))) + .andExpect(jsonPath("$.jobDeletionIntervals[0].metadata.createdByUserId", is(CREATED_BY_SYSTEM))) + .andExpect(jsonPath("$.jobDeletionIntervals[1].exportType", is("BURSAR_FEES_FINES"))) + .andExpect(jsonPath("$.jobDeletionIntervals[1].retentionDays", is(7))) + .andExpect(jsonPath("$.jobDeletionIntervals[1].metadata.createdByUserId", is(CREATED_BY_SYSTEM))) + .andExpect(jsonPath("$.jobDeletionIntervals[2].exportType", is("EDIFACT_ORDERS_EXPORT"))) + .andExpect(jsonPath("$.jobDeletionIntervals[2].retentionDays", is(730))) + .andExpect(jsonPath("$.jobDeletionIntervals[2].metadata.createdByUserId", is(CREATED_BY_SYSTEM))) + .andExpect(jsonPath("$.jobDeletionIntervals[3].exportType", is("CLAIMS"))) + .andExpect(jsonPath("$.jobDeletionIntervals[3].retentionDays", is(730))) + .andExpect(jsonPath("$.jobDeletionIntervals[3].metadata.createdByUserId", is(CREATED_BY_SYSTEM))) + .andExpect(jsonPath("$.jobDeletionIntervals[4].exportType", is("E_HOLDINGS"))) + .andExpect(jsonPath("$.jobDeletionIntervals[4].retentionDays", is(7))) + .andExpect(jsonPath("$.jobDeletionIntervals[4].metadata.createdByUserId", is(CREATED_BY_SYSTEM))) + .andExpect(jsonPath("$.jobDeletionIntervals[5].exportType", is("AUTH_HEADINGS_UPDATES"))) + .andExpect(jsonPath("$.jobDeletionIntervals[5].retentionDays", is(7))) + .andExpect(jsonPath("$.jobDeletionIntervals[5].metadata.createdByUserId", is(CREATED_BY_SYSTEM))); + } + + @Test + void testJobDeletionIntervalCRUD() throws Exception { + var interval = new JobDeletionInterval().exportType(ExportType.INVOICE_EXPORT).retentionDays(20); + + // 1. create interval + assertCreateInterval(interval); + + // 2. check if interval is created with all fields + interval = assertGetInterval(interval); + + // 3. update interval retention days + interval.setRetentionDays(500); + assertUpdateInterval(interval); + + // 4. check if interval is updated with new retention days + assertGetInterval(interval); + + // 5. delete interval + assertDeleteInterval(interval.getExportType()); + + // 6. check if interval is not exists anymore + mockMvc.perform(get("/data-export-spring/job-deletion-intervals/INVOICE_EXPORT") + .contentType(APPLICATION_JSON) + .headers(defaultHeaders()) + .accept(APPLICATION_JSON)) + .andExpect(status().isNotFound()); + + } + + private void assertCreateInterval(JobDeletionInterval interval) throws Exception { + mockMvc.perform(post("/data-export-spring/job-deletion-intervals") + .contentType(APPLICATION_JSON) + .headers(defaultHeaders()) + .content(getObjectMapper().writeValueAsString(interval))) + .andExpect(status().isCreated()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("$.exportType", is("INVOICE_EXPORT"))) + .andExpect(jsonPath("$.retentionDays", is(interval.getRetentionDays()))) + .andExpect(jsonPath("$.metadata.createdByUserId", is(USER_ID))) + .andExpect(jsonPath("$.metadata.createdDate", notNullValue())); + } + + private JobDeletionInterval assertGetInterval(JobDeletionInterval interval) throws Exception { + ResultActions result = mockMvc.perform(get("/data-export-spring/job-deletion-intervals/INVOICE_EXPORT") + .contentType(APPLICATION_JSON) + .headers(defaultHeaders()) + .accept(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("$.exportType", is("INVOICE_EXPORT"))) + .andExpect(jsonPath("$.retentionDays", is(interval.getRetentionDays()))) + .andExpect(jsonPath("$.metadata.createdByUserId", is(USER_ID))) + .andExpect(jsonPath("$.metadata.createdDate", notNullValue())); + + String content = result.andReturn().getResponse().getContentAsString(); + return getObjectMapper().readValue(content, JobDeletionInterval.class); + } + + private void assertUpdateInterval(JobDeletionInterval interval) throws Exception { + mockMvc.perform(put("/data-export-spring/job-deletion-intervals/INVOICE_EXPORT") + .contentType(APPLICATION_JSON) + .headers(defaultHeaders()) + .content(getObjectMapper().writeValueAsString(interval))) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("$.exportType", is("INVOICE_EXPORT"))) + .andExpect(jsonPath("$.retentionDays", is(interval.getRetentionDays()))) + .andExpect(jsonPath("$.metadata.updatedByUserId", is(USER_ID))) + .andExpect(jsonPath("$.metadata.updatedDate", notNullValue())); + } + + private void assertDeleteInterval(ExportType exportType) throws Exception { + mockMvc.perform(delete("/data-export-spring/job-deletion-intervals/{exportType}", exportType) + .contentType(APPLICATION_JSON) + .headers(defaultHeaders())) + .andExpect(status().isNoContent()); + } + + private ObjectMapper getObjectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + return objectMapper; + } +} diff --git a/src/test/java/org/folio/des/scheduling/quartz/OldJobDeleteSchedulerTest.java b/src/test/java/org/folio/des/scheduling/quartz/OldJobDeleteSchedulerTest.java index 20a1366d..a07a57c2 100644 --- a/src/test/java/org/folio/des/scheduling/quartz/OldJobDeleteSchedulerTest.java +++ b/src/test/java/org/folio/des/scheduling/quartz/OldJobDeleteSchedulerTest.java @@ -47,12 +47,12 @@ void testOldJobDeleteTrigger() throws SchedulerException { } @Test - void testDeleteOldJobDeleteScheduler() throws SchedulerException { + void testDeleteOldJobDeleteScheduler() { oldJobDeleteScheduler.scheduleOldJobDeletion(TENANT); oldJobDeleteScheduler.removeJobs(TENANT); await().pollDelay(1, TimeUnit.SECONDS) - .timeout(10, TimeUnit.SECONDS) + .timeout(15, TimeUnit.SECONDS) .untilAsserted(() -> { var jobKeys = scheduler.getJobKeys(GroupMatcher.jobGroupContains(EXPORT_DELETE_GROUP)); log.info("jobKeys: {}", jobKeys); diff --git a/src/test/java/org/folio/des/service/FolioTenantServiceTest.java b/src/test/java/org/folio/des/service/FolioTenantServiceTest.java index 0177d0b6..26a1861d 100644 --- a/src/test/java/org/folio/des/service/FolioTenantServiceTest.java +++ b/src/test/java/org/folio/des/service/FolioTenantServiceTest.java @@ -13,7 +13,6 @@ import org.folio.des.scheduling.quartz.OldJobDeleteScheduler; import org.folio.des.scheduling.quartz.ScheduledJobsRemover; import org.folio.des.service.bursarlegacy.BursarMigrationService; -import org.folio.des.service.config.BulkEditConfigService; import org.folio.spring.FolioExecutionContext; import org.folio.spring.service.PrepareSystemUserService; import org.folio.tenant.domain.dto.TenantAttributes; @@ -32,9 +31,6 @@ class FolioTenantServiceTest { @Mock KafkaService kafka; - @Mock - BulkEditConfigService bulkEditConfigService; - @Mock EdifactScheduledJobInitializer edifactScheduledJobInitializer; @@ -62,8 +58,6 @@ void shouldDoProcessAfterTenantUpdating() { doNothing().when(prepareSystemUserService) .setupSystemUser(); - doNothing().when(bulkEditConfigService) - .checkBulkEditConfiguration(); doNothing().when(edifactScheduledJobInitializer) .initAllScheduledJob(tenantAttributes); doNothing().when(kafka) @@ -80,7 +74,6 @@ void shouldDoProcessAfterTenantUpdating() { folioTenantService.afterTenantUpdate(tenantAttributes); verify(prepareSystemUserService).setupSystemUser(); - verify(bulkEditConfigService, times(1)).checkBulkEditConfiguration(); verify(edifactScheduledJobInitializer, times(1)).initAllScheduledJob(tenantAttributes); verify(bursarScheduledJobInitializer, times(1)).initAllScheduledJob(tenantAttributes); verify(oldJobDeleteScheduler, times(1)).scheduleOldJobDeletion(any()); diff --git a/src/test/java/org/folio/des/service/JobDeletionIntervalServiceTest.java b/src/test/java/org/folio/des/service/JobDeletionIntervalServiceTest.java new file mode 100644 index 00000000..4389d3fe --- /dev/null +++ b/src/test/java/org/folio/des/service/JobDeletionIntervalServiceTest.java @@ -0,0 +1,90 @@ +package org.folio.des.service; + +import org.folio.de.entity.JobDeletionIntervalEntity; +import org.folio.des.CopilotGenerated; +import org.folio.des.domain.dto.ExportType; +import org.folio.des.domain.dto.delete_interval.JobDeletionInterval; +import org.folio.des.repository.JobDeletionIntervalRepository; +import org.folio.des.service.impl.JobDeletionIntervalServiceImpl; +import org.folio.spring.FolioExecutionContext; +import org.folio.spring.exception.NotFoundException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@CopilotGenerated(model = "Claude Sonnet 3.5") +class JobDeletionIntervalServiceTest { + @Mock + private JobDeletionIntervalRepository repository; + + @Mock + private FolioExecutionContext context; + + @InjectMocks + private JobDeletionIntervalServiceImpl service; + + @Test + void getAllReturnsAllIntervals() { + var entity = new JobDeletionIntervalEntity(); + entity.setExportType(ExportType.BURSAR_FEES_FINES); + entity.setRetentionDays(10); + + when(repository.findAll()).thenReturn(List.of(entity)); + + var result = service.getAll(); + + assertEquals(1, result.getTotalRecords()); + assertEquals(ExportType.BURSAR_FEES_FINES, result.getJobDeletionIntervals().get(0).getExportType()); + assertEquals(10, result.getJobDeletionIntervals().get(0).getRetentionDays()); + } + + @Test + void getThrowsNotFoundExceptionWhenIntervalDoesNotExist() { + when(repository.findById(ExportType.BURSAR_FEES_FINES)).thenReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> service.get(ExportType.BURSAR_FEES_FINES)); + } + + @Test + void createThrowsConflictWhenIntervalAlreadyExists() { + var interval = new JobDeletionInterval(); + interval.setExportType(ExportType.BURSAR_FEES_FINES); + + when(repository.existsById(ExportType.BURSAR_FEES_FINES)).thenReturn(true); + + assertThrows(ResponseStatusException.class, () -> service.create(interval)); + } + + @Test + void updateThrowsNotFoundWhenIntervalDoesNotExist() { + var interval = new JobDeletionInterval(); + interval.setExportType(ExportType.BURSAR_FEES_FINES); + + when(repository.existsById(ExportType.BURSAR_FEES_FINES)).thenReturn(false); + + assertThrows(ResponseStatusException.class, () -> service.update(interval)); + } + + @Test + void deleteRemovesIntervalWhenExists() { + doNothing().when(repository).deleteById(ExportType.BURSAR_FEES_FINES); + + service.delete(ExportType.BURSAR_FEES_FINES); + + verify(repository, times(1)).deleteById(ExportType.BURSAR_FEES_FINES); + } +} diff --git a/src/test/java/org/folio/des/service/JobServiceTest.java b/src/test/java/org/folio/des/service/JobServiceTest.java index 0bdf5cf3..23a2d1c8 100644 --- a/src/test/java/org/folio/des/service/JobServiceTest.java +++ b/src/test/java/org/folio/des/service/JobServiceTest.java @@ -1,7 +1,10 @@ package org.folio.des.service; +import static org.folio.des.domain.dto.ExportType.BURSAR_FEES_FINES; +import static org.folio.des.domain.dto.ExportType.CLAIMS; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -15,10 +18,10 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.stream.Stream; import org.folio.de.entity.Job; import org.folio.de.entity.JobCommand; +import org.folio.des.CopilotGenerated; import org.folio.des.builder.job.JobCommandBuilderResolver; import org.folio.des.client.ConfigurationClient; import org.folio.des.client.ExportWorkerClient; @@ -29,8 +32,9 @@ import org.folio.des.domain.dto.ExportType; import org.folio.des.domain.dto.ExportTypeSpecificParameters; import org.folio.des.domain.dto.VendorEdiOrdersExportConfig; +import org.folio.des.domain.dto.delete_interval.JobDeletionInterval; +import org.folio.des.domain.dto.delete_interval.JobDeletionIntervalCollection; import org.folio.des.repository.JobDataExportRepository; -import org.folio.des.service.config.BulkEditConfigService; import org.folio.des.service.impl.JobServiceImpl; import org.folio.des.validator.ExportConfigValidatorResolver; import org.folio.spring.DefaultFolioExecutionContext; @@ -43,7 +47,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import com.fasterxml.jackson.databind.ObjectMapper; @@ -53,8 +56,6 @@ @ExtendWith(MockitoExtension.class) class JobServiceTest { - private static final int DEFAULT_JOB_EXPIRATION_PERIOD = 7; - private static final int DEFAULT_BULK_EDIT_JOB_EXPIRATION_PERIOD = 14; @InjectMocks private JobServiceImpl jobService; @@ -63,8 +64,6 @@ class JobServiceTest { @Mock private JobExecutionService jobExecutionService; @Mock - private BulkEditConfigService configService; - @Mock private ConfigurationClient client; @Mock private ExportWorkerClient exportWorkerClient; @@ -80,6 +79,8 @@ class JobServiceTest { private KafkaService kafka; @Mock private ObjectMapper objectMapper; + @Mock + private JobDeletionIntervalService deletionIntervalService; @BeforeEach void setup() { @@ -87,27 +88,6 @@ void setup() { ReflectionTestUtils.setField(jobService, "jobDownloadFileConnectionTimeout", 5000); } - @Test - void shouldCollectExpiredJobs() { - var expiredJobs = createExpiredJobs(); - - var jobsExpirationDate = getExpiredDate(DEFAULT_JOB_EXPIRATION_PERIOD); - var bulkEditExpirationDate = getExpiredDate(DEFAULT_BULK_EDIT_JOB_EXPIRATION_PERIOD); - - Mockito.when(repository.findByUpdatedDateBefore(any())).thenReturn(expiredJobs); - Mockito.when(configService.getBulkEditJobExpirationPeriod()).thenReturn(DEFAULT_BULK_EDIT_JOB_EXPIRATION_PERIOD); - - jobService.deleteOldJobs(); - - verify(repository).findByUpdatedDateBefore(jobsExpirationDate); - verify(repository).findByUpdatedDateBefore(bulkEditExpirationDate); - List actualDeletedJobs = expiredJobs.stream() - .filter(job -> job.getType() != ExportType.EDIFACT_ORDERS_EXPORT) - .filter(job -> job.getType() != ExportType.CLAIMS) - .toList(); - verify(jobExecutionService).deleteJobs(actualDeletedJobs); - } - @Test @SneakyThrows void testResendJob() { @@ -134,7 +114,7 @@ void testResendJob() { okapiHeaders.put(XOkapiHeaders.TENANT, List.of("diku")); var folioExecutionContext = new DefaultFolioExecutionContext(folioModuleMetadata, okapiHeaders); var jobExecutionService = new JobExecutionService(kafka, exportConfigValidatorResolver, jobCommandBuilderResolver, defaultModelConfigToExportConfigConverter, client, objectMapper); - var jobService = new JobServiceImpl(exportWorkerClient, jobExecutionService, repository, folioExecutionContext, null, null, client); + var internalJobService = new JobServiceImpl(exportWorkerClient, jobExecutionService, repository, folioExecutionContext, null, client, deletionIntervalService); var config = new ExportConfig(); config.setId(configId.toString()); org.folio.des.domain.dto.Job jobDto = new org.folio.des.domain.dto.Job(); @@ -150,28 +130,94 @@ void testResendJob() { return exportConfig; }); when(repository.findById(any())).thenReturn(java.util.Optional.of(job)); - assertThrows(NotFoundException.class, () -> jobService.resendExportedFile(configId)); + assertThrows(NotFoundException.class, () -> internalJobService.resendExportedFile(configId)); job.setFileNames(list); - jobService.resendExportedFile(jobDto.getId()); + internalJobService.resendExportedFile(jobDto.getId()); JobCommand command = jobExecutionService.prepareResendJobCommand(job); Assertions.assertEquals("TestFile.csv", command.getJobParameters().getParameters().get("FILE_NAME").getValue()); Assertions.assertNotNull(command.getJobParameters().getParameters().get("EDIFACT_ORDERS_EXPORT")); } - private List createExpiredJobs() { - var bulkEditExportTypes = new ArrayList(); + @Test + @CopilotGenerated(model = "Claude Sonnet 3.5") + void shouldDeleteExpiredJobsWhenIntervalsExist() { + var bursarExpiredJobs = createExpiredJobs(5, BURSAR_FEES_FINES, 7); + var claimsExpiredJobs = createExpiredJobs(10, CLAIMS, 14); + var date7Days = getExpiredDate(7); + var date14Days = getExpiredDate(14); + + var interval1 = new JobDeletionInterval() + .exportType(BURSAR_FEES_FINES) + .retentionDays(7); + var interval2 = new JobDeletionInterval() + .exportType(ExportType.CLAIMS) + .retentionDays(14); + var jobDeletionIntervals = List.of(interval1, interval2); + + var jobDeletionIntervalCollection = new JobDeletionIntervalCollection() + .jobDeletionIntervals(jobDeletionIntervals); + + when(deletionIntervalService.getAll()).thenReturn(jobDeletionIntervalCollection); + when(repository.findByTypeAndUpdatedDateBefore(BURSAR_FEES_FINES, date7Days)).thenReturn(bursarExpiredJobs); + when(repository.findByTypeAndUpdatedDateBefore(ExportType.CLAIMS, date14Days)).thenReturn(claimsExpiredJobs); + + var expiredJobs = new ArrayList(); + expiredJobs.addAll(bursarExpiredJobs); + expiredJobs.addAll(claimsExpiredJobs); + + jobService.deleteOldJobs(); + + verify(repository).findByTypeAndUpdatedDateBefore(BURSAR_FEES_FINES, date7Days); + verify(repository).findByTypeAndUpdatedDateBefore(ExportType.CLAIMS, date14Days); + verify(repository).deleteAllInBatch(expiredJobs); + verify(jobExecutionService).deleteJobs(expiredJobs); + } + + @Test + @CopilotGenerated(model = "Claude Sonnet 3.5") + void shouldNotDeleteJobsWhenTheyAreNotExpired() { + var date200Days = getExpiredDate(200); + + var interval1 = new JobDeletionInterval() + .exportType(BURSAR_FEES_FINES) + .retentionDays(200); // 200 days retention period + var jobDeletionIntervals = List.of(interval1); + + var jobDeletionIntervalCollection = new JobDeletionIntervalCollection() + .jobDeletionIntervals(jobDeletionIntervals); + + when(deletionIntervalService.getAll()).thenReturn(jobDeletionIntervalCollection); + when(repository.findByTypeAndUpdatedDateBefore(BURSAR_FEES_FINES, date200Days)).thenReturn(List.of()); + + jobService.deleteOldJobs(); + + verify(repository).findByTypeAndUpdatedDateBefore(BURSAR_FEES_FINES, date200Days); + verify(repository, never()).deleteAllInBatch(any()); + verify(jobExecutionService, never()).deleteJobs(any()); + } + + @Test + @CopilotGenerated(model = "Claude Sonnet 3.5") + void shouldSkipDeletionWhenNoIntervalsExist() { + var emptyCollection = new JobDeletionIntervalCollection() + .jobDeletionIntervals(List.of()); + + when(deletionIntervalService.getAll()).thenReturn(emptyCollection); + + jobService.deleteOldJobs(); + + verify(repository, never()).findByTypeAndUpdatedDateBefore(any(), any()); + verify(repository, never()).deleteAllInBatch(any()); + verify(jobExecutionService, never()).deleteJobs(any()); + } + + private List createExpiredJobs(int count, ExportType exportType, int expirationPeriod) { var exportTypes = new ArrayList(); - Stream.of(ExportType.values()) - .forEach(exportType -> { - if (isBulkEdit(exportType)) { - bulkEditExportTypes.add(createJob(exportType, DEFAULT_BULK_EDIT_JOB_EXPIRATION_PERIOD)); - } else { - exportTypes.add(createJob(exportType, DEFAULT_JOB_EXPIRATION_PERIOD)); - } - }); + for (int i = 0; i < count; i++) { + exportTypes.add(createJob(exportType, expirationPeriod)); + } - exportTypes.addAll(bulkEditExportTypes); return exportTypes; } @@ -186,8 +232,4 @@ private Job createJob(ExportType exportType, int expirationPeriod) { private Date getExpiredDate(int days) { return Date.from(LocalDate.now().minusDays(days).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); } - - private boolean isBulkEdit(ExportType exportType) { - return exportType.getValue().startsWith("BULK_EDIT"); - } } diff --git a/src/test/java/org/folio/des/support/BaseTest.java b/src/test/java/org/folio/des/support/BaseTest.java index 1236d977..942f13b7 100644 --- a/src/test/java/org/folio/des/support/BaseTest.java +++ b/src/test/java/org/folio/des/support/BaseTest.java @@ -57,6 +57,7 @@ public abstract class BaseTest { public static final int WIRE_MOCK_PORT = TestSocketUtils.findAvailableTcpPort(); protected static final String TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkaWt1X2FkbWluIiwidXNlcl9pZCI6IjFkM2I1OGNiLTA3YjUtNWZjZC04YTJhLTNjZTA2YTBlYjkwZiIsImlhdCI6MTYxNjQyMDM5MywidGVuYW50IjoiZGlrdSJ9.2nvEYQBbJP1PewEgxixBWLHSX_eELiBEBpjufWiJZRs"; public static final String TENANT = "diku"; + public static final String USER_ID = "625dd2b6-b6f2-4f77-90fe-68954b26ee3c"; public static WireMockServer wireMockServer; public static PostgreSQLContainer postgreDBContainer = new PostgreSQLContainer<>("postgres:16-alpine"); @@ -120,6 +121,7 @@ public static HttpHeaders defaultHeaders() { httpHeaders.put(XOkapiHeaders.TENANT, List.of(TENANT)); httpHeaders.add(XOkapiHeaders.URL, wireMockServer.baseUrl()); httpHeaders.add(XOkapiHeaders.TOKEN, TOKEN); + httpHeaders.add(XOkapiHeaders.USER_ID, USER_ID); return httpHeaders; }