[FINERACT-1678] Introducing synchronous COB execution for loan account changing APIs

This commit is contained in:
taskain7
2022-11-08 14:46:51 +01:00
committed by Arnold Gálovics
parent 70a0a3e2e9
commit 3951e69ad7
8 changed files with 91 additions and 53 deletions
@@ -30,4 +30,6 @@ public interface LoanAccountLockRepository extends JpaRepository<LoanAccountLock
void deleteByLoanIdInAndLockOwner(List<Long> loanIds, LockOwner lockOwner);
List<LoanAccountLock> findAllByLoanIdIn(List<Long> loanIds);
boolean existsByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner);
}
@@ -66,7 +66,7 @@ import org.springframework.transaction.support.TransactionTemplate;
@Service
@Slf4j
@RequiredArgsConstructor
public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService, InitializingBean {
public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService<Long>, InitializingBean {
private static final String JOB_EXECUTION_FAILED_MESSAGE = "Job execution failed for job with name: ";
@@ -91,11 +91,34 @@ public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService,
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public CommandProcessingResult executeInlineJob(JsonCommand command, String jobName) throws LoanAccountLockCannotBeOverruledException {
List<Long> loanIds = dataParser.parseExecution(command);
lockLoanAccounts(loanIds);
execute(loanIds, jobName);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).build();
}
@Override
public void execute(List<Long> loanIds, String jobName) {
lockLoanAccounts(loanIds);
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))).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 List<LoanAccountLock> getLoanAccountLocks(List<Long> loanIds) {
List<LoanAccountLock> loanAccountLocks = new ArrayList<>();
List<Long> alreadyLockedLoanIds = new ArrayList<>();
@@ -121,28 +144,6 @@ public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService,
return loanAccountLocks;
}
private void execute(List<Long> loanIds, String jobName) {
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))).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 Map<String, JobParameter> getJobParametersMap(List<Long> loanIds) {
// TODO: refactor for a more generic solution
String parameterJson = gson.toJson(loanIds);
@@ -26,4 +26,6 @@ public interface LoanAccountLockService {
List<LoanAccountLock> getLockedLoanAccountByPage(int page, int limit);
boolean isLoanHardLocked(Long loanId);
boolean isLoanSoftLocked(Long loanId);
}
@@ -19,9 +19,7 @@
package org.apache.fineract.cob.service;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.cob.domain.LoanAccountLock;
import org.apache.fineract.cob.domain.LoanAccountLockRepository;
import org.apache.fineract.cob.domain.LockOwner;
@@ -45,10 +43,12 @@ public class LoanAccountLockServiceImpl implements LoanAccountLockService {
@Override
public boolean isLoanHardLocked(Long loanId) {
Optional<LoanAccountLock> loanAccountLockOptional = loanAccountLockRepository.findById(loanId);
return loanAccountLockOptional //
.filter(loanAccountLock -> LockOwner.LOAN_COB_CHUNK_PROCESSING.equals(loanAccountLock.getLockOwner()) //
|| LockOwner.LOAN_INLINE_COB_PROCESSING.equals(loanAccountLock.getLockOwner())) //
.filter(loanAccountLock -> StringUtils.isBlank(loanAccountLock.getError())).isPresent();
return loanAccountLockRepository.existsByLoanIdAndLockOwner(loanId, LockOwner.LOAN_COB_CHUNK_PROCESSING) //
|| loanAccountLockRepository.existsByLoanIdAndLockOwner(loanId, LockOwner.LOAN_INLINE_COB_PROCESSING);
}
@Override
public boolean isLoanSoftLocked(Long loanId) {
return loanAccountLockRepository.existsByLoanIdAndLockOwner(loanId, LockOwner.LOAN_COB_PARTITIONING);
}
}
@@ -21,6 +21,7 @@ package org.apache.fineract.infrastructure.core.config;
import org.apache.fineract.infrastructure.instancemode.filter.FineractInstanceModeApiFilter;
import org.apache.fineract.infrastructure.jobs.filter.LoanCOBApiFilter;
import org.apache.fineract.infrastructure.security.filter.InsecureTwoFactorAuthenticationFilter;
import org.apache.fineract.infrastructure.security.filter.TenantAwareBasicAuthenticationFilter;
import org.apache.fineract.infrastructure.security.filter.TwoFactorAuthenticationFilter;
import org.apache.fineract.infrastructure.security.service.TenantAwareJpaPlatformUserDetailsService;
@@ -89,8 +90,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
.and() //
.addFilterAfter(fineractInstanceModeApiFilter, SecurityContextPersistenceFilter.class) //
.addFilterAfter(tenantAwareBasicAuthenticationFilter(), FineractInstanceModeApiFilter.class) //
.addFilterAfter(loanCOBApiFilter, TenantAwareBasicAuthenticationFilter.class) //
.addFilterAfter(twoFactorAuthenticationFilter, BasicAuthenticationFilter.class); //
.addFilterAfter(twoFactorAuthenticationFilter, BasicAuthenticationFilter.class) //
.addFilterAfter(loanCOBApiFilter, InsecureTwoFactorAuthenticationFilter.class);
if (serverProperties.getSsl().isEnabled()) {
http.requiresChannel(channel -> channel.antMatchers("/api/**").requiresSecure());
@@ -21,8 +21,8 @@ package org.apache.fineract.infrastructure.jobs.filter;
import com.google.common.base.Splitter;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
@@ -33,6 +33,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.cob.service.InlineLoanCOBExecutorServiceImpl;
import org.apache.fineract.cob.service.LoanAccountLockService;
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
@@ -51,12 +52,14 @@ public class LoanCOBApiFilter extends OncePerRequestFilter {
private final GLIMAccountInfoRepository glimAccountInfoRepository;
private final LoanAccountLockService loanAccountLockService;
private final PlatformSecurityContext context;
private final InlineLoanCOBExecutorServiceImpl inlineLoanCOBExecutorService;
private static final List<HttpMethod> HTTP_METHODS = List.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE);
private static final Function<String, Boolean> URL_FUNCTION = s -> s.matches("/loans/\\d+.*") || s.matches("/loans/glimAccount/\\d+.*");
private static final Integer LOAN_ID_INDEX_IN_URL = 2;
private static final Integer GLIM_ID_INDEX_IN_URL = 3;
private static final Integer GLIM_STRING_INDEX_IN_URL = 2;
private static final String JOB_NAME = "INLINE_LOAN_COB";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
@@ -67,33 +70,48 @@ public class LoanCOBApiFilter extends OncePerRequestFilter {
Iterable<String> split = Splitter.on('/').split(request.getPathInfo());
Supplier<Stream<String>> streamSupplier = () -> StreamSupport.stream(split.spliterator(), false);
boolean isGlim = isGlim(streamSupplier);
Long loanId = getLoanId(isGlim, streamSupplier);
if (isLoanLocked(loanId, isGlim)) {
reject(loanId, response);
Long loanIdFromRequest = getLoanId(isGlim, streamSupplier);
List<Long> loanIds = isGlim ? getGlimChildLoanIds(loanIdFromRequest) : Collections.singletonList(loanIdFromRequest);
if (isLoanHardLocked(loanIds)) {
reject(loanIdFromRequest, response);
} else if (isLoanSoftLocked(loanIds)) {
executeInlineCob(loanIds);
proceed(filterChain, request, response);
} else {
proceed(filterChain, request, response);
}
}
}
private void executeInlineCob(List<Long> loanIds) {
inlineLoanCOBExecutorService.execute(loanIds, JOB_NAME);
}
private List<Long> getGlimChildLoanIds(Long loanIdFromRequest) {
GroupLoanIndividualMonitoringAccount glimAccount = glimAccountInfoRepository.findOneByIsAcceptingChildAndApplicationId(true,
BigDecimal.valueOf(loanIdFromRequest));
if (glimAccount != null) {
return glimAccount.getChildLoan().stream().map(Loan::getId).toList();
} else {
return Collections.emptyList();
}
}
private boolean isLoanSoftLocked(List<Long> loanIds) {
return isLoanLocked(loanIds, false);
}
private boolean isLoanHardLocked(List<Long> loanIds) {
return isLoanLocked(loanIds, true);
}
private boolean isBypassUser() {
return context.getAuthenticatedUserIfPresent().isBypassUser();
}
private boolean isLoanLocked(Long loanId, boolean isGlim) {
if (!isGlim) {
return loanAccountLockService.isLoanHardLocked(loanId);
} else {
GroupLoanIndividualMonitoringAccount glimAccount = glimAccountInfoRepository.findOneByIsAcceptingChildAndApplicationId(true,
BigDecimal.valueOf(loanId));
if (glimAccount != null) {
Set<Loan> loans = glimAccount.getChildLoan();
List<Long> loanIds = loans.stream().map(Loan::getId).toList();
return loanIds.stream().anyMatch(loanAccountLockService::isLoanHardLocked);
} else {
return false;
}
}
private boolean isLoanLocked(List<Long> loanIds, boolean isHardLock) {
return isHardLock ? loanIds.stream().anyMatch(loanAccountLockService::isLoanHardLocked)
: loanIds.stream().anyMatch(loanAccountLockService::isLoanSoftLocked);
}
private void proceed(FilterChain filterChain, HttpServletRequest request, HttpServletResponse response)
@@ -18,10 +18,18 @@
*/
package org.apache.fineract.infrastructure.jobs.service;
import java.util.Collections;
import java.util.List;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface InlineExecutorService {
public interface InlineExecutorService<T> {
CommandProcessingResult executeInlineJob(JsonCommand command, String jobName);
void execute(List<T> elements, String jobName);
default void execute(T element, String jobName) {
execute(Collections.singletonList(element), jobName);
}
}
@@ -30,6 +30,7 @@ import java.math.BigDecimal;
import java.util.Collections;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import org.apache.fineract.cob.service.InlineLoanCOBExecutorServiceImpl;
import org.apache.fineract.cob.service.LoanAccountLockService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository;
@@ -59,6 +60,8 @@ class LoanCOBApiFilterTest {
private GLIMAccountInfoRepository glimAccountInfoRepository;
@Mock
private PlatformSecurityContext context;
@Mock
private InlineLoanCOBExecutorServiceImpl inlineLoanCOBExecutorService;
@Test
void shouldProceedWhenUrlDoesNotMatch() throws ServletException, IOException {
@@ -99,6 +102,7 @@ class LoanCOBApiFilterTest {
given(request.getPathInfo()).willReturn("/loans/2/charges");
given(request.getMethod()).willReturn(HTTPMethods.POST.value());
given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false);
given(loanAccountLockService.isLoanSoftLocked(2L)).willReturn(false);
given(context.getAuthenticatedUserIfPresent()).willReturn(appUser);
testObj.doFilterInternal(request, response, filterChain);
@@ -106,7 +110,7 @@ class LoanCOBApiFilterTest {
}
@Test
void shouldProceedWhenLoanIsSoftLocked() throws ServletException, IOException {
void shouldRunInlineCOBAndProceedWhenLoanIsSoftLocked() throws ServletException, IOException {
MockHttpServletRequest request = mock(MockHttpServletRequest.class);
MockHttpServletResponse response = mock(MockHttpServletResponse.class);
FilterChain filterChain = mock(FilterChain.class);
@@ -115,9 +119,11 @@ class LoanCOBApiFilterTest {
given(request.getPathInfo()).willReturn("/loans/2/charges");
given(request.getMethod()).willReturn(HTTPMethods.POST.value());
given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false);
given(loanAccountLockService.isLoanSoftLocked(2L)).willReturn(true);
given(context.getAuthenticatedUserIfPresent()).willReturn(appUser);
testObj.doFilterInternal(request, response, filterChain);
verify(inlineLoanCOBExecutorService, times(1)).execute(Collections.singletonList(2L), "INLINE_LOAN_COB");
verify(filterChain, times(1)).doFilter(request, response);
}