[FINERACT-1678] Introducing synchronous COB execution for loan account changing APIs
This commit is contained in:
committed by
Arnold Gálovics
parent
70a0a3e2e9
commit
3951e69ad7
+2
@@ -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);
|
||||
}
|
||||
|
||||
+25
-24
@@ -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);
|
||||
|
||||
+2
@@ -26,4 +26,6 @@ public interface LoanAccountLockService {
|
||||
List<LoanAccountLock> getLockedLoanAccountByPage(int page, int limit);
|
||||
|
||||
boolean isLoanHardLocked(Long loanId);
|
||||
|
||||
boolean isLoanSoftLocked(Long loanId);
|
||||
}
|
||||
|
||||
+7
-7
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -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());
|
||||
|
||||
+36
-18
@@ -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)
|
||||
|
||||
+9
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-1
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user