blob: 92b282208c7320573f735a7afc5fe74b8aec74c9 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.jobs.filter;
import static org.apache.fineract.batch.command.CommandStrategyUtils.isRelativeUrlVersioned;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.json.JsonReadFeature;
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 jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
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.data.LoanIdAndLastClosedBusinessDate;
import org.apache.fineract.cob.loan.RetrieveLoanIdService;
import org.apache.fineract.cob.service.InlineLoanCOBExecutorServiceImpl;
import org.apache.fineract.cob.service.LoanAccountLockService;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.jobs.exception.LoanIdsHardLockedException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
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.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepository;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Conditional;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
@RequiredArgsConstructor
@Component
@Conditional(LoanCOBEnabledCondition.class)
public class LoanCOBFilterHelper implements InitializingBean {
private final GLIMAccountInfoRepository glimAccountInfoRepository;
private final LoanAccountLockService loanAccountLockService;
private final PlatformSecurityContext context;
private final InlineLoanCOBExecutorServiceImpl inlineLoanCOBExecutorService;
private final LoanRepository loanRepository;
private final FineractProperties fineractProperties;
private final RetrieveLoanIdService retrieveLoanIdService;
private final LoanRescheduleRequestRepository loanRescheduleRequestRepository;
private final ObjectMapper objectMapper = new ObjectMapper();
private static final List<HttpMethod> HTTP_METHODS = List.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE);
public static final Pattern IGNORE_LOAN_PATH_PATTERN = Pattern.compile("/v[1-9][0-9]*/loans/catch-up");
public static final Pattern LOAN_PATH_PATTERN = Pattern.compile("/v[1-9][0-9]*/(?:reschedule)?loans/(?:external-id/)?([^/?]+).*");
public static final Pattern LOAN_GLIMACCOUNT_PATH_PATTERN = Pattern.compile("/v[1-9][0-9]*/loans/glimAccount/(\\d+).*");
private static final Predicate<String> URL_FUNCTION = s -> LOAN_PATH_PATTERN.matcher(s).find()
|| LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(s).find();
private static final String JOB_NAME = "INLINE_LOAN_COB";
private Long getLoanId(boolean isGlim, String pathInfo) {
if (!isGlim) {
String id = LOAN_PATH_PATTERN.matcher(pathInfo).replaceAll("$1");
if (isExternal(pathInfo)) {
String externalId = id;
return loanRepository.findIdByExternalId(new ExternalId(externalId));
} else if (isRescheduleLoans(pathInfo)) {
return loanRescheduleRequestRepository.getLoanIdByRescheduleRequestId(Long.valueOf(id)).orElse(null);
} else if (StringUtils.isNumeric(id)) {
return Long.valueOf(id);
} else {
return null;
}
} else {
return Long.valueOf(LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(pathInfo).replaceAll("$1"));
}
}
private boolean isExternal(String pathInfo) {
return LOAN_PATH_PATTERN.matcher(pathInfo).matches() && pathInfo.contains("external-id");
}
private boolean isRescheduleLoans(String pathInfo) {
return LOAN_PATH_PATTERN.matcher(pathInfo).matches() && pathInfo.contains("/v1/rescheduleloans/");
}
public boolean isOnApiList(HttpServletRequest request) throws IOException {
String pathInfo = request.getPathInfo();
String method = request.getMethod();
if (StringUtils.isBlank(pathInfo)) {
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()
&& URL_FUNCTION.test(pathInfo);
}
private boolean isBatchApi(String pathInfo) {
return pathInfo.startsWith("/v1/batches");
}
private boolean isGlim(String pathInfo) {
return LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(pathInfo).matches();
}
public boolean isBypassUser() {
return context.authenticatedUser().isBypassUser();
}
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 isLoanHardLocked(Long... loanIds) {
return isLoanHardLocked(Arrays.asList(loanIds));
}
private boolean isLoanHardLocked(List<Long> loanIds) {
return loanIds.stream().anyMatch(loanAccountLockService::isLoanHardLocked);
}
public boolean isLoanBehind(List<Long> loanIds) {
List<LoanIdAndLastClosedBusinessDate> loanIdAndLastClosedBusinessDates = new ArrayList<>();
List<List<Long>> partitions = Lists.partition(loanIds, fineractProperties.getQuery().getInClauseParameterSizeLimit());
partitions.forEach(partition -> loanIdAndLastClosedBusinessDates.addAll(retrieveLoanIdService
.retrieveLoanIdsBehindDate(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE), partition)));
return CollectionUtils.isNotEmpty(loanIdAndLastClosedBusinessDates);
}
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) {
if (isLoanHardLocked(loanId)) {
throw new LoanIdsHardLockedException(loanId);
} else {
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);
if (isLoanHardLocked(loanIds)) {
throw new LoanIdsHardLockedException(loanIds.get(0));
} else {
return loanIds;
}
}
private List<Long> getLoanIdList(String pathInfo) {
boolean isGlim = isGlim(pathInfo);
Long loanIdFromRequest = getLoanId(isGlim, pathInfo);
if (loanIdFromRequest == null) {
return Collections.emptyList();
}
if (isGlim) {
return getGlimChildLoanIds(loanIdFromRequest);
} else {
return Collections.singletonList(loanIdFromRequest);
}
}
public void executeInlineCob(List<Long> loanIds) {
inlineLoanCOBExecutorService.execute(loanIds, JOB_NAME);
}
@Override
public void afterPropertiesSet() throws Exception {
objectMapper.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true);
}
}