From ca51084e863395356396b4dac9d1d89ba7116078 Mon Sep 17 00:00:00 2001 From: taskain7 Date: Thu, 24 Nov 2022 03:21:59 +0100 Subject: [PATCH] [FINERACT-1678] Introducing `Last COB Business Date` for loans --- .../common/CustomJobParameterResolver.java | 69 +++++++++++++++ .../CustomJobParameterNotFoundException.java | 39 +++++++++ .../cob/loan/AbstractLoanItemProcessor.java | 15 +++- .../cob/loan/FetchAndLockLoanTasklet.java | 9 +- .../cob/loan/InlineCOBLoanItemProcessor.java | 1 + .../fineract/cob/loan/LoanCOBConstant.java | 1 + .../cob/loan/LoanCOBManagerConfiguration.java | 26 +++++- .../fineract/cob/loan/LoanItemProcessor.java | 1 + ...olveLoanCOBCustomJobParametersTasklet.java | 41 +++++++++ ...RetrieveAllNonClosedLoanIdServiceImpl.java | 5 +- .../cob/loan/RetrieveLoanIdService.java | 3 +- .../InlineLoanCOBExecutorServiceImpl.java | 13 ++- .../jobs/api/SchedulerJobApiResource.java | 6 +- .../api/SchedulerJobApiResourceSwagger.java | 13 +++ .../jobs/data/JobParameterDTO.java | 32 +++++++ .../jobs/service/JobParameterDataParser.java | 59 +++++++++++++ .../jobs/service/JobRegisterService.java | 2 +- .../jobs/service/JobRegisterServiceImpl.java | 26 ++++-- .../jobs/service/JobStarter.java | 25 +++++- .../ExecuteAllDirtyJobsTasklet.java | 2 +- .../AbstractJobParameterProvider.java | 29 +++++++ .../JobParameterProvider.java | 31 +++++++ .../LoanCOBJobParameterProvider.java | 85 +++++++++++++++++++ .../loanaccount/api/LoansApiResource.java | 9 +- .../api/LoansApiResourceSwagger.java | 1 + .../loanaccount/data/LoanAccountData.java | 9 +- .../portfolio/loanaccount/domain/Loan.java | 10 +++ .../loanaccount/domain/LoanRepository.java | 6 ++ .../service/LoanReadPlatformServiceImpl.java | 6 +- .../db/changelog/tenant/changelog-tenant.xml | 2 +- ..._last_cob_business_date_column_to_loan.xml | 30 +++++++ .../loan/FetchAndLockLoanStepDefinitions.java | 22 +++-- .../LoanItemProcessorStepDefinitions.java | 9 +- .../DelinquencyBucketsIntegrationTest.java | 3 +- .../SchedulerJobsTestResults.java | 61 ++++++++++++- .../common/SchedulerJobHelper.java | 11 +++ 36 files changed, 664 insertions(+), 48 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/common/CustomJobParameterResolver.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/CustomJobParameterNotFoundException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/loan/ResolveLoanCOBCustomJobParametersTasklet.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobParameterDTO.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobParameterDataParser.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/AbstractJobParameterProvider.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/JobParameterProvider.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/LoanCOBJobParameterProvider.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0074_add_last_cob_business_date_column_to_loan.xml diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/common/CustomJobParameterResolver.java b/fineract-provider/src/main/java/org/apache/fineract/cob/common/CustomJobParameterResolver.java new file mode 100644 index 000000000..9e156856c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/common/CustomJobParameterResolver.java @@ -0,0 +1,69 @@ +/** + * 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.cob.common; + +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import java.util.HashSet; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.cob.exceptions.CustomJobParameterNotFoundException; +import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper; +import org.apache.fineract.infrastructure.jobs.data.JobParameterDTO; +import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameter; +import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameterRepository; +import org.apache.fineract.infrastructure.springbatch.SpringBatchJobConstants; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class CustomJobParameterResolver implements InitializingBean { + + private final GoogleGsonSerializerHelper gsonFactory; + private final CustomJobParameterRepository customJobParameterRepository; + + protected Gson gson; + + @Override + public void afterPropertiesSet() throws Exception { + this.gson = gsonFactory.createSimpleGson(); + } + + public void resolve(StepContribution contribution, ChunkContext chunkContext, String customJobParameterKey, + String parameterNameInExecutionContext) { + Set jobParameterDTOList = getCustomJobParameterSet(chunkContext); + JobParameterDTO businessDateParameter = jobParameterDTOList.stream() + .filter(jobParameterDTO -> customJobParameterKey.equals(jobParameterDTO.getParameterName())) // + .findFirst().orElseThrow(() -> new CustomJobParameterNotFoundException(customJobParameterKey)); + contribution.getStepExecution().getExecutionContext().put(parameterNameInExecutionContext, + businessDateParameter.getParameterValue()); + } + + private Set getCustomJobParameterSet(ChunkContext chunkContext) { + Long customJobParameterId = (Long) chunkContext.getStepContext().getJobParameters() + .get(SpringBatchJobConstants.CUSTOM_JOB_PARAMETER_ID_KEY); + CustomJobParameter customJobParameter = customJobParameterRepository.findById(customJobParameterId) + .orElseThrow(() -> new CustomJobParameterNotFoundException(customJobParameterId)); + String parameterJson = customJobParameter.getParameterJson(); + return gson.fromJson(parameterJson, new TypeToken>() {}.getType()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/CustomJobParameterNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/CustomJobParameterNotFoundException.java new file mode 100644 index 000000000..812f0f33d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/CustomJobParameterNotFoundException.java @@ -0,0 +1,39 @@ +/** + * 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.cob.exceptions; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +public class CustomJobParameterNotFoundException extends AbstractPlatformResourceNotFoundException { + + public CustomJobParameterNotFoundException(String globalisationMessageCode, String defaultUserMessage, + Object... defaultUserMessageArgs) { + super(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs); + } + + public CustomJobParameterNotFoundException(Long customJobParameterId) { + super("error.msg.customjobparameter.not.found", "Custom job parameter not found with the given id: " + customJobParameterId, + customJobParameterId); + } + + public CustomJobParameterNotFoundException(String customJobParameterName) { + super("error.msg.customjobparameter.not.found", "Custom job parameter not found with the given name: " + customJobParameterName, + customJobParameterName); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemProcessor.java index 9c936f991..65ddcfff0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AbstractLoanItemProcessor.java @@ -18,6 +18,9 @@ */ package org.apache.fineract.cob.loan; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Objects; import java.util.TreeMap; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; @@ -40,12 +43,15 @@ public abstract class AbstractLoanItemProcessor implements ItemProcessor businessStepMap = (TreeMap) executionContext.get(LoanCOBConstant.BUSINESS_STEP_MAP); - return cobBusinessStepService.run(businessStepMap, item); + Loan alreadyProcessedLoan = cobBusinessStepService.run(businessStepMap, item); + alreadyProcessedLoan.setLastClosedBusinessDate(businessDate); + return alreadyProcessedLoan; } @AfterStep @@ -53,4 +59,11 @@ public abstract class AbstractLoanItemProcessor implements ItemProcessor allNonClosedLoanIds = retrieveLoanIdService.retrieveLoanIds(); + String businessDateParameter = (String) contribution.getStepExecution().getJobExecution().getExecutionContext() + .get(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME); + LocalDate businessDate = LocalDate.parse(Objects.requireNonNull(businessDateParameter)); + List allNonClosedLoanIds = retrieveLoanIdService.retrieveLoanIdsNDaysBehind(NUMBER_OF_DAYS_BEHIND, businessDate); if (allNonClosedLoanIds.isEmpty()) { return RepeatStatus.FINISHED; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemProcessor.java index 88e365373..75894dd4c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InlineCOBLoanItemProcessor.java @@ -31,5 +31,6 @@ public class InlineCOBLoanItemProcessor extends AbstractLoanItemProcessor { @BeforeStep public void beforeStep(StepExecution stepExecution) { setExecutionContext(stepExecution.getJobExecution().getExecutionContext()); + setBusinessDate(stepExecution); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBConstant.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBConstant.java index bce9222af..8c7a4f66d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBConstant.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBConstant.java @@ -28,6 +28,7 @@ public final class LoanCOBConstant { public static final String ALREADY_LOCKED_BY_INLINE_COB_OR_PROCESSED_LOAN_IDS = "alreadyLockedOrProcessedLoanIds"; public static final String INLINE_LOAN_COB_JOB_NAME = "INLINE_LOAN_COB"; + public static final String BUSINESS_DATE_PARAMETER_NAME = "BusinessDate"; private LoanCOBConstant() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBManagerConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBManagerConfiguration.java index 8c20a4b12..0f86c67a0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBManagerConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBManagerConfiguration.java @@ -20,6 +20,7 @@ package org.apache.fineract.cob.loan; import java.util.List; import org.apache.fineract.cob.COBBusinessStepService; +import org.apache.fineract.cob.common.CustomJobParameterResolver; import org.apache.fineract.cob.domain.LoanAccountLockRepository; import org.apache.fineract.cob.listener.COBExecutionListenerRunner; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; @@ -33,6 +34,7 @@ import org.springframework.batch.core.configuration.annotation.StepBuilderFactor import org.springframework.batch.core.explore.JobExplorer; import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.listener.ExecutionContextPromotionListener; import org.springframework.batch.integration.config.annotation.EnableBatchIntegration; import org.springframework.batch.integration.partition.RemotePartitioningManagerStepBuilderFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -72,6 +74,8 @@ public class LoanCOBManagerConfiguration { private LoanAccountLockRepository accountLockRepository; @Autowired private BusinessEventNotifierService businessEventNotifierService; + @Autowired + private CustomJobParameterResolver customJobParameterResolver; @Bean @JobScope @@ -90,6 +94,12 @@ public class LoanCOBManagerConfiguration { return localStepBuilderFactory.get("Fetch and Lock loan accounts - Step").tasklet(fetchAndLockLoanTasklet()).build(); } + @Bean + public Step resolveCustomJobParametersStep() { + return localStepBuilderFactory.get("Resolve custom job parameters - Step").tasklet(resolveCustomJobParametersTasklet()) + .listener(customJobParametersPromotionListener()).build(); + } + @Bean public Step stayedLockedStep() { return localStepBuilderFactory.get("Stayed locked loan accounts - Step").tasklet(stayedLockedTasklet()).build(); @@ -101,6 +111,12 @@ public class LoanCOBManagerConfiguration { return new FetchAndLockLoanTasklet(accountLockRepository, retrieveLoanIdService); } + @Bean + @JobScope + public ResolveLoanCOBCustomJobParametersTasklet resolveCustomJobParametersTasklet() { + return new ResolveLoanCOBCustomJobParametersTasklet(customJobParameterResolver); + } + @Bean @JobScope public StayedLockedLoansTasklet stayedLockedTasklet() { @@ -111,8 +127,16 @@ public class LoanCOBManagerConfiguration { public Job loanCOBJob() { return jobBuilderFactory.get(JobName.LOAN_COB.name()) // .listener(new COBExecutionListenerRunner(applicationContext, JobName.LOAN_COB.name())) // - .start(fetchAndLockStep()).next(loanCOBStep()).next(stayedLockedStep()) // + .start(resolveCustomJobParametersStep()) // + .next(fetchAndLockStep()).next(loanCOBStep()).next(stayedLockedStep()) // .incrementer(new RunIdIncrementer()) // .build(); } + + @Bean + public ExecutionContextPromotionListener customJobParametersPromotionListener() { + ExecutionContextPromotionListener listener = new ExecutionContextPromotionListener(); + listener.setKeys(new String[] { LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME }); + return listener; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemProcessor.java index daea5f970..2aa95d27b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemProcessor.java @@ -31,5 +31,6 @@ public class LoanItemProcessor extends AbstractLoanItemProcessor { @BeforeStep public void beforeStep(StepExecution stepExecution) { setExecutionContext(stepExecution.getExecutionContext()); + setBusinessDate(stepExecution); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ResolveLoanCOBCustomJobParametersTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ResolveLoanCOBCustomJobParametersTasklet.java new file mode 100644 index 000000000..62d3a339f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ResolveLoanCOBCustomJobParametersTasklet.java @@ -0,0 +1,41 @@ +/** + * 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.cob.loan; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.cob.common.CustomJobParameterResolver; +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; + +@Slf4j +@RequiredArgsConstructor +public class ResolveLoanCOBCustomJobParametersTasklet implements Tasklet { + + private final CustomJobParameterResolver customJobParameterResolver; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + customJobParameterResolver.resolve(contribution, chunkContext, LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME, + LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME); + return RepeatStatus.FINISHED; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java index f4efc788e..a62de399a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.cob.loan; +import java.time.LocalDate; import java.util.List; import lombok.RequiredArgsConstructor; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; @@ -28,7 +29,7 @@ public class RetrieveAllNonClosedLoanIdServiceImpl implements RetrieveLoanIdServ private final LoanRepository loanRepository; @Override - public List retrieveLoanIds() { - return loanRepository.findAllNonClosedLoanIds(); + public List retrieveLoanIdsNDaysBehind(Long numberOfDays, LocalDate businessDate) { + return loanRepository.findAllNonClosedLoanIdsOneDayBehind(businessDate.minusDays(numberOfDays)); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java index 266489ecb..e1c7d9fc0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java @@ -18,9 +18,10 @@ */ package org.apache.fineract.cob.loan; +import java.time.LocalDate; import java.util.List; public interface RetrieveLoanIdService { - List retrieveLoanIds(); + List retrieveLoanIdsNDaysBehind(Long numberOfDays, LocalDate businessDate); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java index 5e2f4aac1..5cb920edf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java @@ -33,11 +33,14 @@ import org.apache.fineract.cob.domain.LoanAccountLock; import org.apache.fineract.cob.domain.LoanAccountLockRepository; import org.apache.fineract.cob.domain.LockOwner; import org.apache.fineract.cob.exceptions.LoanAccountLockCannotBeOverruledException; +import org.apache.fineract.cob.loan.LoanCOBConstant; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.exception.PlatformInternalServerException; import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameter; import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameterRepository; import org.apache.fineract.infrastructure.jobs.exception.JobNotFoundException; @@ -141,11 +144,13 @@ public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService getJobParametersMap(List loanIds) { // TODO: refactor for a more generic solution String parameterJson = gson.toJson(loanIds); - CustomJobParameter customJobParameter = new CustomJobParameter(); - customJobParameter.setParameterJson(parameterJson); - Long customJobParameterId = customJobParameterRepository.saveAndFlush(customJobParameter).getId(); + CustomJobParameter loanIdsJobParameter = new CustomJobParameter(); + loanIdsJobParameter.setParameterJson(parameterJson); + Long loanIdsJobParameterId = customJobParameterRepository.saveAndFlush(loanIdsJobParameter).getId(); Map jobParameterMap = new HashMap<>(); - jobParameterMap.put(SpringBatchJobConstants.CUSTOM_JOB_PARAMETER_ID_KEY, new JobParameter(customJobParameterId)); + jobParameterMap.put(SpringBatchJobConstants.CUSTOM_JOB_PARAMETER_ID_KEY, new JobParameter(loanIdsJobParameterId)); + String businessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE).toString(); + jobParameterMap.put(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME, new JobParameter(businessDate)); return jobParameterMap; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResource.java index 7746d8c70..37750755d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResource.java @@ -128,9 +128,11 @@ public class SchedulerJobApiResource { @POST @Path("{" + SchedulerJobApiConstants.JOB_ID + "}") @Operation(summary = "Run a Job", description = "Manually Execute Specific Job.") + @RequestBody(content = @Content(schema = @Schema(implementation = SchedulerJobApiResourceSwagger.ExecuteJobRequest.class))) @ApiResponses({ @ApiResponse(responseCode = "200", description = "POST: jobs/1?command=executeJob") }) public Response executeJob(@PathParam(SchedulerJobApiConstants.JOB_ID) @Parameter(description = "jobId") final Long jobId, - @QueryParam(SchedulerJobApiConstants.COMMAND) @Parameter(description = "command") final String commandParam) { + @QueryParam(SchedulerJobApiConstants.COMMAND) @Parameter(description = "command") final String commandParam, + @Parameter(hidden = true) final String jsonRequestBody) { // check the logged in user have permissions to execute scheduler jobs Response response; if (fineractProperties.getMode().isBatchManagerEnabled()) { @@ -142,7 +144,7 @@ public class SchedulerJobApiResource { } response = Response.status(400).build(); if (is(commandParam, SchedulerJobApiConstants.COMMAND_EXECUTE_JOB)) { - this.jobRegisterService.executeJob(jobId); + jobRegisterService.executeJobWithParameters(jobId, jsonRequestBody); response = Response.status(202).build(); } else { throw new UnrecognizedQueryParamException(SchedulerJobApiConstants.COMMAND, commandParam); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResourceSwagger.java index 1224abe06..81bfec247 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/api/SchedulerJobApiResourceSwagger.java @@ -22,6 +22,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.util.Date; import java.util.List; import org.apache.fineract.infrastructure.jobs.data.JobDetailHistoryData; +import org.apache.fineract.infrastructure.jobs.data.JobParameterDTO; /** * Created by sanyam on 12/8/17. @@ -100,4 +101,16 @@ final class SchedulerJobApiResourceSwagger { public List pageItems; } + + @Schema(description = "ExecuteJobRequest") + public static final class ExecuteJobRequest { + + private ExecuteJobRequest() { + + } + + @Schema(example = "Update loan Summary") + public List jobParameters; + + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobParameterDTO.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobParameterDTO.java new file mode 100644 index 000000000..59a24df70 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobParameterDTO.java @@ -0,0 +1,32 @@ +/** + * 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.infrastructure.jobs.data; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@AllArgsConstructor +@EqualsAndHashCode +public class JobParameterDTO { + + private String parameterName; + private String parameterValue; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobParameterDataParser.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobParameterDataParser.java new file mode 100644 index 000000000..8aab8271c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobParameterDataParser.java @@ -0,0 +1,59 @@ +/** + * 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.infrastructure.jobs.service; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.jobs.data.JobParameterDTO; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class JobParameterDataParser { + + private final FromJsonHelper jsonHelper; + + public Set parseExecution(String requestBody) { + if (Objects.isNull(requestBody)) { + return Collections.emptySet(); + } + JsonObject element = JsonParser.parseString(requestBody).getAsJsonObject(); + if (Objects.isNull(element)) { + return Collections.emptySet(); + } + JsonArray jobParametersJsonArray = jsonHelper.extractJsonArrayNamed("jobParameters", element); + if (Objects.isNull(jobParametersJsonArray)) { + return Collections.emptySet(); + } + Set jobParameters = new HashSet<>(); + for (final JsonElement jobParameterElement : jobParametersJsonArray) { + jobParameters.add(new JobParameterDTO(jobParameterElement.getAsJsonObject().get("parameterName").getAsString(), + jobParameterElement.getAsJsonObject().get("parameterValue").getAsString())); + } + return Collections.unmodifiableSet(jobParameters); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterService.java index 67dc8c2a4..71810c714 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterService.java @@ -22,7 +22,7 @@ import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetail; public interface JobRegisterService { - void executeJob(Long jobId); + void executeJobWithParameters(Long jobId, String jobParametersJson); void rescheduleJob(Long jobId); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterServiceImpl.java index d5ae619f8..37f67a200 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterServiceImpl.java @@ -20,15 +20,18 @@ package org.apache.fineract.infrastructure.jobs.service; import com.google.common.base.Splitter; import java.text.ParseException; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Properties; +import java.util.Set; import java.util.TimeZone; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; import org.apache.fineract.infrastructure.core.exception.PlatformInternalServerException; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.infrastructure.jobs.data.JobParameterDTO; import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetail; import org.apache.fineract.infrastructure.jobs.domain.SchedulerDetail; import org.apache.fineract.infrastructure.jobs.exception.JobNodeIdMismatchingException; @@ -61,6 +64,8 @@ import org.springframework.stereotype.Service; @Slf4j public class JobRegisterServiceImpl implements JobRegisterService, ApplicationListener { + private static final String JOB_EXECUTION_FAILED_MESSAGE = "Job execution failed for job with name: "; + @Autowired private SchedularWritePlatformService schedularWritePlatformService; @@ -81,12 +86,15 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi @Autowired private JobStarter jobStarter; + @Autowired + private JobParameterDataParser dataParser; + @Autowired private JobNameService jobNameService; private static final String JOB_STARTER_METHOD_NAME = "run"; - public void executeJob(final ScheduledJobDetail scheduledJobDetail, String triggerType) { + public void executeJob(final ScheduledJobDetail scheduledJobDetail, String triggerType, Set jobParameterDTOSet) { try { final JobDataMap jobDataMap = new JobDataMap(); if (triggerType == null) { @@ -96,7 +104,7 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi jobDataMap.put(SchedulerServiceConstants.TENANT_IDENTIFIER, ThreadLocalContextUtil.getTenant().getTenantIdentifier()); final String schedulerName = getSchedulerName(scheduledJobDetail); final Scheduler scheduler = SCHEDULERS.get(schedulerName); - final JobDetail jobDetail = createJobDetail(scheduledJobDetail); + final JobDetail jobDetail = createJobDetail(scheduledJobDetail, jobParameterDTOSet); JobKey jobKey = jobDetail.getKey(); if (scheduler == null || !scheduler.checkExists(jobKey)) { SchedulerStopListener schedulerStopListener = new SchedulerStopListener(this); @@ -157,7 +165,7 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi for (final ScheduledJobDetail jobDetail : scheduledJobDetails) { if (jobDetail.isTriggerMisfired() || jobDetail.isMismatchedJob()) { if (jobDetail.isActiveSchedular()) { - executeJob(jobDetail, SchedulerServiceConstants.TRIGGER_TYPE_CRON); + executeJob(jobDetail, SchedulerServiceConstants.TRIGGER_TYPE_CRON, Collections.emptySet()); jobDetail.setMismatchedJob(false); } final String schedulerName = getSchedulerName(jobDetail); @@ -198,7 +206,8 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi } @Override - public void executeJob(final Long jobId) { + public void executeJobWithParameters(final Long jobId, String jobParametersJson) { + Set jobParameterDTOSet = dataParser.parseExecution(jobParametersJson); final ScheduledJobDetail scheduledJobDetail = this.schedularWritePlatformService.findByJobId(jobId); if (scheduledJobDetail == null) { throw new JobNotFoundException(String.valueOf(jobId)); @@ -206,7 +215,7 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi final String nodeIdStored = scheduledJobDetail.getNodeId().toString(); if (nodeIdStored.equals(fineractProperties.getNodeId()) || nodeIdStored.equals("0")) { - executeJob(scheduledJobDetail, null); + executeJob(scheduledJobDetail, null, jobParameterDTOSet); scheduledJobDetail.setMismatchedJob(false); this.schedularWritePlatformService.saveOrUpdate(scheduledJobDetail); } else { @@ -234,7 +243,7 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi @Override public void scheduleJob(final ScheduledJobDetail scheduledJobDetails) { try { - final JobDetail jobDetail = createJobDetail(scheduledJobDetails); + final JobDetail jobDetail = createJobDetail(scheduledJobDetails, Collections.emptySet()); scheduledJobDetails.setJobKey(getJobKeyAsString(jobDetail.getKey())); if (!scheduledJobDetails.isActiveSchedular()) { scheduledJobDetails.setNextRunTime(null); @@ -315,7 +324,8 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi return schedulerFactoryBean.getScheduler(); } - private JobDetail createJobDetail(final ScheduledJobDetail scheduledJobDetail) throws Exception { + private JobDetail createJobDetail(final ScheduledJobDetail scheduledJobDetail, Set jobParameterDTOSet) + throws Exception { final FineractPlatformTenant tenant = ThreadLocalContextUtil.getTenant(); JobNameData jobName = jobNameService.getJobByHumanReadableName(scheduledJobDetail.getJobName()); @@ -328,7 +338,7 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi jobDetailFactoryBean.setGroup(scheduledJobDetail.getGroupName()); jobDetailFactoryBean.setConcurrent(false); - jobDetailFactoryBean.setArguments(job, scheduledJobDetail, ThreadLocalContextUtil.getContext()); + jobDetailFactoryBean.setArguments(job, scheduledJobDetail, ThreadLocalContextUtil.getContext(), jobParameterDTOSet); jobDetailFactoryBean.afterPropertiesSet(); return jobDetailFactoryBean.getObject(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobStarter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobStarter.java index 9e72cd54b..43e0e3df5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobStarter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobStarter.java @@ -18,14 +18,20 @@ */ package org.apache.fineract.infrastructure.jobs.service; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.domain.FineractContext; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.infrastructure.jobs.data.JobParameterDTO; import org.apache.fineract.infrastructure.jobs.domain.JobParameterRepository; import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetail; +import org.apache.fineract.infrastructure.jobs.service.jobname.JobNameService; +import org.apache.fineract.infrastructure.jobs.service.jobparameterprovider.JobParameterProvider; import org.springframework.batch.core.Job; import org.springframework.batch.core.JobParameter; import org.springframework.batch.core.JobParameters; @@ -45,14 +51,19 @@ public class JobStarter { private final JobExplorer jobExplorer; private final JobLauncher jobLauncher; private final JobParameterRepository jobParameterRepository; + private final List jobParameterProviders; + private final JobNameService jobNameService; - public void run(Job job, ScheduledJobDetail scheduledJobDetail, FineractContext fineractContext) - throws JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException, JobParametersInvalidException, - JobRestartException { + public void run(Job job, ScheduledJobDetail scheduledJobDetail, FineractContext fineractContext, + Set jobParameterDTOSet) throws JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException, + JobParametersInvalidException, JobRestartException { ThreadLocalContextUtil.init(fineractContext); Map jobParameterMap = getJobParameter(scheduledJobDetail); JobParameters jobParameters = new JobParametersBuilder(jobExplorer).getNextJobParameters(job) - .addJobParameters(new JobParameters(jobParameterMap)).toJobParameters(); + .addJobParameters(new JobParameters(jobParameterMap)) + .addJobParameters(new JobParameters(provideCustomJobParameters( + jobNameService.getJobByHumanReadableName(scheduledJobDetail.getJobName()).getEnumStyleName(), jobParameterDTOSet))) + .toJobParameters(); jobLauncher.run(job, jobParameters); } @@ -65,4 +76,10 @@ public class JobStarter { } return jobParameterMap; } + + private Map provideCustomJobParameters(String jobName, Set jobParameterDTOSet) { + Optional jobParameterProvider = jobParameterProviders.stream() + .filter(provider -> provider.canProvideParametersForJob(jobName)).findFirst(); + return jobParameterProvider.map(parameterProvider -> parameterProvider.provide(jobParameterDTOSet)).orElse(Collections.emptyMap()); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/executealldirtyjobs/ExecuteAllDirtyJobsTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/executealldirtyjobs/ExecuteAllDirtyJobsTasklet.java index 28feabe88..53c878245 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/executealldirtyjobs/ExecuteAllDirtyJobsTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/executealldirtyjobs/ExecuteAllDirtyJobsTasklet.java @@ -44,7 +44,7 @@ public class ExecuteAllDirtyJobsTasklet implements Tasklet { for (ScheduledJobDetail scheduledJobDetail : jobDetails) { if (scheduledJobDetail.getNodeId().toString().equals(fineractProperties.getNodeId())) { - jobRegisterService.executeJob(scheduledJobDetail.getId()); + jobRegisterService.executeJobWithParameters(scheduledJobDetail.getId(), null); } } return RepeatStatus.FINISHED; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/AbstractJobParameterProvider.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/AbstractJobParameterProvider.java new file mode 100644 index 000000000..17c478572 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/AbstractJobParameterProvider.java @@ -0,0 +1,29 @@ +/** + * 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.infrastructure.jobs.service.jobparameterprovider; + +public abstract class AbstractJobParameterProvider implements JobParameterProvider { + + @Override + public boolean canProvideParametersForJob(String jobName) { + return jobName.equals(getJobName()); + } + + protected abstract String getJobName(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/JobParameterProvider.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/JobParameterProvider.java new file mode 100644 index 000000000..e96145039 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/JobParameterProvider.java @@ -0,0 +1,31 @@ +/** + * 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.infrastructure.jobs.service.jobparameterprovider; + +import java.util.Map; +import java.util.Set; +import org.apache.fineract.infrastructure.jobs.data.JobParameterDTO; +import org.springframework.batch.core.JobParameter; + +public interface JobParameterProvider { + + Map provide(Set jobParameterDTOList); + + boolean canProvideParametersForJob(String jobName); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/LoanCOBJobParameterProvider.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/LoanCOBJobParameterProvider.java new file mode 100644 index 000000000..f84be7816 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/jobparameterprovider/LoanCOBJobParameterProvider.java @@ -0,0 +1,85 @@ +/** + * 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.infrastructure.jobs.service.jobparameterprovider; + +import com.google.gson.Gson; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.cob.loan.LoanCOBConstant; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.infrastructure.jobs.data.JobParameterDTO; +import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameter; +import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameterRepository; +import org.apache.fineract.infrastructure.jobs.service.JobName; +import org.apache.fineract.infrastructure.springbatch.SpringBatchJobConstants; +import org.springframework.batch.core.JobParameter; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +public class LoanCOBJobParameterProvider extends AbstractJobParameterProvider implements InitializingBean { + + private final CustomJobParameterRepository customJobParameterRepository; + private final GoogleGsonSerializerHelper gsonFactory; + + private Gson gson; + + @Override + public void afterPropertiesSet() throws Exception { + this.gson = gsonFactory.createSimpleGson(); + } + + @Override + @Transactional + public Map provide(Set jobParameterDTOSet) { + Map jobParameterMap = new HashMap<>(); + CustomJobParameter customJobParameter = new CustomJobParameter(); + customJobParameter.setParameterJson(gson.toJson(getJobParameterDTOListWithCorrectBusinessDate(jobParameterDTOSet))); + CustomJobParameter savedCustomJobParameter = customJobParameterRepository.saveAndFlush(customJobParameter); + jobParameterMap.put(SpringBatchJobConstants.CUSTOM_JOB_PARAMETER_ID_KEY, new JobParameter(savedCustomJobParameter.getId())); + return jobParameterMap; + } + + @Override + public String getJobName() { + return JobName.LOAN_COB.name(); + } + + private Set getJobParameterDTOListWithCorrectBusinessDate(Set jobParameterDTOset) { + Set jobParameterDTOListWithCorrectBusinessDate = jobParameterDTOset.isEmpty() ? new HashSet<>() + : new HashSet<>(jobParameterDTOset); + Optional optionalBusinessDateJobParameter = jobParameterDTOListWithCorrectBusinessDate.stream() + .filter(jobParameterDTO -> LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME.equals(jobParameterDTO.getParameterName())) + .findFirst(); + if (optionalBusinessDateJobParameter.isEmpty()) { + jobParameterDTOListWithCorrectBusinessDate.add(new JobParameterDTO(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME, + ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE).format(DateTimeFormatter.ISO_DATE))); + } + return jobParameterDTOListWithCorrectBusinessDate; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java index 95d6a082a..ad4148635 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java @@ -227,10 +227,11 @@ public class LoansApiResource { "interestRateFrequencyTypeOptions", "fundOptions", "repaymentStrategyOptions", "chargeOptions", "loanOfficerOptions", "loanPurposeOptions", "loanCollateralOptions", "chargeTemplate", "calendarOptions", "syncDisbursementWithMeeting", "loanCounter", "loanProductCounter", "notes", "accountLinkingOptions", "linkedAccount", "interestRateDifferential", - "isFloatingInterestRate", "interestRatesPeriods", LoanApiConstants.canUseForTopup, LoanApiConstants.isTopup, - LoanApiConstants.loanIdToClose, LoanApiConstants.topupAmount, LoanApiConstants.clientActiveLoanOptions, - LoanApiConstants.datatables, LoanProductConstants.RATES_PARAM_NAME, LoanApiConstants.MULTIDISBURSE_DETAILS_PARAMNAME, - LoanApiConstants.EMI_AMOUNT_VARIATIONS_PARAMNAME, LoanApiConstants.COLLECTION_PARAMNAME)); + "isFloatingInterestRate", "interestRatesPeriods", "lastClosedBusinessDate", LoanApiConstants.canUseForTopup, + LoanApiConstants.isTopup, LoanApiConstants.loanIdToClose, LoanApiConstants.topupAmount, + LoanApiConstants.clientActiveLoanOptions, LoanApiConstants.datatables, LoanProductConstants.RATES_PARAM_NAME, + LoanApiConstants.MULTIDISBURSE_DETAILS_PARAMNAME, LoanApiConstants.EMI_AMOUNT_VARIATIONS_PARAMNAME, + LoanApiConstants.COLLECTION_PARAMNAME)); private static final Set LOAN_APPROVAL_DATA_PARAMETERS = new HashSet<>(Arrays.asList("approvalDate", "approvalAmount")); private static final Set GLIM_ACCOUNTS_DATA_PARAMETERS = new HashSet<>(Arrays.asList("glimId", "groupId", "clientId", diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java index 5e5829594..fbd8777b0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java @@ -978,6 +978,7 @@ final class LoansApiResourceSwagger { public Boolean fraud; @Schema(example = "250.000000") public Double totalOverpaid; + public LocalDate lastClosedBusinessDate; } @Schema(description = "GetLoansResponse") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java index 5b149027b..caf3dc0f8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java @@ -250,6 +250,7 @@ public class LoanAccountData { private CollectionData delinquent; private DelinquencyRangeData delinquencyRange; + private LocalDate lastClosedBusinessDate; public static LoanAccountData importInstanceIndividual(EnumOptionData loanTypeEnumOption, Long clientId, Long productId, Long loanOfficerId, LocalDate submittedOnDate, Long fundId, BigDecimal principal, Integer numberOfRepayments, @@ -648,7 +649,8 @@ public class LoanAccountData { final Boolean isVariableInstallmentsAllowed, Integer minimumGap, Integer maximumGap, final EnumOptionData subStatus, final boolean canUseForTopup, final boolean isTopup, final Long closureLoanId, final String closureLoanAccountNo, final BigDecimal topupAmount, final boolean isEqualAmortization, final BigDecimal fixedPrincipalPercentagePerInstallment, - final DelinquencyRangeData delinquencyRange, final boolean disallowExpectedDisbursements, final boolean fraud) { + final DelinquencyRangeData delinquencyRange, final boolean disallowExpectedDisbursements, final boolean fraud, + LocalDate lastClosedBusinessDate) { final CollectionData delinquent = CollectionData.template(); @@ -687,7 +689,8 @@ public class LoanAccountData { .setSubStatus(subStatus).setCanUseForTopup(canUseForTopup).setTopup(isTopup).setClosureLoanId(closureLoanId) .setClosureLoanAccountNo(closureLoanAccountNo).setTopupAmount(topupAmount).setIsEqualAmortization(isEqualAmortization) .setFixedPrincipalPercentagePerInstallment(fixedPrincipalPercentagePerInstallment).setDelinquent(delinquent) - .setDelinquencyRange(delinquencyRange).setDisallowExpectedDisbursements(disallowExpectedDisbursements).setFraud(fraud); + .setDelinquencyRange(delinquencyRange).setDisallowExpectedDisbursements(disallowExpectedDisbursements).setFraud(fraud) + .setLastClosedBusinessDate(lastClosedBusinessDate); } /* @@ -771,7 +774,7 @@ public class LoanAccountData { .setIsEqualAmortization(acc.isEqualAmortization) .setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment) .setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements) - .setFraud(acc.fraud); + .setFraud(acc.fraud).setLastClosedBusinessDate(acc.getLastClosedBusinessDate()); } public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection productOptions, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 7df1cc288..ac4b0a997 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -432,6 +432,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @Column(name = "fixed_principal_percentage_per_installment", scale = 2, precision = 5, nullable = true) private BigDecimal fixedPrincipalPercentagePerInstallment; + @Column(name = "last_closed_business_date") + private LocalDate lastClosedBusinessDate; + public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final Integer loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose, final String transactionProcessingStrategyCode, final LoanProductRelatedDetail loanRepaymentScheduleDetail, @@ -6920,4 +6923,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return ageOfOverdueDays; } + public LocalDate getLastClosedBusinessDate() { + return this.lastClosedBusinessDate; + } + + public void setLastClosedBusinessDate(LocalDate lastClosedBusinessDate) { + this.lastClosedBusinessDate = lastClosedBusinessDate; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java index 0ddeb8d2b..6e3a1e3ed 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java @@ -71,6 +71,8 @@ public interface LoanRepository extends JpaRepository, JpaSpecificat String FIND_ALL_NON_CLOSED = "select loan.id from Loan loan where loan.loanStatus in (100,200,300,303,304)"; + String FIND_ALL_NON_CLOSED_ONE_DAY_BEHIND = "select loan.id from Loan loan where loan.loanStatus in (100,200,300,303,304) and (:last_closed_business_date = loan.lastClosedBusinessDate or loan.lastClosedBusinessDate is NULL)"; + String FIND_NON_CLOSED_LOAN_THAT_BELONGS_TO_CLIENT = "select loan from Loan loan where loan.id = :loanId and loan.loanStatus = 300 and loan.client.id = :clientId"; String FIND_BY_ACCOUNT_NUMBER = "select loan from Loan loan where loan.accountNumber = :accountNumber"; @@ -172,4 +174,8 @@ public interface LoanRepository extends JpaRepository, JpaSpecificat @Query(FIND_ID_BY_EXTERNAL_ID) Long findIdByExternalId(@Param("externalId") String externalId); + + @Query(FIND_ALL_NON_CLOSED_ONE_DAY_BEHIND) + List findAllNonClosedLoanIdsOneDayBehind(@Param("last_closed_business_date") LocalDate businessDate); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index b8ae00ea8..16ecd7028 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -661,7 +661,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { + " lpvi.minimum_gap as minimuminstallmentgap, lpvi.maximum_gap as maximuminstallmentgap, " + " lp.can_use_for_topup as canUseForTopup, l.is_topup as isTopup, topup.closure_loan_id as closureLoanId, " + " l.total_recovered_derived as totalRecovered, topuploan.account_no as closureLoanAccountNo, " - + " topup.topup_amount as topupAmount from m_loan l" // + + " topup.topup_amount as topupAmount, l.last_closed_business_date as lastClosedBusinessDate from m_loan l" // + " join m_product_loan lp on lp.id = l.product_id" // + " left join m_loan_recalculation_details lir on lir.loan_id = l.id join m_currency rc on rc." + sqlGenerator.escape("code") + " = l.currency_code" // @@ -987,6 +987,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { DelinquencyRangeData delinquencyRange = this.delinquencyReadPlatformService.retrieveCurrentDelinquencyTag(id); final boolean isFraud = rs.getBoolean("isFraud"); + final LocalDate lastClosedBusinessDate = JdbcSupport.getLocalDate(rs, "lastClosedBusinessDate"); return LoanAccountData.basicLoanDetails(id, accountNo, status, externalId, clientId, clientAccountNo, clientName, clientOfficeId, clientExternalId, groupData, loanType, loanProductId, loanProductName, loanProductDescription, @@ -1002,7 +1003,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, createStandingInstructionAtDisbursement, isvariableInstallmentsAllowed, minimumGap, maximumGap, loanSubStatus, canUseForTopup, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization, - fixedPrincipalPercentagePerInstallment, delinquencyRange, disallowExpectedDisbursements, isFraud); + fixedPrincipalPercentagePerInstallment, delinquencyRange, disallowExpectedDisbursements, isFraud, + lastClosedBusinessDate); } } diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index c521010ca..f9185c1d3 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -93,5 +93,5 @@ - + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0074_add_last_cob_business_date_column_to_loan.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0074_add_last_cob_business_date_column_to_loan.xml new file mode 100644 index 000000000..9bf126b79 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0074_add_last_cob_business_date_column_to_loan.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/FetchAndLockLoanStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/FetchAndLockLoanStepDefinitions.java index 44ab858c5..2a4a16f7b 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/FetchAndLockLoanStepDefinitions.java +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/FetchAndLockLoanStepDefinitions.java @@ -19,16 +19,22 @@ package org.apache.fineract.cob.loan; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import io.cucumber.java8.En; +import java.time.LocalDate; +import java.time.ZoneId; import java.util.Collections; +import java.util.HashMap; import java.util.List; import org.apache.fineract.cob.domain.LoanAccountLock; import org.apache.fineract.cob.domain.LoanAccountLockRepository; import org.apache.fineract.cob.domain.LockOwner; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.mockito.Mockito; @@ -50,23 +56,26 @@ public class FetchAndLockLoanStepDefinitions implements En { public FetchAndLockLoanStepDefinitions() { Given("/^The FetchAndLockLoanTasklet.execute method with action (.*)$/", (String action) -> { ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); + HashMap businessDateMap = new HashMap<>(); + businessDateMap.put(BusinessDateType.COB_DATE, LocalDate.now(ZoneId.systemDefault())); + ThreadLocalContextUtil.setBusinessDates(businessDateMap); this.action = action; if ("empty loanIds".equals(action)) { - lenient().when(retrieveLoanIdService.retrieveLoanIds()).thenReturn(Collections.emptyList()); + lenient().when(retrieveLoanIdService.retrieveLoanIdsNDaysBehind(anyLong(), any())).thenReturn(Collections.emptyList()); } else if ("good".equals(action)) { - lenient().when(retrieveLoanIdService.retrieveLoanIds()).thenReturn(List.of(1L, 2L, 3L)); + lenient().when(retrieveLoanIdService.retrieveLoanIdsNDaysBehind(anyLong(), any())).thenReturn(List.of(1L, 2L, 3L)); lenient().when(loanAccountLockRepository.findAllByLoanIdIn(Mockito.anyList())).thenReturn(Collections.emptyList()); } else if ("soft lock".equals(action)) { - lenient().when(retrieveLoanIdService.retrieveLoanIds()).thenReturn(List.of(1L, 2L, 3L)); + lenient().when(retrieveLoanIdService.retrieveLoanIdsNDaysBehind(anyLong(), any())).thenReturn(List.of(1L, 2L, 3L)); lenient().when(loanAccountLockRepository.findAllByLoanIdIn(Mockito.anyList())) .thenReturn(List.of(new LoanAccountLock(1L, LockOwner.LOAN_COB_PARTITIONING))); } else if ("inline cob".equals(action)) { - lenient().when(retrieveLoanIdService.retrieveLoanIds()).thenReturn(List.of(1L, 2L, 3L)); + lenient().when(retrieveLoanIdService.retrieveLoanIdsNDaysBehind(anyLong(), any())).thenReturn(List.of(1L, 2L, 3L)); lenient().when(loanAccountLockRepository.findAllByLoanIdIn(Mockito.anyList())) .thenReturn(List.of(new LoanAccountLock(2L, LockOwner.LOAN_INLINE_COB_PROCESSING))); } else if ("chunk processing".equals(action)) { - lenient().when(retrieveLoanIdService.retrieveLoanIds()).thenReturn(List.of(1L, 2L, 3L)); + lenient().when(retrieveLoanIdService.retrieveLoanIdsNDaysBehind(anyLong(), any())).thenReturn(List.of(1L, 2L, 3L)); lenient().when(loanAccountLockRepository.findAllByLoanIdIn(Mockito.anyList())) .thenReturn(List.of(new LoanAccountLock(3L, LockOwner.LOAN_COB_CHUNK_PROCESSING))); } @@ -74,7 +83,8 @@ public class FetchAndLockLoanStepDefinitions implements En { JobExecution jobExecution = new JobExecution(1L); StepExecution stepExecution = new StepExecution("step", jobExecution); contribution = new StepContribution(stepExecution); - + contribution.getStepExecution().getJobExecution().getExecutionContext().put(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME, + LocalDate.now(ZoneId.systemDefault()).toString()); fetchAndLockLoanTasklet = new FetchAndLockLoanTasklet(loanAccountLockRepository, retrieveLoanIdService); }); diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemProcessorStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemProcessorStepDefinitions.java index 543f1c03e..3c2e2f842 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemProcessorStepDefinitions.java +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemProcessorStepDefinitions.java @@ -24,9 +24,12 @@ import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import io.cucumber.java8.En; +import java.time.LocalDate; +import java.time.ZoneId; import java.util.TreeMap; import org.apache.fineract.cob.COBBusinessStepService; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.StepExecution; import org.springframework.batch.item.ExecutionContext; @@ -47,8 +50,10 @@ public class LoanItemProcessorStepDefinitions implements En { public LoanItemProcessorStepDefinitions() { Given("/^The LoanItemProcessor.process method with item (.*)$/", (String loanItem) -> { - - StepExecution stepExecution = new StepExecution("test", null); + JobExecution jobExecution = new JobExecution(1L); + jobExecution.getExecutionContext().put(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME, + LocalDate.now(ZoneId.systemDefault()).toString()); + StepExecution stepExecution = new StepExecution("test", jobExecution); ExecutionContext stepExecutionContext = new ExecutionContext(); stepExecutionContext.put(LoanCOBConstant.BUSINESS_STEP_MAP, treeMap); stepExecution.setExecutionContext(stepExecutionContext); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java index fbc2e73ed..63b6c627c 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java @@ -781,7 +781,9 @@ public class DelinquencyBucketsIntegrationTest { } // Move the Business date to get older the loan and to have an overdue loan + LocalDate lastLoanCOBBusinessDate = businessDate; businessDate = businessDate.plusDays(50); + schedulerJobHelper.fastForwardTime(lastLoanCOBBusinessDate, businessDate, jobName, responseSpec); log.info("Current date {}", businessDate); BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, businessDate); // Run Second time the Job @@ -838,5 +840,4 @@ public class DelinquencyBucketsIntegrationTest { log.info("{}", chargeApplyJSON); return chargeApplyJSON; } - } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SchedulerJobsTestResults.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SchedulerJobsTestResults.java index 96d44fc25..ac8604444 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SchedulerJobsTestResults.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SchedulerJobsTestResults.java @@ -46,6 +46,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.integrationtests.common.BusinessDateHelper; import org.apache.fineract.integrationtests.common.ClientHelper; @@ -648,6 +649,58 @@ public class SchedulerJobsTestResults { } } + @Test + public void testLoanCOBRunsOnlyOnLoansOneDayBehind() { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec); + + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec); + Assertions.assertNotNull(clientID); + + Integer overdueFeeChargeId = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanOverdueFeeJSONWithCalculationTypePercentage("1")); + Assertions.assertNotNull(overdueFeeChargeId); + + final Integer loanProductID = createLoanProduct(overdueFeeChargeId.toString()); + Assertions.assertNotNull(loanProductID); + HashMap loanStatusHashMap; + + final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "10 January 2020"); + + Assertions.assertNotNull(loanID); + + loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = loanTransactionHelper.approveLoan("01 March 2020", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + + String loanDetails = loanTransactionHelper.getLoanDetails(requestSpec, responseSpec, loanID); + loanStatusHashMap = loanTransactionHelper.disburseLoanWithNetDisbursalAmount("02 March 2020", loanID, + JsonPath.from(loanDetails).get("netDisbursalAmount").toString()); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.COB_DATE, LocalDate.of(2020, 3, 2)); + String jobName = "Loan COB"; + + schedulerJobHelper.executeAndAwaitJob(jobName); + GetLoansLoanIdResponse loan = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanID); + Assertions.assertEquals(LocalDate.of(2020, 3, 2), loan.getLastClosedBusinessDate()); + + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.COB_DATE, LocalDate.of(2020, 3, 3)); + schedulerJobHelper.executeAndAwaitJob(jobName); + + loan = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanID); + Assertions.assertEquals(LocalDate.of(2020, 3, 3), loan.getLastClosedBusinessDate()); + + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.COB_DATE, LocalDate.of(2020, 3, 5)); + schedulerJobHelper.executeAndAwaitJob(jobName); + + loan = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanID); + Assertions.assertEquals(LocalDate.of(2020, 3, 3), loan.getLastClosedBusinessDate()); + + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + @Test public void testLoanCOBApplyPenaltyOnDue() { GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); @@ -688,15 +741,17 @@ public class SchedulerJobsTestResults { Assertions.assertEquals(0, (Integer) repaymentScheduleDataAfter.get(1).get("penaltyChargesDue"), "Verifying From Penalty Charges due fot first Repayment after Successful completion of Scheduler Job"); - BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.COB_DATE, LocalDate.of(2020, 4, 2)); + LocalDate lastBusinessDateBeforeFastForward = LocalDate.of(2020, 4, 2); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.COB_DATE, lastBusinessDateBeforeFastForward); this.schedulerJobHelper.executeAndAwaitJob(jobName); repaymentScheduleDataAfter = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); Assertions.assertEquals(39.39f, (Float) repaymentScheduleDataAfter.get(1).get("penaltyChargesDue"), "Verifying From Penalty Charges due fot first Repayment after Successful completion of Scheduler Job"); GlobalConfigurationHelper.updateValueForGlobalConfiguration(this.requestSpec, this.responseSpec, "10", "1"); - - BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.COB_DATE, LocalDate.of(2020, 5, 2)); + LocalDate dateToFastForward = LocalDate.of(2020, 5, 2); + schedulerJobHelper.fastForwardTime(lastBusinessDateBeforeFastForward, dateToFastForward, jobName, responseSpec); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.COB_DATE, dateToFastForward); this.schedulerJobHelper.executeAndAwaitJob(jobName); repaymentScheduleDataAfter = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); Assertions.assertEquals(0, (Integer) repaymentScheduleDataAfter.get(2).get("penaltyChargesDue"), diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java index 6d6155afc..1488485bf 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/SchedulerJobHelper.java @@ -31,6 +31,7 @@ import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import java.time.Duration; import java.time.Instant; +import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.HashMap; @@ -39,6 +40,7 @@ import java.util.Map; import java.util.concurrent.Callable; import java.util.function.Function; import java.util.stream.Collectors; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -203,6 +205,15 @@ public class SchedulerJobHelper { // JobDetailData anyway. } + public void fastForwardTime(LocalDate lastBusinessDateBeforeFastForward, LocalDate dateToFastForward, String jobName, + ResponseSpecification responseSpec) { + while (lastBusinessDateBeforeFastForward.isBefore(dateToFastForward)) { + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.COB_DATE, lastBusinessDateBeforeFastForward); + executeAndAwaitJob(jobName); + lastBusinessDateBeforeFastForward = lastBusinessDateBeforeFastForward.plusDays(1); + } + } + @SuppressWarnings("unchecked") private Callable> jobLastRunHistorySupplier(int jobId) { return () -> {