diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java index 85fbef4b7..9a2299d9d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java @@ -50,6 +50,8 @@ public class FineractProperties { private FineractReportProperties report; + private FineractJobProperties job; + @Getter @Setter public static class FineractTenantProperties { @@ -225,4 +227,11 @@ public class FineractProperties { private String bucketName; private Boolean enabled; } + + @Getter + @Setter + public static class FineractJobProperties { + + private int stuckRetryThreshold; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java index 016a92b0f..0b4753059 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java @@ -22,10 +22,12 @@ import static org.springframework.batch.core.BatchStatus.COMPLETED; import static org.springframework.batch.core.BatchStatus.FAILED; import static org.springframework.batch.core.BatchStatus.STARTED; import static org.springframework.batch.core.BatchStatus.STARTING; +import static org.springframework.batch.core.BatchStatus.UNKNOWN; import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Component; @@ -34,68 +36,148 @@ import org.springframework.stereotype.Component; public class JobExecutionRepository { private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + private final FineractProperties fineractProperties; public List getStuckJobNames(NamedParameterJdbcTemplate jdbcTemplate) { - return jdbcTemplate.queryForList( - "SELECT bji.JOB_NAME as STUCK_JOB_NAME FROM BATCH_JOB_INSTANCE bji " - + "INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID " - + "WHERE bje.STATUS IN (:statuses) AND bje.JOB_INSTANCE_ID NOT IN (" - + "SELECT bje.JOB_INSTANCE_ID FROM BATCH_JOB_INSTANCE bji " - + "INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID " - + "WHERE bje.STATUS = :completedStatus)", - Map.of("statuses", List.of(STARTED.name(), FAILED.name()), "completedStatus", COMPLETED.name()), String.class); + int threshold = fineractProperties.getJob().getStuckRetryThreshold(); + return jdbcTemplate.queryForList(""" + SELECT DISTINCT(bji.JOB_NAME) as STUCK_JOB_NAME + FROM BATCH_JOB_INSTANCE bji + INNER JOIN BATCH_JOB_EXECUTION bje + ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID + WHERE + bje.STATUS IN (:statuses) + AND + bje.JOB_INSTANCE_ID NOT IN ( + SELECT bje.JOB_INSTANCE_ID + FROM BATCH_JOB_INSTANCE bji + INNER JOIN BATCH_JOB_EXECUTION bje + ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID + WHERE bje.STATUS IN (:completedStatuses) + ) + GROUP BY BJI.JOB_INSTANCE_ID + HAVING COUNT(BJI.JOB_INSTANCE_ID) <= :threshold + """, Map.of("statuses", List.of(STARTED.name()), "completedStatuses", + List.of(COMPLETED.name(), FAILED.name(), UNKNOWN.name()), "threshold", threshold), String.class); } public Long getStuckJobCountByJobName(String jobName) { - return namedParameterJdbcTemplate.queryForObject( - "SELECT COUNT(*) as STUCK_JOB_COUNT FROM BATCH_JOB_INSTANCE bji " - + "INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID " - + "WHERE bje.STATUS IN (:statuses) AND bji.JOB_NAME = :jobName AND bje.JOB_INSTANCE_ID NOT IN (" - + "SELECT bje.JOB_INSTANCE_ID FROM BATCH_JOB_INSTANCE bji " - + "INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID " - + "WHERE bje.STATUS = :completedStatus AND bji.JOB_NAME = :jobName)", - Map.of("statuses", List.of(STARTED.name(), FAILED.name()), "jobName", jobName, "completedStatus", COMPLETED.name()), - Long.class); + int threshold = fineractProperties.getJob().getStuckRetryThreshold(); + return namedParameterJdbcTemplate.queryForObject(""" + SELECT COUNT(DISTINCT bji.JOB_NAME) as STUCK_JOB_COUNT + FROM BATCH_JOB_INSTANCE bji + INNER JOIN BATCH_JOB_EXECUTION bje + ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID + WHERE + bje.STATUS IN (:statuses) + AND + bji.JOB_NAME = :jobName + AND + bje.JOB_INSTANCE_ID NOT IN ( + SELECT bje.JOB_INSTANCE_ID + FROM BATCH_JOB_INSTANCE bji + INNER JOIN BATCH_JOB_EXECUTION bje + ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID + WHERE + bje.STATUS IN (:completedStatuses) + AND + bji.JOB_NAME = :jobName + ) + GROUP BY BJI.JOB_INSTANCE_ID + HAVING COUNT(BJI.JOB_INSTANCE_ID) <= :threshold + """, Map.of("statuses", List.of(STARTED.name()), "jobName", jobName, "completedStatuses", + List.of(COMPLETED.name(), FAILED.name(), UNKNOWN.name()), "threshold", threshold), Long.class); } public List getStuckJobIdsByJobName(String jobName) { - return namedParameterJdbcTemplate.queryForList( - "SELECT bje.JOB_EXECUTION_ID FROM BATCH_JOB_INSTANCE bji " - + "INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID " - + "WHERE bje.STATUS IN (:statuses) AND bji.JOB_NAME = :jobName AND bje.JOB_INSTANCE_ID NOT IN (" - + "SELECT bje.JOB_INSTANCE_ID FROM BATCH_JOB_INSTANCE bji " - + "INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID " - + "WHERE bje.STATUS = :completedStatus AND bji.JOB_NAME = :jobName)", - Map.of("statuses", List.of(STARTED.name(), FAILED.name()), "jobName", jobName, "completedStatus", COMPLETED.name()), - Long.class); + int threshold = fineractProperties.getJob().getStuckRetryThreshold(); + return namedParameterJdbcTemplate.queryForList(""" + SELECT bje.JOB_EXECUTION_ID + FROM BATCH_JOB_INSTANCE bji + INNER JOIN BATCH_JOB_EXECUTION bje + ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID + WHERE + bje.STATUS IN (:statuses) + AND + bji.JOB_NAME = :jobName + AND + bje.JOB_INSTANCE_ID NOT IN ( + SELECT bje.JOB_INSTANCE_ID + FROM BATCH_JOB_INSTANCE bji + INNER JOIN BATCH_JOB_EXECUTION bje + ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID + WHERE + bje.STATUS IN (:completedStatuses) + AND + bji.JOB_NAME = :jobName + ) + GROUP BY BJI.JOB_INSTANCE_ID, BJE.JOB_EXECUTION_ID + HAVING COUNT(BJI.JOB_INSTANCE_ID) <= :threshold + """, Map.of("statuses", List.of(STARTED.name()), "jobName", jobName, "completedStatuses", + List.of(COMPLETED.name(), FAILED.name(), UNKNOWN.name()), "threshold", threshold), Long.class); } public Long getNotCompletedPartitionsCount(Long jobExecutionId, String partitionerStepName) { - return namedParameterJdbcTemplate.queryForObject( - "SELECT COUNT(bse.STEP_EXECUTION_ID) FROM BATCH_STEP_EXECUTION bse " - + "WHERE bse.JOB_EXECUTION_ID = :jobExecutionId AND bse.STEP_NAME <> :stepName AND bse.status <> :status", - Map.of("jobExecutionId", jobExecutionId, "stepName", partitionerStepName, "status", COMPLETED.name()), Long.class); + return namedParameterJdbcTemplate.queryForObject(""" + SELECT COUNT(bse.STEP_EXECUTION_ID) + FROM BATCH_STEP_EXECUTION bse + WHERE + bse.JOB_EXECUTION_ID = :jobExecutionId + AND + bse.STEP_NAME <> :stepName + AND + bse.status <> :status + """, Map.of("jobExecutionId", jobExecutionId, "stepName", partitionerStepName, "status", COMPLETED.name()), Long.class); } public void updateJobStatusToFailed(Long stuckJobId, String partitionerStepName) { - namedParameterJdbcTemplate.update( - "UPDATE BATCH_STEP_EXECUTION SET STATUS = :status WHERE JOB_EXECUTION_ID = :jobExecutionId AND STEP_NAME = :stepName", - Map.of("status", FAILED.name(), "jobExecutionId", stuckJobId, "stepName", partitionerStepName)); - namedParameterJdbcTemplate.update( - "UPDATE BATCH_JOB_EXECUTION SET STATUS = :status, START_TIME = null, END_TIME = null WHERE JOB_EXECUTION_ID = :jobExecutionId", - Map.of("status", FAILED.name(), "jobExecutionId", stuckJobId)); + namedParameterJdbcTemplate.update(""" + UPDATE BATCH_STEP_EXECUTION + SET STATUS = :status + WHERE + JOB_EXECUTION_ID = :jobExecutionId + AND + STEP_NAME = :stepName + """, Map.of("status", FAILED.name(), "jobExecutionId", stuckJobId, "stepName", partitionerStepName)); + namedParameterJdbcTemplate.update(""" + UPDATE BATCH_JOB_EXECUTION + SET + STATUS = :status, + START_TIME = null, + END_TIME = null + WHERE + JOB_EXECUTION_ID = :jobExecutionId + """, Map.of("status", FAILED.name(), "jobExecutionId", stuckJobId)); } public List getRunningJobsByExecutionParameter(String jobName, String parameterKeyName, String parameterValue) { - return namedParameterJdbcTemplate.queryForList("SELECT bje.JOB_EXECUTION_ID FROM BATCH_JOB_INSTANCE bji " - + "INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID " - + "INNER JOIN BATCH_JOB_EXECUTION_PARAMS bjep ON bje.JOB_EXECUTION_ID = bjep.JOB_EXECUTION_ID " - + "WHERE bje.STATUS IN (:statuses) AND bji.JOB_NAME = :jobName AND bjep.KEY_NAME = :parameterKeyName AND bjep.STRING_VAL = :parameterValue AND bje.JOB_INSTANCE_ID NOT IN (" - + "SELECT bje.JOB_INSTANCE_ID FROM BATCH_JOB_INSTANCE bji " - + "INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID " - + "WHERE bje.STATUS = :completedStatus AND bji.JOB_NAME = :jobName)", - Map.of("statuses", List.of(STARTED.name(), STARTING.name()), "jobName", jobName, "completedStatus", COMPLETED.name(), - "parameterKeyName", parameterKeyName, "parameterValue", parameterValue), - Long.class); + return namedParameterJdbcTemplate.queryForList(""" + SELECT bje.JOB_EXECUTION_ID + FROM BATCH_JOB_INSTANCE bji + INNER JOIN BATCH_JOB_EXECUTION bje + ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID + INNER JOIN BATCH_JOB_EXECUTION_PARAMS bjep + ON bje.JOB_EXECUTION_ID = bjep.JOB_EXECUTION_ID + WHERE + bje.STATUS IN (:statuses) + AND + bji.JOB_NAME = :jobName + AND + bjep.KEY_NAME = :parameterKeyName + AND + bjep.STRING_VAL = :parameterValue + AND + bje.JOB_INSTANCE_ID NOT IN ( + SELECT bje.JOB_INSTANCE_ID + FROM BATCH_JOB_INSTANCE bji + INNER JOIN BATCH_JOB_EXECUTION bje + ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID + WHERE + bje.STATUS = :completedStatus + AND + bji.JOB_NAME = :jobName + ) + """, Map.of("statuses", List.of(STARTED.name(), STARTING.name()), "jobName", jobName, "completedStatus", COMPLETED.name(), + "parameterKeyName", parameterKeyName, "parameterValue", parameterValue), Long.class); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobExecutorService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobExecutorService.java index 70e365308..8fe2c3346 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobExecutorService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobExecutorService.java @@ -20,5 +20,5 @@ package org.apache.fineract.infrastructure.jobs.service; public interface StuckJobExecutorService { - void executeStuckJob(String jobName, Long jobId); + void resumeStuckJob(String jobName); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobExecutorServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobExecutorServiceImpl.java index d69aed9f5..5f46c88f7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobExecutorServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobExecutorServiceImpl.java @@ -25,7 +25,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.jobs.data.partitionedjobs.PartitionedJob; import org.apache.fineract.infrastructure.jobs.domain.JobExecutionRepository; -import org.jetbrains.annotations.NotNull; import org.springframework.batch.core.launch.JobOperator; import org.springframework.stereotype.Service; import org.springframework.transaction.TransactionStatus; @@ -40,20 +39,33 @@ public class StuckJobExecutorServiceImpl implements StuckJobExecutorService { private final JobExecutionRepository jobExecutionRepository; private final TransactionTemplate transactionTemplate; private final JobOperator jobOperator; - private final JobRegisterService jobRegisterService; @Override - public void executeStuckJob(String jobName, Long jobId) { + public void resumeStuckJob(String jobName) { + List stuckJobIds = getStuckJobIds(jobName); if (isPartitionedJob(jobName) && areThereStuckJobs(jobName)) { - // Restarting stuck Partitioned jobs - List stuckJobIds = getStuckJobIds(jobName); - stuckJobIds.forEach(stuckJobId -> handleStuckJob(stuckJobId, getPartitionerStepName(jobName))); + restartPartitionedJobs(jobName, stuckJobIds); } else { - // Executing stuck Tasklet jobs - jobRegisterService.executeJobWithParameters(jobId, null); + restartTaskletJobs(stuckJobIds); } } + private void restartTaskletJobs(List stuckJobIds) { + stuckJobIds.forEach(this::handleStuckTaskletJob); + } + + private void handleStuckTaskletJob(Long stuckJobId) { + try { + jobOperator.restart(stuckJobId); + } catch (Exception e) { + throw new RuntimeException("Exception while handling a stuck job", e); + } + } + + private void restartPartitionedJobs(String jobName, List stuckJobIds) { + stuckJobIds.forEach(stuckJobId -> handleStuckPartitionedJob(stuckJobId, getPartitionerStepName(jobName))); + } + private boolean isPartitionedJob(String jobName) { return PartitionedJob.existsByJobName(jobName); } @@ -71,14 +83,14 @@ public class StuckJobExecutorServiceImpl implements StuckJobExecutorService { return jobExecutionRepository.getStuckJobIdsByJobName(jobName); } - private void handleStuckJob(Long stuckJobId, String partitionerStepName) { + private void handleStuckPartitionedJob(Long stuckJobId, String partitionerStepName) { try { waitUntilAllPartitionsFinished(stuckJobId, partitionerStepName); transactionTemplate.setPropagationBehavior(PROPAGATION_REQUIRES_NEW); transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override - protected void doInTransactionWithoutResult(@NotNull TransactionStatus status) { + protected void doInTransactionWithoutResult(TransactionStatus status) { jobExecutionRepository.updateJobStatusToFailed(stuckJobId, partitionerStepName); } }); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobListener.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobListener.java index 948f7be59..353c5c914 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobListener.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobListener.java @@ -21,8 +21,6 @@ package org.apache.fineract.infrastructure.jobs.service; import java.time.LocalDate; import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService; @@ -31,7 +29,6 @@ import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.event.external.service.JdbcTemplateFactory; import org.apache.fineract.infrastructure.jobs.domain.JobExecutionRepository; -import org.apache.fineract.infrastructure.jobs.service.jobname.JobNameData; import org.apache.fineract.infrastructure.jobs.service.jobname.JobNameProvider; import org.apache.fineract.infrastructure.security.service.TenantDetailsService; import org.springframework.batch.core.configuration.JobRegistry; @@ -67,18 +64,9 @@ public class StuckJobListener implements ApplicationListener businessDates = businessDateReadPlatformService.getBusinessDates(); ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); ThreadLocalContextUtil.setBusinessDates(businessDates); - stuckJobNames.forEach(stuckJobName -> { - String sql = "select id from job where name = :jobName"; - Optional jobNameDataOptional = jobNameProvider.provide().stream() - .filter(jobNameData -> stuckJobName.equals(jobNameData.getEnumStyleName())).findFirst(); - JobNameData jobNameData = jobNameDataOptional - .orElseThrow(() -> new IllegalArgumentException("Job not found by name: " + stuckJobName)); - Long stuckJobId = namedParameterJdbcTemplate.queryForObject(sql, - Map.of("jobName", jobNameData.getHumanReadableName()), Long.class); - stuckJobExecutorService.executeStuckJob(stuckJobName, stuckJobId); - }); + stuckJobNames.forEach(stuckJobExecutorService::resumeStuckJob); } catch (Exception e) { - throw new RuntimeException(e); + throw new RuntimeException("Error while trying to restart stuck jobs", e); } finally { ThreadLocalContextUtil.reset(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/generateloanlossprovisioning/GenerateLoanlossProvisioningTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/generateloanlossprovisioning/GenerateLoanlossProvisioningTasklet.java index 2cb450608..a444c4542 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/generateloanlossprovisioning/GenerateLoanlossProvisioningTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/generateloanlossprovisioning/GenerateLoanlossProvisioningTasklet.java @@ -22,6 +22,7 @@ import java.time.LocalDate; import java.util.Collection; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.apache.fineract.accounting.provisioning.exception.ProvisioningEntryAlreadyCreatedException; import org.apache.fineract.accounting.provisioning.service.ProvisioningEntriesWritePlatformService; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -31,8 +32,6 @@ import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.repeat.RepeatStatus; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.orm.jpa.JpaSystemException; @Slf4j @RequiredArgsConstructor @@ -48,14 +47,13 @@ public class GenerateLoanlossProvisioningTasklet implements Tasklet { try { Collection criteriaCollection = provisioningCriteriaReadPlatformService .retrieveAllProvisioningCriterias(); - if (criteriaCollection == null || criteriaCollection.size() == 0) { - throw new NoProvisioningCriteriaDefinitionFoundException(); + if (CollectionUtils.isNotEmpty(criteriaCollection)) { + provisioningEntriesWritePlatformService.createProvisioningEntry(currentDate, addJournalEntries); } - provisioningEntriesWritePlatformService.createProvisioningEntry(currentDate, addJournalEntries); - } catch (ProvisioningEntryAlreadyCreatedException peace) { - log.error("Provisioning Entry already created", peace); - } catch (final JpaSystemException | DataIntegrityViolationException dve) { - log.error("Problem occurred in generateLoanLossProvisioningAmount function", dve); + } catch (ProvisioningEntryAlreadyCreatedException e) { + log.error("Provisioning entry already created", e); + } catch (Exception e) { + log.error("Problem occurred when generating provisioning entries", e); } return RepeatStatus.FINISHED; } diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index c7f8078ab..170ff1cee 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -43,6 +43,8 @@ fineract.mode.batch-manager-enabled=${FINERACT_MODE_BATCH_MANAGER_ENABLED:true} fineract.correlation.enabled=${FINERACT_LOGGING_HTTP_CORRELATION_ID_ENABLED:false} fineract.correlation.header-name=${FINERACT_LOGGING_HTTP_CORRELATION_ID_HEADER_NAME:X-Correlation-ID} +fineract.job.stuck-retry-threshold=${FINERACT_JOB_STUCK_RETRY_THRESHOLD:5} + fineract.partitioned-job.partitioned-job-properties[0].job-name=LOAN_COB fineract.partitioned-job.partitioned-job-properties[0].chunk-size=${LOAN_COB_CHUNK_SIZE:100} fineract.partitioned-job.partitioned-job-properties[0].partition-size=${LOAN_COB_PARTITION_SIZE:100} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/jobs/generateloanlossprovisioning/GenerateLoanlossProvisioningTaskletTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/jobs/generateloanlossprovisioning/GenerateLoanlossProvisioningTaskletTest.java new file mode 100644 index 000000000..2a5462c5d --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/jobs/generateloanlossprovisioning/GenerateLoanlossProvisioningTaskletTest.java @@ -0,0 +1,137 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.jobs.generateloanlossprovisioning; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.springframework.batch.repeat.RepeatStatus.FINISHED; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.fineract.accounting.provisioning.exception.ProvisioningEntryAlreadyCreatedException; +import org.apache.fineract.accounting.provisioning.service.ProvisioningEntriesWritePlatformService; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.organisation.provisioning.data.ProvisioningCriteriaData; +import org.apache.fineract.organisation.provisioning.service.ProvisioningCriteriaReadPlatformService; +import org.junit.jupiter.api.BeforeEach; +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.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.repeat.RepeatStatus; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class GenerateLoanlossProvisioningTaskletTest { + + public static final LocalDate BUSINESS_DATE = LocalDate.of(2022, 6, 12); + + @Mock + private StepContribution stepContribution; + + @Mock + private ChunkContext chunkContext; + + @Mock + private ProvisioningCriteriaReadPlatformService provisioningCriteriaReadPlatformService; + @Mock + private ProvisioningEntriesWritePlatformService provisioningEntriesWritePlatformService; + + @InjectMocks + private GenerateLoanlossProvisioningTasklet underTest; + + @BeforeEach + public void setUp() { + ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, BUSINESS_DATE))); + } + + @Test + public void testExecuteShouldCreateProvisioningEntry() throws Exception { + // given + Collection provisioningCriterias = List.of(new ProvisioningCriteriaData()); + given(provisioningCriteriaReadPlatformService.retrieveAllProvisioningCriterias()).willReturn(provisioningCriterias); + // when + RepeatStatus result = underTest.execute(stepContribution, chunkContext); + // then + verify(provisioningEntriesWritePlatformService).createProvisioningEntry(BUSINESS_DATE, true); + assertThat(result).isEqualTo(FINISHED); + } + + @Test + public void testExecuteShouldNotCreateProvisioningEntryWhenNoProvisioningCriteriasArePresent() throws Exception { + // given + Collection provisioningCriterias = List.of(); + given(provisioningCriteriaReadPlatformService.retrieveAllProvisioningCriterias()).willReturn(provisioningCriterias); + // when + RepeatStatus result = underTest.execute(stepContribution, chunkContext); + // then + verifyNoInteractions(provisioningEntriesWritePlatformService); + assertThat(result).isEqualTo(FINISHED); + } + + @Test + public void testExecuteShouldNotCreateProvisioningEntryWhenNullProvisioningCriteriasArePresent() throws Exception { + // given + given(provisioningCriteriaReadPlatformService.retrieveAllProvisioningCriterias()).willReturn(null); + // when + RepeatStatus result = underTest.execute(stepContribution, chunkContext); + // then + verifyNoInteractions(provisioningEntriesWritePlatformService); + assertThat(result).isEqualTo(FINISHED); + } + + @Test + public void testExecuteShouldNotFailWhenProvisioningEntryIsAlreadyCreated() throws Exception { + // given + Collection provisioningCriterias = List.of(new ProvisioningCriteriaData()); + given(provisioningCriteriaReadPlatformService.retrieveAllProvisioningCriterias()).willReturn(provisioningCriterias); + given(provisioningEntriesWritePlatformService.createProvisioningEntry(BUSINESS_DATE, true)) + .willThrow(new ProvisioningEntryAlreadyCreatedException(1L, BUSINESS_DATE)); + // when + RepeatStatus result = underTest.execute(stepContribution, chunkContext); + // then + verify(provisioningEntriesWritePlatformService).createProvisioningEntry(BUSINESS_DATE, true); + assertThat(result).isEqualTo(FINISHED); + } + + @Test + public void testExecuteShouldNotFailWhenExceptionIsThrownInProvisioningEntryCreation() throws Exception { + // given + Collection provisioningCriterias = List.of(new ProvisioningCriteriaData()); + given(provisioningCriteriaReadPlatformService.retrieveAllProvisioningCriterias()).willReturn(provisioningCriterias); + given(provisioningEntriesWritePlatformService.createProvisioningEntry(BUSINESS_DATE, true)).willThrow(new RuntimeException("Test")); + // when + RepeatStatus result = underTest.execute(stepContribution, chunkContext); + // then + verify(provisioningEntriesWritePlatformService).createProvisioningEntry(BUSINESS_DATE, true); + assertThat(result).isEqualTo(FINISHED); + } + +}