[FINERACT-1678] Introducing Last COB Business Date for loans

This commit is contained in:
taskain7
2022-11-28 15:27:07 +01:00
committed by Arnold Gálovics
parent 6f7f55a227
commit ca51084e86
36 changed files with 664 additions and 48 deletions
@@ -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<JobParameterDTO> 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<JobParameterDTO> 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<HashSet<JobParameterDTO>>() {}.getType());
}
}
@@ -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);
}
}
@@ -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<Loan, L
@Setter(AccessLevel.PROTECTED)
private ExecutionContext executionContext;
private LocalDate businessDate;
@Override
public Loan process(@NotNull Loan item) throws Exception {
TreeMap<Long, String> businessStepMap = (TreeMap<Long, String>) 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<Loan, L
return ExitStatus.COMPLETED;
}
protected void setBusinessDate(StepExecution stepExecution) {
this.businessDate = LocalDate.parse(
Objects.requireNonNull(
(String) stepExecution.getJobExecution().getExecutionContext().get(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME)),
DateTimeFormatter.ISO_DATE);
}
}
@@ -18,8 +18,10 @@
*/
package org.apache.fineract.cob.loan;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.domain.LoanAccountLock;
@@ -35,13 +37,18 @@ import org.springframework.batch.repeat.RepeatStatus;
@RequiredArgsConstructor
public class FetchAndLockLoanTasklet implements Tasklet {
private static final Long NUMBER_OF_DAYS_BEHIND = 1L;
private final LoanAccountLockRepository loanAccountLockRepository;
private final RetrieveLoanIdService retrieveLoanIdService;
@Override
public RepeatStatus execute(@NotNull StepContribution contribution, @NotNull ChunkContext chunkContext) throws Exception {
List<Long> allNonClosedLoanIds = retrieveLoanIdService.retrieveLoanIds();
String businessDateParameter = (String) contribution.getStepExecution().getJobExecution().getExecutionContext()
.get(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME);
LocalDate businessDate = LocalDate.parse(Objects.requireNonNull(businessDateParameter));
List<Long> allNonClosedLoanIds = retrieveLoanIdService.retrieveLoanIdsNDaysBehind(NUMBER_OF_DAYS_BEHIND, businessDate);
if (allNonClosedLoanIds.isEmpty()) {
return RepeatStatus.FINISHED;
}
@@ -31,5 +31,6 @@ public class InlineCOBLoanItemProcessor extends AbstractLoanItemProcessor {
@BeforeStep
public void beforeStep(StepExecution stepExecution) {
setExecutionContext(stepExecution.getJobExecution().getExecutionContext());
setBusinessDate(stepExecution);
}
}
@@ -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() {
@@ -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;
}
}
@@ -31,5 +31,6 @@ public class LoanItemProcessor extends AbstractLoanItemProcessor {
@BeforeStep
public void beforeStep(StepExecution stepExecution) {
setExecutionContext(stepExecution.getExecutionContext());
setBusinessDate(stepExecution);
}
}
@@ -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;
}
}
@@ -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<Long> retrieveLoanIds() {
return loanRepository.findAllNonClosedLoanIds();
public List<Long> retrieveLoanIdsNDaysBehind(Long numberOfDays, LocalDate businessDate) {
return loanRepository.findAllNonClosedLoanIdsOneDayBehind(businessDate.minusDays(numberOfDays));
}
}
@@ -18,9 +18,10 @@
*/
package org.apache.fineract.cob.loan;
import java.time.LocalDate;
import java.util.List;
public interface RetrieveLoanIdService {
List<Long> retrieveLoanIds();
List<Long> retrieveLoanIdsNDaysBehind(Long numberOfDays, LocalDate businessDate);
}
@@ -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<L
private Map<String, JobParameter> getJobParametersMap(List<Long> 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<String, JobParameter> 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;
}
@@ -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);
@@ -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<JobDetailHistoryDataSwagger> pageItems;
}
@Schema(description = "ExecuteJobRequest")
public static final class ExecuteJobRequest {
private ExecuteJobRequest() {
}
@Schema(example = "Update loan Summary")
public List<JobParameterDTO> jobParameters;
}
}
@@ -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;
}
@@ -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<JobParameterDTO> 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<JobParameterDTO> 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);
}
}
@@ -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);
@@ -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<ContextClosedEvent> {
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<JobParameterDTO> 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<JobParameterDTO> 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<JobParameterDTO> 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();
}
@@ -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<JobParameterProvider> 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<JobParameterDTO> jobParameterDTOSet) throws JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException,
JobParametersInvalidException, JobRestartException {
ThreadLocalContextUtil.init(fineractContext);
Map<String, JobParameter> 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<String, JobParameter> provideCustomJobParameters(String jobName, Set<JobParameterDTO> jobParameterDTOSet) {
Optional<JobParameterProvider> jobParameterProvider = jobParameterProviders.stream()
.filter(provider -> provider.canProvideParametersForJob(jobName)).findFirst();
return jobParameterProvider.map(parameterProvider -> parameterProvider.provide(jobParameterDTOSet)).orElse(Collections.emptyMap());
}
}
@@ -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;
@@ -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();
}
@@ -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<String, JobParameter> provide(Set<JobParameterDTO> jobParameterDTOList);
boolean canProvideParametersForJob(String jobName);
}
@@ -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<String, JobParameter> provide(Set<JobParameterDTO> jobParameterDTOSet) {
Map<String, JobParameter> 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<JobParameterDTO> getJobParameterDTOListWithCorrectBusinessDate(Set<JobParameterDTO> jobParameterDTOset) {
Set<JobParameterDTO> jobParameterDTOListWithCorrectBusinessDate = jobParameterDTOset.isEmpty() ? new HashSet<>()
: new HashSet<>(jobParameterDTOset);
Optional<JobParameterDTO> 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;
}
}
@@ -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<String> LOAN_APPROVAL_DATA_PARAMETERS = new HashSet<>(Arrays.asList("approvalDate", "approvalAmount"));
private static final Set<String> GLIM_ACCOUNTS_DATA_PARAMETERS = new HashSet<>(Arrays.asList("glimId", "groupId", "clientId",
@@ -978,6 +978,7 @@ final class LoansApiResourceSwagger {
public Boolean fraud;
@Schema(example = "250.000000")
public Double totalOverpaid;
public LocalDate lastClosedBusinessDate;
}
@Schema(description = "GetLoansResponse")
@@ -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<LoanProductData> productOptions,
@@ -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;
}
}
@@ -71,6 +71,8 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, 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<Loan, Long>, JpaSpecificat
@Query(FIND_ID_BY_EXTERNAL_ID)
Long findIdByExternalId(@Param("externalId") String externalId);
@Query(FIND_ALL_NON_CLOSED_ONE_DAY_BEHIND)
List<Long> findAllNonClosedLoanIdsOneDayBehind(@Param("last_closed_business_date") LocalDate businessDate);
}
@@ -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);
}
}
@@ -93,5 +93,5 @@
<include file="parts/0071_add_external_id_support_for_loan_transaction.xml" relativeToChangelogFile="true"/>
<include file="parts/0072_add_result_and status_to_command_source.xml" relativeToChangelogFile="true" />
<include file="parts/0073_add_result_status_code_to_command_source.xml" relativeToChangelogFile="true" />
<include file="parts/0074_add_last_cob_business_date_column_to_loan.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<changeSet author="fineract" id="1">
<addColumn tableName="m_loan">
<column name="last_closed_business_date" type="DATE"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -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<BusinessDateType, LocalDate> 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);
});
@@ -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);
@@ -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;
}
}
@@ -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"),
@@ -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<Map<String, String>> jobLastRunHistorySupplier(int jobId) {
return () -> {