[FINERACT-1678] Reject write API calls for hard-locked loan accounts
This commit is contained in:
committed by
Arnold Gálovics
parent
e76adfbe56
commit
7be193dd0b
+2
@@ -24,4 +24,6 @@ import org.apache.fineract.cob.domain.LoanAccountLock;
|
||||
public interface LoanAccountLockService {
|
||||
|
||||
List<LoanAccountLock> getLockedLoanAccountByPage(int page, int limit);
|
||||
|
||||
boolean isLoanHardLocked(Long loanId);
|
||||
}
|
||||
|
||||
+12
@@ -19,9 +19,12 @@
|
||||
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;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -39,4 +42,13 @@ public class LoanAccountLockServiceImpl implements LoanAccountLockService {
|
||||
Page<LoanAccountLock> loanAccountLocks = loanAccountLockRepository.findAll(loanAccountLockPage);
|
||||
return loanAccountLocks.getContent();
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -20,6 +20,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.TenantAwareBasicAuthenticationFilter;
|
||||
import org.apache.fineract.infrastructure.security.filter.TwoFactorAuthenticationFilter;
|
||||
import org.apache.fineract.infrastructure.security.service.TenantAwareJpaPlatformUserDetailsService;
|
||||
@@ -56,6 +57,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Autowired
|
||||
private FineractInstanceModeApiFilter fineractInstanceModeApiFilter;
|
||||
@Autowired
|
||||
private LoanCOBApiFilter loanCOBApiFilter;
|
||||
|
||||
@Autowired
|
||||
private FineractProperties fineractProperties;
|
||||
@@ -86,6 +89,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
.and() //
|
||||
.addFilterAfter(fineractInstanceModeApiFilter, SecurityContextPersistenceFilter.class) //
|
||||
.addFilterAfter(tenantAwareBasicAuthenticationFilter(), FineractInstanceModeApiFilter.class) //
|
||||
.addFilterAfter(loanCOBApiFilter, TenantAwareBasicAuthenticationFilter.class) //
|
||||
.addFilterAfter(twoFactorAuthenticationFilter, BasicAuthenticationFilter.class); //
|
||||
|
||||
if (serverProperties.getSsl().isEnabled()) {
|
||||
|
||||
+10
@@ -89,6 +89,16 @@ public class ApiGlobalErrorResponse {
|
||||
return globalErrorResponse;
|
||||
}
|
||||
|
||||
public static ApiGlobalErrorResponse loanIsLocked(final Long loanId) {
|
||||
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
|
||||
globalErrorResponse.setHttpStatusCode(Status.CONFLICT.toString());
|
||||
globalErrorResponse.setDeveloperMessage("Loan is locked by the COB job. Loan ID: " + loanId);
|
||||
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.loan.locked");
|
||||
globalErrorResponse.setDefaultUserMessage("Loan is locked by the COB job. Loan ID: \" + loanId");
|
||||
|
||||
return globalErrorResponse;
|
||||
}
|
||||
|
||||
public static ApiGlobalErrorResponse unAuthorized(final String defaultUserMessage) {
|
||||
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
|
||||
globalErrorResponse.setHttpStatusCode("403");
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 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.filter;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.sun.research.ws.wadl.HTTPMethods;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
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.LoanAccountLockService;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class LoanCOBApiFilter extends OncePerRequestFilter {
|
||||
|
||||
private final GLIMAccountInfoRepository glimAccountInfoRepository;
|
||||
private final LoanAccountLockService loanAccountLockService;
|
||||
|
||||
private static final List<HTTPMethods> HTTP_METHODS = List.of(HTTPMethods.POST, HTTPMethods.PUT, HTTPMethods.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;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
if (!isOnApiList(request)) {
|
||||
proceed(filterChain, request, response);
|
||||
} else {
|
||||
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);
|
||||
} else {
|
||||
proceed(filterChain, request, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 void proceed(FilterChain filterChain, HttpServletRequest request, HttpServletResponse response)
|
||||
throws IOException, ServletException {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private void reject(Long loanId, HttpServletResponse response) throws IOException {
|
||||
response.setStatus(HttpStatus.SC_CONFLICT);
|
||||
ApiGlobalErrorResponse errorResponse = ApiGlobalErrorResponse.loanIsLocked(loanId);
|
||||
response.getWriter().write(errorResponse.toJson());
|
||||
}
|
||||
|
||||
private Long getLoanId(boolean isGlim, Supplier<Stream<String>> streamSupplier) {
|
||||
if (!isGlim) {
|
||||
if (streamSupplier.get().count() >= LOAN_ID_INDEX_IN_URL + 1) {
|
||||
return Long.valueOf(streamSupplier.get().skip(LOAN_ID_INDEX_IN_URL).findFirst().get());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
if (streamSupplier.get().count() >= GLIM_ID_INDEX_IN_URL + 1) {
|
||||
return Long.valueOf(streamSupplier.get().skip(GLIM_ID_INDEX_IN_URL).findFirst().get());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isOnApiList(HttpServletRequest request) {
|
||||
if (StringUtils.isBlank(request.getPathInfo())) {
|
||||
return false;
|
||||
}
|
||||
return HTTP_METHODS.contains(HTTPMethods.fromValue(request.getMethod())) && URL_FUNCTION.apply(request.getPathInfo());
|
||||
}
|
||||
|
||||
private boolean isGlim(Supplier<Stream<String>> streamSupplier) {
|
||||
if (streamSupplier.get().count() >= GLIM_STRING_INDEX_IN_URL + 1) {
|
||||
return "glimAccount".equals(streamSupplier.get().skip(GLIM_STRING_INDEX_IN_URL).findFirst().get());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+21
-8
@@ -39,6 +39,9 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
|
||||
import org.apache.fineract.cob.domain.LoanAccountLockRepository;
|
||||
import org.apache.fineract.cob.exceptions.LoanAccountLockCannotBeOverruledException;
|
||||
import org.apache.fineract.cob.service.LoanAccountLockService;
|
||||
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
||||
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||
@@ -278,6 +281,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
private final PostDatedChecksRepository postDatedChecksRepository;
|
||||
private final LoanDisbursementDetailsRepository loanDisbursementDetailsRepository;
|
||||
private final LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository;
|
||||
private final LoanAccountLockRepository loanAccountLockRepository;
|
||||
private final LoanAccountLockService loanAccountLockService;
|
||||
|
||||
private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) {
|
||||
return e.getFromDate().isBefore(loanCharge.getDueDate()) && !loanCharge.getDueDate().isAfter(e.getDueDate());
|
||||
@@ -2551,20 +2556,28 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
|
||||
final Staff fromLoanOfficer = this.loanAssembler.findLoanOfficerByIdIfProvided(fromLoanOfficerId);
|
||||
final Staff toLoanOfficer = this.loanAssembler.findLoanOfficerByIdIfProvided(toLoanOfficerId);
|
||||
List<Long> lockedLoanIds = new ArrayList<>();
|
||||
|
||||
for (final String loanIdString : loanIds) {
|
||||
final Long loanId = Long.valueOf(loanIdString);
|
||||
final Loan loan = this.loanAssembler.assembleFrom(loanId);
|
||||
businessEventNotifierService.notifyPreBusinessEvent(new LoanReassignOfficerBusinessEvent(loan));
|
||||
checkClientOrGroupActive(loan);
|
||||
if (loanAccountLockService.isLoanHardLocked(loanId)) {
|
||||
lockedLoanIds.add(loanId);
|
||||
} else {
|
||||
businessEventNotifierService.notifyPreBusinessEvent(new LoanReassignOfficerBusinessEvent(loan));
|
||||
checkClientOrGroupActive(loan);
|
||||
|
||||
if (!loan.hasLoanOfficer(fromLoanOfficer)) {
|
||||
throw new LoanOfficerAssignmentException(loanId, fromLoanOfficerId);
|
||||
if (!loan.hasLoanOfficer(fromLoanOfficer)) {
|
||||
throw new LoanOfficerAssignmentException(loanId, fromLoanOfficerId);
|
||||
}
|
||||
|
||||
loan.reassignLoanOfficer(toLoanOfficer, dateOfLoanOfficerAssignment);
|
||||
saveLoanWithDataIntegrityViolationChecks(loan);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanReassignOfficerBusinessEvent(loan));
|
||||
}
|
||||
|
||||
loan.reassignLoanOfficer(toLoanOfficer, dateOfLoanOfficerAssignment);
|
||||
saveLoanWithDataIntegrityViolationChecks(loan);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanReassignOfficerBusinessEvent(loan));
|
||||
}
|
||||
if (!lockedLoanIds.isEmpty()) {
|
||||
throw new LoanAccountLockCannotBeOverruledException("There are hard-lcoked loan accounts: " + lockedLoanIds);
|
||||
}
|
||||
this.loanRepositoryWrapper.flush();
|
||||
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 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.filter;
|
||||
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import com.sun.research.ws.wadl.HTTPMethods;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collections;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import org.apache.fineract.cob.service.LoanAccountLockService;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class LoanCOBApiFilterTest {
|
||||
|
||||
@InjectMocks
|
||||
private LoanCOBApiFilter testObj;
|
||||
@Mock
|
||||
private LoanAccountLockService loanAccountLockService;
|
||||
@Mock
|
||||
private GLIMAccountInfoRepository glimAccountInfoRepository;
|
||||
|
||||
@Test
|
||||
void shouldProceedWhenUrlDoesNotMatch() throws ServletException, IOException {
|
||||
MockHttpServletRequest request = mock(MockHttpServletRequest.class);
|
||||
given(request.getPathInfo()).willReturn("/jobs/2/inline");
|
||||
given(request.getMethod()).willReturn(HTTPMethods.POST.value());
|
||||
MockHttpServletResponse response = mock(MockHttpServletResponse.class);
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
testObj.doFilterInternal(request, response, filterChain);
|
||||
verify(filterChain, times(1)).doFilter(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProceedWhenLoanIsNotLocked() throws ServletException, IOException {
|
||||
MockHttpServletRequest request = mock(MockHttpServletRequest.class);
|
||||
MockHttpServletResponse response = mock(MockHttpServletResponse.class);
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
given(request.getPathInfo()).willReturn("/loans/2/charges");
|
||||
given(request.getMethod()).willReturn(HTTPMethods.POST.value());
|
||||
given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false);
|
||||
|
||||
testObj.doFilterInternal(request, response, filterChain);
|
||||
verify(filterChain, times(1)).doFilter(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProceedWhenLoanIsSoftLocked() throws ServletException, IOException {
|
||||
MockHttpServletRequest request = mock(MockHttpServletRequest.class);
|
||||
MockHttpServletResponse response = mock(MockHttpServletResponse.class);
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
given(request.getPathInfo()).willReturn("/loans/2/charges");
|
||||
given(request.getMethod()).willReturn(HTTPMethods.POST.value());
|
||||
given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false);
|
||||
|
||||
testObj.doFilterInternal(request, response, filterChain);
|
||||
verify(filterChain, times(1)).doFilter(request, response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectWhenLoanIsHardLocked() throws ServletException, IOException {
|
||||
MockHttpServletRequest request = mock(MockHttpServletRequest.class);
|
||||
MockHttpServletResponse response = mock(MockHttpServletResponse.class);
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
PrintWriter writer = mock(PrintWriter.class);
|
||||
|
||||
given(request.getPathInfo()).willReturn("/loans/2/charges");
|
||||
given(request.getMethod()).willReturn(HTTPMethods.POST.value());
|
||||
given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(true);
|
||||
given(response.getWriter()).willReturn(writer);
|
||||
|
||||
testObj.doFilterInternal(request, response, filterChain);
|
||||
verify(response, times(1)).setStatus(HttpStatus.SC_CONFLICT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectWhenGlimLoanIsHardLocked() throws ServletException, IOException {
|
||||
MockHttpServletRequest request = mock(MockHttpServletRequest.class);
|
||||
MockHttpServletResponse response = mock(MockHttpServletResponse.class);
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
PrintWriter writer = mock(PrintWriter.class);
|
||||
GroupLoanIndividualMonitoringAccount glimAccount = mock(GroupLoanIndividualMonitoringAccount.class);
|
||||
Loan loan = mock(Loan.class);
|
||||
Long loanId = 2L;
|
||||
|
||||
given(request.getPathInfo()).willReturn("/loans/glimAccount/2");
|
||||
given(request.getMethod()).willReturn(HTTPMethods.POST.value());
|
||||
given(glimAccountInfoRepository.findOneByIsAcceptingChildAndApplicationId(true, BigDecimal.valueOf(2))).willReturn(glimAccount);
|
||||
given(glimAccount.getChildLoan()).willReturn(Collections.singleton(loan));
|
||||
given(loan.getId()).willReturn(loanId);
|
||||
given(loanAccountLockService.isLoanHardLocked(loanId)).willReturn(true);
|
||||
given(response.getWriter()).willReturn(writer);
|
||||
|
||||
testObj.doFilterInternal(request, response, filterChain);
|
||||
verify(response, times(1)).setStatus(HttpStatus.SC_CONFLICT);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user