blob: cd23952bc72f564ba0ec2524f02f5c9decabdfb1 [file] [log] [blame]
/**
* 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.service;
import static org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW;
import com.google.common.collect.Lists;
import com.google.gson.Gson;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition;
import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate;
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.cob.loan.RetrieveLoanIdService;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
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.exception.PlatformRequestBodyItemLimitValidationException;
import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.jobs.data.JobParameterDTO;
import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameterRepository;
import org.apache.fineract.infrastructure.jobs.exception.JobNotFoundException;
import org.apache.fineract.infrastructure.jobs.service.InlineExecutorService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.springbatch.SpringBatchJobConstants;
import org.jetbrains.annotations.NotNull;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameter;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.configuration.JobLocator;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.launch.NoSuchJobException;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
@Service
@Slf4j
@RequiredArgsConstructor
@Conditional(LoanCOBEnabledCondition.class)
public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService<Long> {
private static final String JOB_EXECUTION_FAILED_MESSAGE = "Job execution failed for job with name: ";
private final LoanAccountLockRepository loanAccountLockRepository;
private final InlineLoanCOBExecutionDataParser dataParser;
private final JobLauncher jobLauncher;
private final JobLocator jobLocator;
private final JobExplorer jobExplorer;
private final TransactionTemplate transactionTemplate;
private final CustomJobParameterRepository customJobParameterRepository;
private final PlatformSecurityContext context;
private final RetrieveLoanIdService retrieveLoanIdService;
private final FineractProperties fineractProperties;
private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson();
@Override
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public CommandProcessingResult executeInlineJob(JsonCommand command, String jobName) throws LoanAccountLockCannotBeOverruledException {
List<Long> loanIds = dataParser.parseExecution(command);
validateLoanIdsListSize(loanIds);
execute(loanIds, jobName);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).build();
}
@Override
public void execute(List<Long> loanIds, String jobName) {
LocalDate cobBusinessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE);
List<LoanIdAndLastClosedBusinessDate> loansToBeProcessed = getLoansToBeProcessed(loanIds, cobBusinessDate);
LocalDate executingBusinessDate = getOldestCOBBusinessDate(loansToBeProcessed).plusDays(1);
if (!loansToBeProcessed.isEmpty()) {
while (!DateUtils.isAfter(executingBusinessDate, cobBusinessDate)) {
execute(getLoanIdsToBeProcessed(loansToBeProcessed, executingBusinessDate), jobName, executingBusinessDate);
executingBusinessDate = executingBusinessDate.plusDays(1);
}
}
}
private List<Long> getLoanIdsToBeProcessed(List<LoanIdAndLastClosedBusinessDate> loansToBeProcessed, LocalDate executingBusinessDate) {
List<Long> loanIdsToBeProcessed = new ArrayList<>();
loansToBeProcessed.forEach(loan -> {
if (loan.getLastClosedBusinessDate() != null) {
if (DateUtils.isBefore(loan.getLastClosedBusinessDate(), executingBusinessDate)) {
loanIdsToBeProcessed.add(loan.getId());
}
} else {
loanIdsToBeProcessed.add(loan.getId());
}
});
return loanIdsToBeProcessed;
}
@SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT")
private void execute(List<Long> loanIds, String jobName, LocalDate businessDate) {
lockLoanAccounts(loanIds, businessDate);
Job inlineLoanCOBJob;
try {
inlineLoanCOBJob = jobLocator.getJob(jobName);
} catch (NoSuchJobException e) {
throw new JobNotFoundException(jobName, e);
}
JobParameters jobParameters = new JobParametersBuilder(jobExplorer).getNextJobParameters(inlineLoanCOBJob)
.addJobParameters(new JobParameters(getJobParametersMap(loanIds, businessDate))).toJobParameters();
JobExecution jobExecution;
try {
jobExecution = jobLauncher.run(inlineLoanCOBJob, jobParameters);
} catch (Exception e) {
log.error("{}{}", JOB_EXECUTION_FAILED_MESSAGE, jobName, e);
throw new PlatformInternalServerException("error.msg.sheduler.job.execution.failed", JOB_EXECUTION_FAILED_MESSAGE, jobName, e);
}
if (!BatchStatus.COMPLETED.equals(jobExecution.getStatus())) {
log.error("{}{}", JOB_EXECUTION_FAILED_MESSAGE, jobName);
throw new PlatformInternalServerException("error.msg.sheduler.job.execution.failed", JOB_EXECUTION_FAILED_MESSAGE, jobName);
}
}
private LocalDate getOldestCOBBusinessDate(List<LoanIdAndLastClosedBusinessDate> loans) {
LoanIdAndLastClosedBusinessDate oldestLoan = loans.stream().min(Comparator
.comparing(LoanIdAndLastClosedBusinessDate::getLastClosedBusinessDate, Comparator.nullsLast(Comparator.naturalOrder())))
.orElse(null);
return oldestLoan != null && oldestLoan.getLastClosedBusinessDate() != null ? oldestLoan.getLastClosedBusinessDate()
: ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE).minusDays(1);
}
private List<LoanIdAndLastClosedBusinessDate> getLoansToBeProcessed(List<Long> loanIds, LocalDate cobBusinessDate) {
List<LoanIdAndLastClosedBusinessDate> loanIdAndLastClosedBusinessDates = new ArrayList<>();
List<List<Long>> partitions = Lists.partition(loanIds, fineractProperties.getQuery().getInClauseParameterSizeLimit());
partitions.forEach(partition -> loanIdAndLastClosedBusinessDates
.addAll(retrieveLoanIdService.retrieveLoanIdsBehindDateOrNull(cobBusinessDate, partition)));
return loanIdAndLastClosedBusinessDates;
}
private List<LoanAccountLock> getLoanAccountLocks(List<Long> loanIds, LocalDate businessDate) {
List<LoanAccountLock> loanAccountLocks = new ArrayList<>();
List<Long> alreadyLockedLoanIds = new ArrayList<>();
loanIds.forEach(loanId -> {
Optional<LoanAccountLock> loanLockOptional = loanAccountLockRepository.findById(loanId);
if (loanLockOptional.isPresent()) {
LoanAccountLock loanAccountLock = loanLockOptional.get();
if (isLockOverrulable(loanAccountLock)) {
loanAccountLocks.add(loanAccountLock);
} else {
alreadyLockedLoanIds.add(loanId);
}
} else {
loanAccountLocks.add(new LoanAccountLock(loanId, LockOwner.LOAN_INLINE_COB_PROCESSING, businessDate));
}
});
if (!alreadyLockedLoanIds.isEmpty()) {
String message = "There is a hard lock on the loan account without any error, so it can't be overruled.";
String loanIdsMessage = " Locked loan IDs: " + alreadyLockedLoanIds;
throw new LoanAccountLockCannotBeOverruledException(message + loanIdsMessage);
}
return loanAccountLocks;
}
private Map<String, JobParameter<?>> getJobParametersMap(List<Long> loanIds, LocalDate businessDate) {
// TODO: refactor for a more generic solution
String parameterJson = gson.toJson(loanIds);
JobParameterDTO loanIdsParameterDTO = new JobParameterDTO(LoanCOBConstant.LOAN_IDS_PARAMETER_NAME, parameterJson);
Set<JobParameterDTO> loanIdJobParameter = Collections.singleton(loanIdsParameterDTO);
Long loanIdsJobParameterId = customJobParameterRepository.save(loanIdJobParameter);
JobParameterDTO businessDateParameterDTO = new JobParameterDTO(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME,
businessDate.format(DateTimeFormatter.ISO_DATE));
Set<JobParameterDTO> businessDateJobParameter = Collections.singleton(businessDateParameterDTO);
Long businessDateJobParameterId = customJobParameterRepository.save(businessDateJobParameter);
Map<String, JobParameter<?>> jobParameterMap = new HashMap<>();
jobParameterMap.put(SpringBatchJobConstants.CUSTOM_JOB_PARAMETER_ID_KEY, new JobParameter<>(loanIdsJobParameterId, Long.class));
jobParameterMap.put(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME, new JobParameter<>(businessDateJobParameterId, Long.class));
return jobParameterMap;
}
private void lockLoanAccounts(List<Long> loanIds, LocalDate businessDate) {
transactionTemplate.setPropagationBehavior(PROPAGATION_REQUIRES_NEW);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(@NotNull TransactionStatus status) {
List<LoanAccountLock> loanAccountLocks = getLoanAccountLocks(loanIds, businessDate);
loanAccountLocks.forEach(loanAccountLock -> {
try {
loanAccountLock.setNewLockOwner(LockOwner.LOAN_INLINE_COB_PROCESSING);
loanAccountLockRepository.saveAndFlush(loanAccountLock);
} catch (Exception e) {
log.error("Error updating lock on loan account. Locked loan ID: {}", loanAccountLock.getLoanId(), e);
throw new LoanAccountLockCannotBeOverruledException(
"Error updating lock on loan account. Locked loan ID: %s".formatted(loanAccountLock.getLoanId()), e);
}
});
}
});
}
private boolean isLockOverrulable(LoanAccountLock loanAccountLock) {
if (isBypassUser()) {
return true;
} else {
return StringUtils.isNotBlank(loanAccountLock.getError());
}
}
private boolean isBypassUser() {
return context.getAuthenticatedUserIfPresent().isBypassUser();
}
private void validateLoanIdsListSize(List<Long> loanIds) {
int inlineLoanCobRequestItemLimit = fineractProperties.getApi().getBodyItemSizeLimit().getInlineLoanCob();
if (loanIds.size() > inlineLoanCobRequestItemLimit) {
String userMessage = "Size of the loan IDs list cannot be over " + inlineLoanCobRequestItemLimit;
throw new PlatformRequestBodyItemLimitValidationException(userMessage);
}
}
}