FINERACT-1971: Handling inline COB execution before batch API execution outside of the batch transaction

This commit is contained in:
Arnold Galovics
2024-02-06 16:00:17 +01:00
committed by Arnold Gálovics
parent 99352c66b2
commit 770719e8ec
11 changed files with 484 additions and 121 deletions
@@ -0,0 +1,88 @@
/**
* 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.core.http;
import static java.nio.charset.StandardCharsets.UTF_8;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.apache.commons.io.IOUtils;
public class BodyCachingHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] cachedBody;
@SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW")
public BodyCachingHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.cachedBody = IOUtils.toByteArray(request.getInputStream());
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new CachedBodyServletInputStream(cachedBody);
}
@Override
public BufferedReader getReader() throws IOException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody);
return new BufferedReader(new InputStreamReader(byteArrayInputStream, UTF_8));
}
public static class CachedBodyServletInputStream extends ServletInputStream {
private final InputStream inputStream;
public CachedBodyServletInputStream(byte[] cachedBody) {
this.inputStream = new ByteArrayInputStream(cachedBody);
}
@Override
public boolean isFinished() {
try {
return inputStream.available() == 0;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
throw new UnsupportedOperationException();
}
@Override
public int read() throws IOException {
return inputStream.read();
}
}
}
@@ -27,6 +27,7 @@ import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition; import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition;
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse; import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
import org.apache.fineract.infrastructure.core.http.BodyCachingHttpServletRequestWrapper;
import org.apache.fineract.infrastructure.jobs.exception.LoanIdsHardLockedException; import org.apache.fineract.infrastructure.jobs.exception.LoanIdsHardLockedException;
import org.apache.fineract.useradministration.exception.UnAuthenticatedUserException; import org.apache.fineract.useradministration.exception.UnAuthenticatedUserException;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
@@ -62,7 +63,9 @@ public class LoanCOBApiFilter extends OncePerRequestFilter {
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException { throws ServletException, IOException {
if (!helper.isOnApiList(request.getPathInfo(), request.getMethod())) { request = new BodyCachingHttpServletRequestWrapper(request);
if (!helper.isOnApiList(request)) {
proceed(filterChain, request, response); proceed(filterChain, request, response);
} else { } else {
try { try {
@@ -71,7 +74,7 @@ public class LoanCOBApiFilter extends OncePerRequestFilter {
proceed(filterChain, request, response); proceed(filterChain, request, response);
} else { } else {
try { try {
List<Long> loanIds = helper.calculateRelevantLoanIds(request.getPathInfo()); List<Long> loanIds = helper.calculateRelevantLoanIds(request);
if (!loanIds.isEmpty() && helper.isLoanBehind(loanIds)) { if (!loanIds.isEmpty() && helper.isLoanBehind(loanIds)) {
helper.executeInlineCob(loanIds); helper.executeInlineCob(loanIds);
} }
@@ -1,74 +0,0 @@
/**
* 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.apache.fineract.batch.command.CommandStrategyUtils.isRelativeUrlVersioned;
import io.github.resilience4j.core.functions.Either;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition;
import org.apache.fineract.infrastructure.core.filters.BatchRequestPreprocessor;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
@Component
@RequiredArgsConstructor
@Conditional(LoanCOBEnabledCondition.class)
public class LoanCOBBatchPreprocessor implements BatchRequestPreprocessor {
private final LoanCOBFilterHelper helper;
private final PlatformTransactionManager transactionManager;
@Override
public Either<RuntimeException, BatchRequest> preprocess(BatchRequest batchRequest) {
TransactionTemplate tr = new TransactionTemplate(transactionManager);
tr.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED);
return tr.execute(status -> {
try {
String method = batchRequest.getMethod();
String relativeUrl = "/" + batchRequest.getRelativeUrl();
if (!isRelativeUrlVersioned(batchRequest.getRelativeUrl())) {
// to support pre-versioned relative paths
relativeUrl = "/v1/" + batchRequest.getRelativeUrl();
}
if (helper.isOnApiList(relativeUrl, method)) {
boolean bypassUser = helper.isBypassUser();
if (!bypassUser) {
List<Long> result = helper.calculateRelevantLoanIds(relativeUrl);
if (!result.isEmpty() && helper.isLoanBehind(result)) {
helper.executeInlineCob(result);
}
}
}
} catch (LoanNotFoundException e) {
return Either.right(batchRequest);
} catch (RuntimeException e) {
return Either.left(e);
}
return Either.right(batchRequest);
});
}
}
@@ -18,7 +18,15 @@
*/ */
package org.apache.fineract.infrastructure.jobs.filter; package org.apache.fineract.infrastructure.jobs.filter;
import static org.apache.fineract.batch.command.CommandStrategyUtils.isRelativeUrlVersioned;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@@ -28,6 +36,7 @@ import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition; import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition;
import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate; import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate;
import org.apache.fineract.cob.loan.RetrieveLoanIdService; import org.apache.fineract.cob.loan.RetrieveLoanIdService;
@@ -62,6 +71,7 @@ public class LoanCOBFilterHelper {
private final RetrieveLoanIdService retrieveLoanIdService; private final RetrieveLoanIdService retrieveLoanIdService;
private final LoanRescheduleRequestRepository loanRescheduleRequestRepository; private final LoanRescheduleRequestRepository loanRescheduleRequestRepository;
private final ObjectMapper objectMapper;
private static final List<HttpMethod> HTTP_METHODS = List.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE); private static final List<HttpMethod> HTTP_METHODS = List.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE);
@@ -100,14 +110,51 @@ public class LoanCOBFilterHelper {
return LOAN_PATH_PATTERN.matcher(pathInfo).matches() && pathInfo.contains("/v1/rescheduleloans/"); return LOAN_PATH_PATTERN.matcher(pathInfo).matches() && pathInfo.contains("/v1/rescheduleloans/");
} }
public boolean isOnApiList(String pathInfo, String method) { public boolean isOnApiList(HttpServletRequest request) throws IOException {
String pathInfo = request.getPathInfo();
String method = request.getMethod();
if (StringUtils.isBlank(pathInfo)) { if (StringUtils.isBlank(pathInfo)) {
return false; return false;
} }
if (isBatchApi(pathInfo)) {
return isBatchApiMatching(request);
} else {
return isApiMatching(method, pathInfo);
}
}
private boolean isBatchApiMatching(HttpServletRequest request) throws IOException {
for (BatchRequest batchRequest : getBatchRequests(request)) {
String method = batchRequest.getMethod();
String pathInfo = batchRequest.getRelativeUrl();
if (isApiMatching(method, pathInfo)) {
return true;
}
}
return false;
}
private List<BatchRequest> getBatchRequests(HttpServletRequest request) throws IOException {
List<BatchRequest> batchRequests = objectMapper.readValue(request.getInputStream(), new TypeReference<>() {});
for (BatchRequest batchRequest : batchRequests) {
String pathInfo = "/" + batchRequest.getRelativeUrl();
if (!isRelativeUrlVersioned(batchRequest.getRelativeUrl())) {
pathInfo = "/v1/" + batchRequest.getRelativeUrl();
}
batchRequest.setRelativeUrl(pathInfo);
}
return batchRequests;
}
private boolean isApiMatching(String method, String pathInfo) {
return HTTP_METHODS.contains(HttpMethod.valueOf(method)) && !IGNORE_LOAN_PATH_PATTERN.matcher(pathInfo).find() return HTTP_METHODS.contains(HttpMethod.valueOf(method)) && !IGNORE_LOAN_PATH_PATTERN.matcher(pathInfo).find()
&& URL_FUNCTION.test(pathInfo); && URL_FUNCTION.test(pathInfo);
} }
private boolean isBatchApi(String pathInfo) {
return pathInfo.startsWith("/v1/batches");
}
private boolean isGlim(String pathInfo) { private boolean isGlim(String pathInfo) {
return LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(pathInfo).matches(); return LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(pathInfo).matches();
} }
@@ -138,8 +185,47 @@ public class LoanCOBFilterHelper {
return CollectionUtils.isNotEmpty(loanIdAndLastClosedBusinessDates); return CollectionUtils.isNotEmpty(loanIdAndLastClosedBusinessDates);
} }
public List<Long> calculateRelevantLoanIds(String pathInfo) { public List<Long> calculateRelevantLoanIds(HttpServletRequest request) throws IOException {
String pathInfo = request.getPathInfo();
if (isBatchApi(pathInfo)) {
return getLoanIdsFromBatchApi(request);
} else {
return getLoanIdsFromApi(pathInfo);
}
}
private List<Long> getLoanIdsFromBatchApi(HttpServletRequest request) throws IOException {
List<Long> loanIds = new ArrayList<>();
for (BatchRequest batchRequest : getBatchRequests(request)) {
// check the URL for Loan related ID
String relativeUrl = batchRequest.getRelativeUrl();
if (!relativeUrl.contains("$.resourceId")) {
// if resourceId reference is used, we simply don't know the resourceId without executing the requests
// first, so skipping it
loanIds.addAll(getLoanIdsFromApi(relativeUrl));
}
// check the body for Loan ID
Long loanId = getTopLevelLoanIdFromBatchRequest(batchRequest);
if (loanId != null) {
loanIds.add(loanId);
}
}
return loanIds;
}
private Long getTopLevelLoanIdFromBatchRequest(BatchRequest batchRequest) throws JsonProcessingException {
String body = batchRequest.getBody();
if (StringUtils.isNotBlank(body)) {
JsonNode jsonNode = objectMapper.readTree(body);
if (jsonNode.has("loanId")) {
return jsonNode.get("loanId").asLong();
}
}
return null;
}
private List<Long> getLoanIdsFromApi(String pathInfo) {
List<Long> loanIds = getLoanIdList(pathInfo); List<Long> loanIds = getLoanIdList(pathInfo);
if (isLoanHardLocked(loanIds)) { if (isLoanHardLocked(loanIds)) {
throw new LoanIdsHardLockedException(loanIds.get(0)); throw new LoanIdsHardLockedException(loanIds.get(0));
@@ -31,6 +31,7 @@ import static org.mockito.Mockito.verify;
import com.sun.research.ws.wadl.HTTPMethods; import com.sun.research.ws.wadl.HTTPMethods;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -47,6 +48,7 @@ import org.apache.fineract.cob.service.LoanAccountLockService;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.http.BodyCachingHttpServletRequestWrapper;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository; import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository;
@@ -142,9 +144,10 @@ class LoanCOBApiFilterTest {
given(request.getPathInfo()).willReturn("/v1/jobs/2/inline"); given(request.getPathInfo()).willReturn("/v1/jobs/2/inline");
given(request.getMethod()).willReturn(HTTPMethods.POST.value()); given(request.getMethod()).willReturn(HTTPMethods.POST.value());
given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
testObj.doFilterInternal(request, response, filterChain); testObj.doFilterInternal(request, response, filterChain);
verify(filterChain, times(1)).doFilter(request, response); verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
} }
@Test @Test
@@ -162,6 +165,7 @@ class LoanCOBApiFilterTest {
given(request.getPathInfo()).willReturn("/v1/loans/invalid2LoanId/charges"); given(request.getPathInfo()).willReturn("/v1/loans/invalid2LoanId/charges");
given(request.getMethod()).willReturn(HTTPMethods.POST.value()); given(request.getMethod()).willReturn(HTTPMethods.POST.value());
given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
given(context.authenticatedUser()).willReturn(appUser); given(context.authenticatedUser()).willReturn(appUser);
given(fineractProperties.getQuery()).willReturn(fineractQueryProperties); given(fineractProperties.getQuery()).willReturn(fineractQueryProperties);
given(fineractQueryProperties.getInClauseParameterSizeLimit()).willReturn(65000); given(fineractQueryProperties.getInClauseParameterSizeLimit()).willReturn(65000);
@@ -169,7 +173,7 @@ class LoanCOBApiFilterTest {
anyList())).willReturn(Collections.emptyList()); anyList())).willReturn(Collections.emptyList());
testObj.doFilterInternal(request, response, filterChain); testObj.doFilterInternal(request, response, filterChain);
verify(filterChain, times(1)).doFilter(request, response); verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
} }
@Test @Test
@@ -181,11 +185,12 @@ class LoanCOBApiFilterTest {
given(request.getPathInfo()).willReturn("/v1/jobs/2/inline"); given(request.getPathInfo()).willReturn("/v1/jobs/2/inline");
given(request.getMethod()).willReturn(HTTPMethods.POST.value()); given(request.getMethod()).willReturn(HTTPMethods.POST.value());
given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
given(context.authenticatedUser()).willReturn(appUser); given(context.authenticatedUser()).willReturn(appUser);
given(appUser.isBypassUser()).willReturn(true); given(appUser.isBypassUser()).willReturn(true);
testObj.doFilterInternal(request, response, filterChain); testObj.doFilterInternal(request, response, filterChain);
verify(filterChain, times(1)).doFilter(request, response); verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
} }
@Test @Test
@@ -203,6 +208,7 @@ class LoanCOBApiFilterTest {
given(request.getPathInfo()).willReturn("/v1/loans/2/charges"); given(request.getPathInfo()).willReturn("/v1/loans/2/charges");
given(request.getMethod()).willReturn(HTTPMethods.POST.value()); given(request.getMethod()).willReturn(HTTPMethods.POST.value());
given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false); given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false);
given(context.authenticatedUser()).willReturn(appUser); given(context.authenticatedUser()).willReturn(appUser);
given(fineractProperties.getQuery()).willReturn(fineractQueryProperties); given(fineractProperties.getQuery()).willReturn(fineractQueryProperties);
@@ -211,7 +217,7 @@ class LoanCOBApiFilterTest {
anyList())).willReturn(Collections.emptyList()); anyList())).willReturn(Collections.emptyList());
testObj.doFilterInternal(request, response, filterChain); testObj.doFilterInternal(request, response, filterChain);
verify(filterChain, times(1)).doFilter(request, response); verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
} }
@Test @Test
@@ -229,6 +235,7 @@ class LoanCOBApiFilterTest {
String uuid = UUID.randomUUID().toString(); String uuid = UUID.randomUUID().toString();
given(request.getPathInfo()).willReturn("/v1/loans/external-id/" + uuid + "/charges"); given(request.getPathInfo()).willReturn("/v1/loans/external-id/" + uuid + "/charges");
given(request.getMethod()).willReturn(HTTPMethods.POST.value()); given(request.getMethod()).willReturn(HTTPMethods.POST.value());
given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false); given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false);
given(context.authenticatedUser()).willReturn(appUser); given(context.authenticatedUser()).willReturn(appUser);
given(loanRepository.findIdByExternalId(any())).willReturn(2L); given(loanRepository.findIdByExternalId(any())).willReturn(2L);
@@ -238,7 +245,7 @@ class LoanCOBApiFilterTest {
anyList())).willReturn(Collections.emptyList()); anyList())).willReturn(Collections.emptyList());
testObj.doFilterInternal(request, response, filterChain); testObj.doFilterInternal(request, response, filterChain);
verify(filterChain, times(1)).doFilter(request, response); verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
} }
@Test @Test
@@ -256,6 +263,7 @@ class LoanCOBApiFilterTest {
Long resourceId = 123L; Long resourceId = 123L;
given(request.getPathInfo()).willReturn("/v1/rescheduleloans/" + resourceId + "/charges"); given(request.getPathInfo()).willReturn("/v1/rescheduleloans/" + resourceId + "/charges");
given(request.getMethod()).willReturn(HTTPMethods.POST.value()); given(request.getMethod()).willReturn(HTTPMethods.POST.value());
given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false); given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false);
given(fineractProperties.getQuery()).willReturn(fineractQueryProperties); given(fineractProperties.getQuery()).willReturn(fineractQueryProperties);
given(fineractQueryProperties.getInClauseParameterSizeLimit()).willReturn(65000); given(fineractQueryProperties.getInClauseParameterSizeLimit()).willReturn(65000);
@@ -266,7 +274,7 @@ class LoanCOBApiFilterTest {
anyList())).willReturn(Collections.emptyList()); anyList())).willReturn(Collections.emptyList());
testObj.doFilterInternal(request, response, filterChain); testObj.doFilterInternal(request, response, filterChain);
verify(filterChain, times(1)).doFilter(request, response); verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
} }
@Test @Test
@@ -288,6 +296,7 @@ class LoanCOBApiFilterTest {
given(result.getLastClosedBusinessDate()).willReturn(businessDate.minusDays(2)); given(result.getLastClosedBusinessDate()).willReturn(businessDate.minusDays(2));
given(request.getPathInfo()).willReturn("/v1/loans/2?command=approve"); given(request.getPathInfo()).willReturn("/v1/loans/2?command=approve");
given(request.getMethod()).willReturn(HTTPMethods.POST.value()); given(request.getMethod()).willReturn(HTTPMethods.POST.value());
given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false); given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false);
given(fineractProperties.getQuery()).willReturn(fineractQueryProperties); given(fineractProperties.getQuery()).willReturn(fineractQueryProperties);
given(fineractQueryProperties.getInClauseParameterSizeLimit()).willReturn(65000); given(fineractQueryProperties.getInClauseParameterSizeLimit()).willReturn(65000);
@@ -297,7 +306,7 @@ class LoanCOBApiFilterTest {
testObj.doFilterInternal(request, response, filterChain); testObj.doFilterInternal(request, response, filterChain);
verify(inlineLoanCOBExecutorService, times(1)).execute(Collections.singletonList(2L), "INLINE_LOAN_COB"); verify(inlineLoanCOBExecutorService, times(1)).execute(Collections.singletonList(2L), "INLINE_LOAN_COB");
verify(filterChain, times(1)).doFilter(request, response); verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
} }
@Test @Test
@@ -318,6 +327,7 @@ class LoanCOBApiFilterTest {
given(result.getId()).willReturn(2L); given(result.getId()).willReturn(2L);
given(request.getPathInfo()).willReturn("/v1/loans/2?command=approve"); given(request.getPathInfo()).willReturn("/v1/loans/2?command=approve");
given(request.getMethod()).willReturn(HTTPMethods.POST.value()); given(request.getMethod()).willReturn(HTTPMethods.POST.value());
given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false); given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false);
given(fineractProperties.getQuery()).willReturn(fineractQueryProperties); given(fineractProperties.getQuery()).willReturn(fineractQueryProperties);
given(fineractQueryProperties.getInClauseParameterSizeLimit()).willReturn(65000); given(fineractQueryProperties.getInClauseParameterSizeLimit()).willReturn(65000);
@@ -328,7 +338,7 @@ class LoanCOBApiFilterTest {
testObj.doFilterInternal(request, response, filterChain); testObj.doFilterInternal(request, response, filterChain);
verify(inlineLoanCOBExecutorService, times(0)).execute(Collections.singletonList(2L), "INLINE_LOAN_COB"); verify(inlineLoanCOBExecutorService, times(0)).execute(Collections.singletonList(2L), "INLINE_LOAN_COB");
verify(filterChain, times(1)).doFilter(request, response); verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
} }
@Test @Test
@@ -340,12 +350,13 @@ class LoanCOBApiFilterTest {
given(request.getPathInfo()).willReturn("/v1/loans"); given(request.getPathInfo()).willReturn("/v1/loans");
given(request.getMethod()).willReturn(HTTPMethods.POST.value()); given(request.getMethod()).willReturn(HTTPMethods.POST.value());
given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
given(context.authenticatedUser()).willReturn(appUser); given(context.authenticatedUser()).willReturn(appUser);
testObj.doFilterInternal(request, response, filterChain); testObj.doFilterInternal(request, response, filterChain);
verify(inlineLoanCOBExecutorService, times(0)).execute(Collections.singletonList(2L), "INLINE_LOAN_COB"); verify(inlineLoanCOBExecutorService, times(0)).execute(Collections.singletonList(2L), "INLINE_LOAN_COB");
verify(filterChain, times(1)).doFilter(request, response); verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
} }
@Test @Test
@@ -357,12 +368,13 @@ class LoanCOBApiFilterTest {
given(request.getPathInfo()).willReturn("/v1/loans/catch-up"); given(request.getPathInfo()).willReturn("/v1/loans/catch-up");
given(request.getMethod()).willReturn(HTTPMethods.POST.value()); given(request.getMethod()).willReturn(HTTPMethods.POST.value());
given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
given(context.authenticatedUser()).willReturn(appUser); given(context.authenticatedUser()).willReturn(appUser);
testObj.doFilterInternal(request, response, filterChain); testObj.doFilterInternal(request, response, filterChain);
verify(inlineLoanCOBExecutorService, times(0)).execute(Collections.singletonList(2L), "INLINE_LOAN_COB"); verify(inlineLoanCOBExecutorService, times(0)).execute(Collections.singletonList(2L), "INLINE_LOAN_COB");
verify(filterChain, times(1)).doFilter(request, response); verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
} }
@Test @Test
@@ -375,6 +387,7 @@ class LoanCOBApiFilterTest {
given(request.getPathInfo()).willReturn("/v1/loans/2/charges"); given(request.getPathInfo()).willReturn("/v1/loans/2/charges");
given(request.getMethod()).willReturn(HTTPMethods.POST.value()); given(request.getMethod()).willReturn(HTTPMethods.POST.value());
given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(true); given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(true);
given(response.getWriter()).willReturn(writer); given(response.getWriter()).willReturn(writer);
given(context.authenticatedUser()).willReturn(appUser); given(context.authenticatedUser()).willReturn(appUser);
@@ -396,6 +409,7 @@ class LoanCOBApiFilterTest {
given(request.getPathInfo()).willReturn("/v1/loans/glimAccount/2"); given(request.getPathInfo()).willReturn("/v1/loans/glimAccount/2");
given(request.getMethod()).willReturn(HTTPMethods.POST.value()); given(request.getMethod()).willReturn(HTTPMethods.POST.value());
given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
given(glimAccountInfoRepository.findOneByIsAcceptingChildAndApplicationId(true, BigDecimal.valueOf(2))).willReturn(glimAccount); given(glimAccountInfoRepository.findOneByIsAcceptingChildAndApplicationId(true, BigDecimal.valueOf(2))).willReturn(glimAccount);
given(glimAccount.getChildLoan()).willReturn(Collections.singleton(loan)); given(glimAccount.getChildLoan()).willReturn(Collections.singleton(loan));
given(loan.getId()).willReturn(loanId); given(loan.getId()).willReturn(loanId);
@@ -29,11 +29,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import io.restassured.builder.RequestSpecBuilder; import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder; import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import io.restassured.internal.RequestSpecificationImpl;
import io.restassured.specification.RequestSpecification; import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification; import io.restassured.specification.ResponseSpecification;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -42,7 +44,10 @@ import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer; import java.util.function.Consumer;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.ToString; import lombok.ToString;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.client.models.AdvancedPaymentData; import org.apache.fineract.client.models.AdvancedPaymentData;
import org.apache.fineract.client.models.AllowAttributeOverrides; import org.apache.fineract.client.models.AllowAttributeOverrides;
import org.apache.fineract.client.models.BusinessDateRequest; import org.apache.fineract.client.models.BusinessDateRequest;
@@ -59,6 +64,7 @@ import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
import org.apache.fineract.client.models.PostLoansRequest; import org.apache.fineract.client.models.PostLoansRequest;
import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.client.util.CallFailedRuntimeException; import org.apache.fineract.client.util.CallFailedRuntimeException;
import org.apache.fineract.integrationtests.common.BatchHelper;
import org.apache.fineract.integrationtests.common.BusinessDateHelper; import org.apache.fineract.integrationtests.common.BusinessDateHelper;
import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
@@ -71,6 +77,8 @@ import org.apache.fineract.integrationtests.common.loans.LoanProductHelper;
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.integrationtests.inlinecob.InlineLoanCOBHelper;
import org.apache.fineract.integrationtests.useradministration.users.UserHelper;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
@@ -89,13 +97,18 @@ public abstract class BaseLoanIntegrationTest {
protected static final String DATETIME_PATTERN = "dd MMMM yyyy"; protected static final String DATETIME_PATTERN = "dd MMMM yyyy";
protected final ResponseSpecification responseSpec = createResponseSpecification(Matchers.is(200)); protected final ResponseSpecification responseSpec = createResponseSpecification(Matchers.is(200));
protected final RequestSpecification requestSpec = createRequestSpecification();
private final String fullAdminAuthKey = getFullAdminAuthKey();
protected final RequestSpecification requestSpec = createRequestSpecification(fullAdminAuthKey);
private final String nonByPassUserAuthKey = getNonByPassUserAuthKey(requestSpec, responseSpec);
protected final AccountHelper accountHelper = new AccountHelper(requestSpec, responseSpec); protected final AccountHelper accountHelper = new AccountHelper(requestSpec, responseSpec);
protected final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec); protected final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec);
protected final LoanProductHelper loanProductHelper = new LoanProductHelper(); protected final LoanProductHelper loanProductHelper = new LoanProductHelper();
protected JournalEntryHelper journalEntryHelper = new JournalEntryHelper(requestSpec, responseSpec); protected JournalEntryHelper journalEntryHelper = new JournalEntryHelper(requestSpec, responseSpec);
protected ClientHelper clientHelper = new ClientHelper(requestSpec, responseSpec); protected ClientHelper clientHelper = new ClientHelper(requestSpec, responseSpec);
protected final InlineLoanCOBHelper inlineLoanCOBHelper = new InlineLoanCOBHelper(requestSpec, responseSpec);
protected BusinessDateHelper businessDateHelper = new BusinessDateHelper(); protected BusinessDateHelper businessDateHelper = new BusinessDateHelper();
protected DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN); protected DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
@@ -119,6 +132,16 @@ public abstract class BaseLoanIntegrationTest {
protected final Account writtenOffAccount = accountHelper.createExpenseAccount(); protected final Account writtenOffAccount = accountHelper.createExpenseAccount();
protected final Account goodwillExpenseAccount = accountHelper.createExpenseAccount(); protected final Account goodwillExpenseAccount = accountHelper.createExpenseAccount();
private String getNonByPassUserAuthKey(RequestSpecification requestSpec, ResponseSpecification responseSpec) {
// creates the user
UserHelper.getSimpleUserWithoutBypassPermission(requestSpec, responseSpec);
return Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(UserHelper.SIMPLE_USER_NAME, UserHelper.SIMPLE_USER_PASSWORD);
}
private String getFullAdminAuthKey() {
return Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey();
}
// Loan product with proper accounting setup // Loan product with proper accounting setup
protected PostLoanProductsRequest createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() { protected PostLoanProductsRequest createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() {
return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6))// return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6))//
@@ -256,14 +279,14 @@ public abstract class BaseLoanIntegrationTest {
.amortizationType(amortizationType); .amortizationType(amortizationType);
} }
private static RequestSpecification createRequestSpecification() { private RequestSpecification createRequestSpecification(String authKey) {
RequestSpecification request = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); RequestSpecification requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
request.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); requestSpec.header("Authorization", "Basic " + authKey);
request.header("Fineract-Platform-TenantId", "default"); requestSpec.header("Fineract-Platform-TenantId", "default");
return request; return requestSpec;
} }
protected static ResponseSpecification createResponseSpecification(Matcher<Integer> statusCodeMatcher) { protected ResponseSpecification createResponseSpecification(Matcher<Integer> statusCodeMatcher) {
return new ResponseSpecBuilder().expectStatusCode(statusCodeMatcher).build(); return new ResponseSpecBuilder().expectStatusCode(statusCodeMatcher).build();
} }
@@ -317,6 +340,16 @@ public abstract class BaseLoanIntegrationTest {
} }
} }
protected void executeInlineCOB(Long loanId) {
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
}
protected void verifyLastClosedBusinessDate(Long loanId, String lastClosedBusinessDate) {
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
Assertions.assertNotNull(loanDetails.getLastClosedBusinessDate());
Assertions.assertEquals(lastClosedBusinessDate, loanDetails.getLastClosedBusinessDate().format(dateTimeFormatter));
}
protected void disburseLoan(Long loanId, BigDecimal amount, String date) { protected void disburseLoan(Long loanId, BigDecimal amount, String date) {
loanTransactionHelper.disburseLoan(loanId, new PostLoansLoanIdRequest().actualDisbursementDate(date).dateFormat(DATETIME_PATTERN) loanTransactionHelper.disburseLoan(loanId, new PostLoansLoanIdRequest().actualDisbursementDate(date).dateFormat(DATETIME_PATTERN)
.transactionAmount(amount).locale("en")); .transactionAmount(amount).locale("en"));
@@ -408,6 +441,16 @@ public abstract class BaseLoanIntegrationTest {
} }
} }
protected void runAsNonByPass(Runnable runnable) {
RequestSpecificationImpl requestSpecImpl = (RequestSpecificationImpl) requestSpec;
try {
requestSpecImpl.replaceHeader("Authorization", "Basic " + nonByPassUserAuthKey);
runnable.run();
} finally {
requestSpecImpl.replaceHeader("Authorization", "Basic " + fullAdminAuthKey);
}
}
protected PostLoansRequest applyLoanRequest(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount, protected PostLoansRequest applyLoanRequest(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount,
int numberOfRepayments) { int numberOfRepayments) {
return applyLoanRequest(clientId, loanProductId, loanDisbursementDate, amount, numberOfRepayments, null); return applyLoanRequest(clientId, loanProductId, loanDisbursementDate, amount, numberOfRepayments, null);
@@ -508,6 +551,69 @@ public abstract class BaseLoanIntegrationTest {
return new Installment(principalAmount, interestAmount, feeAmount, totalOutstandingAmount, completed, dueDate); return new Installment(principalAmount, interestAmount, feeAmount, totalOutstandingAmount, completed, dueDate);
} }
protected BatchRequestBuilder batchRequest() {
return new BatchRequestBuilder(requestSpec, responseSpec);
}
@RequiredArgsConstructor
public static class BatchRequestBuilder {
private final RequestSpecification requestSpec;
private final ResponseSpecification responseSpec;
private List<BatchRequest> requests = new ArrayList<>();
public BatchRequestBuilder rescheduleLoan(Long requestId, Long loanId, String submittedOnDate, String rescheduleFromDate,
String adjustedDueDate) {
BatchRequest bRequest = new BatchRequest();
bRequest.setRequestId(requestId);
bRequest.setRelativeUrl("rescheduleloans");
bRequest.setMethod("POST");
bRequest.setBody("""
{
"loanId": %d,
"rescheduleFromDate": "%s",
"rescheduleReasonId": 1,
"submittedOnDate": "%s",
"rescheduleReasonComment": "",
"adjustedDueDate": "%s",
"graceOnPrincipal": "",
"graceOnInterest": "",
"extraTerms": "",
"newInterestRate": "",
"dateFormat": "%s",
"locale": "en"
}
""".formatted(loanId, rescheduleFromDate, submittedOnDate, adjustedDueDate, DATETIME_PATTERN));
requests.add(bRequest);
return this;
}
public BatchRequestBuilder approveRescheduleLoan(Long requestId, Long rescheduleBatchRequestId, String approvedOnDate) {
BatchRequest bRequest = new BatchRequest();
bRequest.setRequestId(requestId);
bRequest.setRelativeUrl("rescheduleloans/$.resourceId?command=approve");
bRequest.setMethod("POST");
bRequest.setReference(rescheduleBatchRequestId);
bRequest.setBody("""
{
"approvedOnDate": "%s",
"dateFormat": "%s",
"locale": "en"
}
""".formatted(approvedOnDate, DATETIME_PATTERN));
requests.add(bRequest);
return this;
}
public List<BatchResponse> executeEnclosingTransaction() {
return BatchHelper.postBatchRequestsWithEnclosingTransaction(requestSpec, responseSpec, BatchHelper.toJsonString(requests));
}
}
@ToString @ToString
@AllArgsConstructor @AllArgsConstructor
public static class Transaction { public static class Transaction {
@@ -18,6 +18,7 @@
*/ */
package org.apache.fineract.integrationtests; package org.apache.fineract.integrationtests;
import static java.lang.Integer.parseInt;
import static org.apache.http.HttpStatus.SC_FORBIDDEN; import static org.apache.http.HttpStatus.SC_FORBIDDEN;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -54,6 +55,7 @@ import org.apache.fineract.integrationtests.common.CollateralManagementHelper;
import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.charges.ChargesHelper; import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
import org.apache.fineract.integrationtests.common.error.ErrorResponse;
import org.apache.fineract.integrationtests.common.loans.LoanAccountLockHelper; import org.apache.fineract.integrationtests.common.loans.LoanAccountLockHelper;
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
@@ -75,14 +77,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/**
* Test class for {@link org.apache.fineract.batch.command.CommandStrategyProvider}. This tests the response provided by
* commandStrategy by injecting it with a {@code BatchRequest}.
*
* @author RishabhShukla
* @see org.apache.fineract.integrationtests.common.BatchHelper
* @see org.apache.fineract.batch.domain.BatchRequest
*/
@ExtendWith(LoanTestLifecycleExtension.class) @ExtendWith(LoanTestLifecycleExtension.class)
public class BatchApiTest { public class BatchApiTest {
@@ -1197,8 +1191,8 @@ public class BatchApiTest {
} }
/** /**
* Test for the successful create client, creat, approve and get loan. A '200' status code is expected on successful * Test for the successful create client, create, approve and get loan. A '200' status code is expected on
* responses. * successful responses.
* *
* @see org.apache.fineract.batch.command.internal.CreateClientCommandStrategy * @see org.apache.fineract.batch.command.internal.CreateClientCommandStrategy
* @see org.apache.fineract.batch.command.internal.ApplyLoanCommandStrategy * @see org.apache.fineract.batch.command.internal.ApplyLoanCommandStrategy
@@ -2461,10 +2455,11 @@ public class BatchApiTest {
final String jsonifiedRepaymentRequest = BatchHelper.toJsonString(List.of(br)); final String jsonifiedRepaymentRequest = BatchHelper.toJsonString(List.of(br));
final List<BatchResponse> repaymentResponse = BatchHelper.postBatchRequestsWithoutEnclosingTransaction(requestSpec, // verify HTTP 409
this.responseSpec, jsonifiedRepaymentRequest); ResponseSpecification conflictResponseSpec = new ResponseSpecBuilder().expectStatusCode(409).build();
ErrorResponse errorResponse = BatchHelper.postBatchRequestsWithoutEnclosingTransactionError(requestSpec, conflictResponseSpec,
Assertions.assertEquals(HttpStatus.SC_CONFLICT, repaymentResponse.get(0).getStatusCode(), "Verify Status Code 409 for Locked Loan"); jsonifiedRepaymentRequest);
assertEquals(409, parseInt(errorResponse.getHttpStatusCode()));
} }
/** /**
@@ -2542,10 +2537,11 @@ public class BatchApiTest {
final String jsonifiedRepaymentRequest = BatchHelper.toJsonString(List.of(br)); final String jsonifiedRepaymentRequest = BatchHelper.toJsonString(List.of(br));
final List<BatchResponse> repaymentResponse = BatchHelper.postBatchRequestsWithoutEnclosingTransaction(requestSpec, // verify HTTP 409
this.responseSpec, jsonifiedRepaymentRequest); ResponseSpecification conflictResponseSpec = new ResponseSpecBuilder().expectStatusCode(409).build();
ErrorResponse errorResponse = BatchHelper.postBatchRequestsWithoutEnclosingTransactionError(requestSpec, conflictResponseSpec,
Assertions.assertEquals(HttpStatus.SC_CONFLICT, repaymentResponse.get(0).getStatusCode(), "Verify Status Code 409 for Locked Loan"); jsonifiedRepaymentRequest);
assertEquals(409, parseInt(errorResponse.getHttpStatusCode()));
} }
@Test @Test
@@ -0,0 +1,101 @@
/**
* 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.integrationtests;
import java.math.BigDecimal;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
import org.apache.fineract.client.models.PostLoansRequest;
import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.http.HttpStatus;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class BatchLoanIntegrationTest extends BaseLoanIntegrationTest {
@Test
public void test_InlineLoanCOB_ShouldExecute_WhenLoanIsBehind_And_RescheduleIsRequestedViaBatchApi() {
AtomicLong createdLoanId = new AtomicLong();
runAt("01 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
int numberOfRepayments = 24;
int repaymentEvery = 1;
// Create Loan Product
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() //
.numberOfRepayments(numberOfRepayments) //
.repaymentEvery(repaymentEvery) //
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()); //
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
Long loanProductId = loanProductResponse.getResourceId();
// Apply and Approve Loan
double amount = 1250.0;
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
.repaymentEvery(repaymentEvery)//
.loanTermFrequency(numberOfRepayments)//
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
.loanTermFrequencyType(RepaymentFrequencyType.MONTHS);
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
approveLoanRequest(amount, "01 January 2023"));
Long loanId = approvedLoanResult.getLoanId();
// disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023");
createdLoanId.set(loanId);
});
runAt("02 January 2023", () -> {
executeInlineCOB(createdLoanId.get());
});
runAt("05 January 2023", () -> {
runAsNonByPass(() -> {
long loanId = createdLoanId.get();
List<BatchResponse> responses = batchRequest() //
.rescheduleLoan(1L, loanId, "01 January 2023", "01 February 2023", "01 March 2023") //
.approveRescheduleLoan(2L, 1L, "01 January 2023") //
.executeEnclosingTransaction(); //
Assertions.assertEquals(HttpStatus.SC_OK, responses.get(0).getStatusCode(),
"Verify Status Code 200 for Create Reschedule Loan request");
Assertions.assertEquals(HttpStatus.SC_OK, responses.get(1).getStatusCode(),
"Verify Status Code 200 for Approve Reschedule Loan request");
verifyLastClosedBusinessDate(loanId, "04 January 2023");
});
});
}
}
@@ -35,6 +35,8 @@ import java.util.stream.Collectors;
import org.apache.fineract.batch.command.internal.CreateTransactionLoanCommandStrategy; import org.apache.fineract.batch.command.internal.CreateTransactionLoanCommandStrategy;
import org.apache.fineract.batch.domain.BatchRequest; import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse; import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.client.util.JSON;
import org.apache.fineract.integrationtests.common.error.ErrorResponse;
import org.apache.fineract.integrationtests.common.savings.SavingsTransactionData; import org.apache.fineract.integrationtests.common.savings.SavingsTransactionData;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -57,6 +59,8 @@ public final class BatchHelper {
private static final String BATCH_API_WITHOUT_ENCLOSING_URL_EXT = BATCH_API_URL + "&enclosingTransaction=false"; private static final String BATCH_API_WITHOUT_ENCLOSING_URL_EXT = BATCH_API_URL + "&enclosingTransaction=false";
private static final SecureRandom secureRandom = new SecureRandom(); private static final SecureRandom secureRandom = new SecureRandom();
private static final Gson GSON = new JSON().getGson();
private BatchHelper() { private BatchHelper() {
} }
@@ -108,6 +112,14 @@ public final class BatchHelper {
return BatchHelper.fromJsonString(response); return BatchHelper.fromJsonString(response);
} }
public static ErrorResponse postBatchRequestsWithoutEnclosingTransactionError(final RequestSpecification requestSpec,
final ResponseSpecification responseSpec, final String jsonifiedBatchRequests) {
final String response = Utils.performServerPost(requestSpec, responseSpec, BATCH_API_WITHOUT_ENCLOSING_URL_EXT,
jsonifiedBatchRequests, null);
LOG.info("BatchHelper Response {}", response);
return GSON.fromJson(response, ErrorResponse.class);
}
/** /**
* Returns a list of BatchResponse with query parameter enclosing transaction set to true by posting the jsonified * Returns a list of BatchResponse with query parameter enclosing transaction set to true by posting the jsonified
* BatchRequest to the server. * BatchRequest to the server.
@@ -0,0 +1,27 @@
/**
* 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.integrationtests.common.error;
import lombok.Data;
@Data
public class ErrorResponse {
private String httpStatusCode;
}
@@ -43,8 +43,9 @@ public final class UserHelper {
private static final String REPAYMENT_LOAN_PERMISSION = "REPAYMENT_LOAN"; private static final String REPAYMENT_LOAN_PERMISSION = "REPAYMENT_LOAN";
private static final String READ_LOAN_PERMISSION = "READ_LOAN"; private static final String READ_LOAN_PERMISSION = "READ_LOAN";
private static boolean simpleUserCreated = false; public static final String SIMPLE_USER_NAME = Utils.uniqueRandomStringGenerator("NotificationUser", 4);
private static String simpleUsername; public static final String SIMPLE_USER_PASSWORD = "aA1qwerty56";
private static boolean SIMPLE_USER_CREATED = false;
private UserHelper() {} private UserHelper() {}
@@ -141,23 +142,22 @@ public final class UserHelper {
public static RequestSpecification getSimpleUserWithoutBypassPermission(final RequestSpecification requestSpec, public static RequestSpecification getSimpleUserWithoutBypassPermission(final RequestSpecification requestSpec,
final ResponseSpecification responseSpec) { final ResponseSpecification responseSpec) {
String password = "aA1qwerty56"; String password = SIMPLE_USER_PASSWORD;
if (!simpleUserCreated) { if (!SIMPLE_USER_CREATED) {
GetOfficesResponse headOffice = OfficeHelper.getHeadOffice(requestSpec, responseSpec); GetOfficesResponse headOffice = OfficeHelper.getHeadOffice(requestSpec, responseSpec);
simpleUsername = Utils.uniqueRandomStringGenerator("NotificationUser", 4);
String simpleRoleId = createSimpleRole(requestSpec, responseSpec); String simpleRoleId = createSimpleRole(requestSpec, responseSpec);
PostUsersRequest createUserRequest = new PostUsersRequest().username(simpleUsername) PostUsersRequest createUserRequest = new PostUsersRequest().username(SIMPLE_USER_NAME)
.firstname(Utils.randomStringGenerator("NotificationFN", 4)).lastname(Utils.randomStringGenerator("NotificationLN", 4)) .firstname(Utils.randomStringGenerator("NotificationFN", 4)).lastname(Utils.randomStringGenerator("NotificationLN", 4))
.email("whatever@mifos.org").password(password).repeatPassword(password).sendPasswordToEmail(false) .email("whatever@mifos.org").password(password).repeatPassword(password).sendPasswordToEmail(false)
.roles(List.of(Long.valueOf(simpleRoleId))).officeId(headOffice.getId()); .roles(List.of(Long.valueOf(simpleRoleId))).officeId(headOffice.getId());
PostUsersResponse userCreationResponse = UserHelper.createUser(requestSpec, responseSpec, createUserRequest); PostUsersResponse userCreationResponse = UserHelper.createUser(requestSpec, responseSpec, createUserRequest);
Assertions.assertNotNull(userCreationResponse.getResourceId()); Assertions.assertNotNull(userCreationResponse.getResourceId());
simpleUserCreated = true; SIMPLE_USER_CREATED = true;
} }
RequestSpecification responseRequestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); RequestSpecification responseRequestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
responseRequestSpec.header("Authorization", responseRequestSpec.header("Authorization",
"Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(simpleUsername, password)); "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(SIMPLE_USER_NAME, password));
return responseRequestSpec; return responseRequestSpec;
} }
@@ -172,6 +172,10 @@ public final class UserHelper {
HashMap<String, Boolean> permissionMap = new HashMap<>(); HashMap<String, Boolean> permissionMap = new HashMap<>();
permissionMap.put(REPAYMENT_LOAN_PERMISSION, true); permissionMap.put(REPAYMENT_LOAN_PERMISSION, true);
permissionMap.put(READ_LOAN_PERMISSION, true); permissionMap.put(READ_LOAN_PERMISSION, true);
permissionMap.put("READ_RESCHEDULELOAN", true);
permissionMap.put("CREATE_RESCHEDULELOAN", true);
permissionMap.put("REJECT_RESCHEDULELOAN", true);
permissionMap.put("APPROVE_RESCHEDULELOAN", true);
RolesHelper.addPermissionsToRole(requestSpec, responseSpec, roleId, permissionMap); RolesHelper.addPermissionsToRole(requestSpec, responseSpec, roleId, permissionMap);
} }
} }