FINERACT-1724 - Transaction handling fix

[x] - Remove requires new transaction from NewCommandSourceHandler
[x] - Rewrite batch processing
[x] - Fix Idempotency transaction handling
[x] - Fix Batch enclosingTransaction handling
This commit is contained in:
Janos Haber
2023-03-27 20:18:29 +02:00
committed by Adam Saghy
parent eb91d44c90
commit 92838e5fff
16 changed files with 442 additions and 206 deletions
@@ -19,17 +19,21 @@
package org.apache.fineract.batch.service;
import com.google.gson.Gson;
import io.github.resilience4j.core.functions.Either;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import lombok.RequiredArgsConstructor;
@@ -51,9 +55,14 @@ import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProces
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessUnderProcessingException;
import org.apache.fineract.infrastructure.core.filters.BatchCallHandler;
import org.apache.fineract.infrastructure.core.filters.BatchFilter;
import org.apache.fineract.infrastructure.core.filters.BatchRequestPreprocessor;
import org.jetbrains.annotations.NotNull;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionExecution;
import org.springframework.transaction.support.TransactionTemplate;
/**
@@ -72,10 +81,12 @@ public class BatchApiServiceImpl implements BatchApiService {
private final CommandStrategyProvider strategyProvider;
private final ResolutionHelper resolutionHelper;
private final TransactionTemplate transactionTemplate;
private final PlatformTransactionManager transactionManager;
private final List<BatchFilter> batchFilters;
private final List<BatchRequestPreprocessor> batchPreprocessors;
@PersistenceContext
private final EntityManager entityManager;
@@ -87,8 +98,8 @@ public class BatchApiServiceImpl implements BatchApiService {
* @param uriInfo
* @return {@code List<BatchResponse>}
*/
private List<BatchResponse> handleBatchRequests(final List<BatchRequest> requestList, final UriInfo uriInfo,
boolean isEnclosingTransaction) {
private List<BatchResponse> handleBatchRequests(boolean enclosingTransaction, final List<BatchRequest> requestList,
final UriInfo uriInfo) {
final List<BatchResponse> responseList = new ArrayList<>(requestList.size());
@@ -103,52 +114,150 @@ public class BatchApiServiceImpl implements BatchApiService {
}
for (BatchRequestNode rootNode : batchRequestNodes) {
final BatchRequest rootRequest = rootNode.getRequest();
final CommandStrategy commandStrategy = this.strategyProvider
.getCommandStrategy(CommandContext.resource(rootRequest.getRelativeUrl()).method(rootRequest.getMethod()).build());
log.debug("Batch request: method [{}], relative url [{}]", rootRequest.getMethod(), rootRequest.getRelativeUrl());
final BatchResponse rootResponse = safelyExecuteStrategy(commandStrategy, rootRequest, uriInfo, isEnclosingTransaction);
log.debug("Batch response: status code [{}], method [{}], relative url [{}]", rootResponse.getStatusCode(),
rootRequest.getMethod(), rootRequest.getRelativeUrl());
responseList.add(rootResponse);
responseList.addAll(this.processChildRequests(rootNode, rootResponse, uriInfo, isEnclosingTransaction));
if (enclosingTransaction) {
this.callRequestRecursive(rootNode.getRequest(), rootNode, responseList, uriInfo);
} else {
responseList.addAll(callInTransaction(
transactionTemplate -> transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
() -> {
List<BatchResponse> localResponseList = new ArrayList<>();
this.callRequestRecursive(rootNode.getRequest(), rootNode, localResponseList, uriInfo);
return localResponseList;
}));
}
}
Collections.sort(responseList, Comparator.comparing(BatchResponse::getRequestId));
return responseList;
}
private BatchResponse safelyExecuteStrategy(CommandStrategy commandStrategy, BatchRequest request, UriInfo originalUriInfo,
boolean isEnclosingTransaction) {
/**
* Executes the request and call child requests recursively.
*
* @param request
* the current batch request
* @param requestNode
* the batch request holder node
* @param responseList
* the collected responses
* @return {@code BatchResponse}
*/
private void callRequestRecursive(BatchRequest request, BatchRequestNode requestNode, List<BatchResponse> responseList,
UriInfo uriInfo) {
// 1. run current node
BatchResponse response = executeRequest(request, uriInfo);
responseList.add(response);
if (response.getStatusCode() == 200) {
requestNode.getChildRequests().forEach(childNode -> {
BatchRequest resolvedChildRequest;
try {
resolvedChildRequest = this.resolutionHelper.resoluteRequest(childNode.getRequest(), response);
} catch (RuntimeException ex) {
throw new BatchExecutionException(childNode.getRequest(), ex);
}
callRequestRecursive(resolvedChildRequest, childNode, responseList, uriInfo);
});
} else {
responseList.addAll(parentRequestFailedRecursive(request, requestNode));
}
// If the current request fails, then all the child requests are not executed. If we want to write out all the
// child requests, here is the place.
}
/**
* All requests recursively are set to status 409 if the parent request fails.
*
* @param request
* the current request
* @param requestNode
* the current request node
* @return {@code BatchResponse} list of the generated batch responses
*/
private List<BatchResponse> parentRequestFailedRecursive(BatchRequest request, BatchRequestNode requestNode) {
List<BatchResponse> responseList = new ArrayList<>();
BatchRequestContextHolder.getEnclosingTransaction().ifPresent(TransactionExecution::setRollbackOnly);
BatchResponse errorResponse = new BatchResponse();
errorResponse.setRequestId(request.getRequestId());
errorResponse.setStatusCode(Status.CONFLICT.getStatusCode());
// Some detail information about the error
final ErrorInfo conflictError = new ErrorInfo(Status.CONFLICT.getStatusCode(), 8001,
"Parent request with id " + request.getRequestId() + " was erroneous!");
errorResponse.setBody(conflictError.getMessage());
requestNode.getChildRequests().stream()
.flatMap(childNode -> parentRequestFailedRecursive(childNode.getRequest(), childNode).stream()).forEach(responseList::add);
return responseList;
}
/**
* Execute the request
*
* @param request
* @param uriInfo
* @return
*/
private BatchResponse executeRequest(BatchRequest request, UriInfo uriInfo) {
final CommandStrategy commandStrategy = this.strategyProvider
.getCommandStrategy(CommandContext.resource(request.getRelativeUrl()).method(request.getMethod()).build());
log.debug("Batch request: method [{}], relative url [{}]", request.getMethod(), request.getRelativeUrl());
Either<RuntimeException, BatchRequest> preprocessorResult = runPreprocessors(request);
if (preprocessorResult.isLeft()) {
throw new BatchExecutionException(request, preprocessorResult.getLeft());
} else {
request = preprocessorResult.get();
}
try {
if (isEnclosingTransaction) {
entityManager.flush();
}
BatchRequestContextHolder.setRequestAttributes(new HashMap<>(Optional.ofNullable(request.getHeaders())
.map(list -> list.stream().collect(Collectors.toMap(Header::getName, Header::getValue)))
.orElse(Collections.emptyMap())));
return new BatchCallHandler(this.batchFilters, commandStrategy::execute).serviceCall(request, originalUriInfo);
BatchCallHandler callHandler = new BatchCallHandler(this.batchFilters, commandStrategy::execute);
if (BatchRequestContextHolder.getEnclosingTransaction().isPresent()) {
if (BatchRequestContextHolder.getEnclosingTransaction().get().isRollbackOnly()) {
return new BatchResponse();
}
entityManager.flush();
}
final BatchResponse rootResponse = callHandler.serviceCall(request, uriInfo);
log.debug("Batch response: status code [{}], method [{}], relative url [{}]", rootResponse.getStatusCode(), request.getMethod(),
request.getRelativeUrl());
return rootResponse;
} catch (AbstractIdempotentCommandException idempotentException) {
return handleIdempotentRequests(idempotentException, request);
} catch (RuntimeException e) {
log.warn("Exception while executing batch strategy {}", commandStrategy.getClass().getSimpleName(), e);
ErrorInfo ex = ErrorHandler.handler(e);
final BatchResponse response = new BatchResponse();
response.setRequestId(request.getRequestId());
response.setHeaders(request.getHeaders());
response.setStatusCode(ex.getStatusCode());
response.setBody(ex.getMessage());
return response;
} catch (RuntimeException ex) {
throw new BatchExecutionException(request, ex);
} finally {
BatchRequestContextHolder.resetRequestAttributes();
}
}
private Either<RuntimeException, BatchRequest> runPreprocessors(BatchRequest request) {
return runPreprocessor(batchPreprocessors, request);
}
private Either<RuntimeException, BatchRequest> runPreprocessor(List<BatchRequestPreprocessor> remainingPreprocessor,
BatchRequest request) {
if (remainingPreprocessor.isEmpty()) {
return Either.right(request);
} else {
BatchRequestPreprocessor preprocessor = remainingPreprocessor.get(0);
Either<RuntimeException, BatchRequest> processingResult = preprocessor.preprocess(request);
if (processingResult.isLeft()) {
return processingResult;
} else {
return runPreprocessor(remainingPreprocessor.subList(1, remainingPreprocessor.size()), processingResult.get());
}
}
}
/**
* Return the idempotent response when idempotent exception raised
*
* @param idempotentException
* the idempotent exception
* @param request
* the called request
* @return
*/
private BatchResponse handleIdempotentRequests(AbstractIdempotentCommandException idempotentException, BatchRequest request) {
BatchResponse response = new BatchResponse();
response.setRequestId(request.getRequestId());
@@ -167,98 +276,96 @@ public class BatchApiServiceImpl implements BatchApiService {
return response;
}
private List<BatchResponse> processChildRequests(final BatchRequestNode rootRequest, BatchResponse rootResponse, UriInfo uriInfo,
boolean isEnclosingTransaction) {
final List<BatchResponse> childResponses = new ArrayList<>();
if (!rootRequest.getChildRequests().isEmpty()) {
for (BatchRequestNode childNode : rootRequest.getChildRequests()) {
BatchRequest childRequest = childNode.getRequest();
BatchResponse childResponse;
try {
if (rootResponse.getStatusCode().equals(200)) {
childRequest = this.resolutionHelper.resoluteRequest(childRequest, rootResponse);
final CommandStrategy commandStrategy = this.strategyProvider.getCommandStrategy(
CommandContext.resource(childRequest.getRelativeUrl()).method(childRequest.getMethod()).build());
childResponse = safelyExecuteStrategy(commandStrategy, childRequest, uriInfo, isEnclosingTransaction);
} else {
// Something went wrong with the parent request, create
// a response with status code 409
childResponse = new BatchResponse();
childResponse.setRequestId(childRequest.getRequestId());
childResponse.setStatusCode(Status.CONFLICT.getStatusCode());
// Some detail information about the error
final ErrorInfo conflictError = new ErrorInfo(Status.CONFLICT.getStatusCode(), 8001,
"Parent request with id " + rootResponse.getRequestId() + " was erroneous!");
childResponse.setBody(conflictError.getMessage());
}
childResponses.addAll(this.processChildRequests(childNode, childResponse, uriInfo, isEnclosingTransaction));
} catch (Throwable ex) {
childResponse = new BatchResponse();
childResponse.setRequestId(childRequest.getRequestId());
childResponse.setStatusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode());
childResponse.setBody(ex.getMessage());
}
childResponses.add(childResponse);
}
}
return childResponses;
}
/**
* Run each request root step in a separated transaction
*
* @param requestList
* @param uriInfo
* @return
*/
@Override
public List<BatchResponse> handleBatchRequestsWithoutEnclosingTransaction(final List<BatchRequest> requestList, UriInfo uriInfo) {
return handleBatchRequests(requestList, uriInfo, false);
BatchRequestContextHolder.setEnclosingTransaction(Optional.empty());
return handleBatchRequests(false, requestList, uriInfo);
}
/**
* Run the batch request in transaction
*
* @param requestList
* @param uriInfo
* @return
*/
@Override
public List<BatchResponse> handleBatchRequestsWithEnclosingTransaction(final List<BatchRequest> requestList, final UriInfo uriInfo) {
return callInTransaction(() -> handleBatchRequests(true, requestList, uriInfo));
}
@NotNull
private List<BatchResponse> createErrorResponse(List<BatchResponse> responseList, int statusCode) {
BatchResponse errResponse = new BatchResponse();
for (BatchResponse res : responseList) {
if (!res.getStatusCode().equals(200)) {
errResponse.setBody("Transaction is being rolled back. First erroneous request: \n" + new Gson().toJson(res));
errResponse.setRequestId(res.getRequestId());
if (statusCode == -1) {
statusCode = res.getStatusCode();
}
break;
}
}
errResponse.setStatusCode(statusCode);
return Arrays.asList(errResponse);
}
private List<BatchResponse> callInTransaction(Supplier<List<BatchResponse>> request) {
return callInTransaction(Function.identity()::apply, request);
}
/**
* Helper method to run the command in transaction
*
* @param request
* the enclosing supplier of the command
* @param transactionConfigurator
* consumer to configure the transaction behavior and isolation
* @return
*/
private List<BatchResponse> callInTransaction(Consumer<TransactionTemplate> transactionConfigurator,
Supplier<List<BatchResponse>> request) {
List<BatchResponse> responseList = new ArrayList<>();
try {
return this.transactionTemplate.execute(status -> {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionConfigurator.accept(transactionTemplate);
return transactionTemplate.execute(status -> {
BatchRequestContextHolder.setEnclosingTransaction(Optional.of(status));
try {
responseList.addAll(handleBatchRequests(requestList, uriInfo, true));
responseList.addAll(request.get());
if (status.isRollbackOnly()) {
return createErrorResponse(responseList, -1);
}
return responseList;
} catch (BatchExecutionException ex) {
status.setRollbackOnly();
BatchResponse errResponse = new BatchResponse();
errResponse.setStatusCode(ex.getErrorInfo().getStatusCode());
errResponse.setRequestId(ex.getRequest().getRequestId());
errResponse.setBody(ex.getErrorInfo().getMessage());
return Arrays.asList(errResponse);
} catch (RuntimeException ex) {
status.setRollbackOnly();
ErrorInfo e = ErrorHandler.handler(ex);
BatchResponse errResponse = new BatchResponse();
errResponse.setStatusCode(e.getStatusCode());
errResponse.setBody(e.getMessage());
List<BatchResponse> errResponseList = new ArrayList<>();
errResponseList.add(errResponse);
status.setRollbackOnly();
return errResponseList;
return Arrays.asList(errResponse);
}
});
} catch (TransactionException | NonTransientDataAccessException ex) {
ErrorInfo e = ErrorHandler.handler(ex);
BatchResponse errResponse = new BatchResponse();
errResponse.setStatusCode(e.getStatusCode());
for (BatchResponse res : responseList) {
if (!res.getStatusCode().equals(200)) {
errResponse.setBody("Transaction is being rolled back. First erroneous request: \n" + new Gson().toJson(res));
break;
}
}
List<BatchResponse> errResponseList = new ArrayList<>();
errResponseList.add(errResponse);
return errResponseList;
return createErrorResponse(responseList, e.getStatusCode());
}
}
}
@@ -0,0 +1,37 @@
/**
* 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.batch.service;
import lombok.Getter;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.exception.ErrorHandler;
import org.apache.fineract.batch.exception.ErrorInfo;
@Getter
public class BatchExecutionException extends RuntimeException {
private final BatchRequest request;
private final ErrorInfo errorInfo;
public BatchExecutionException(BatchRequest request, RuntimeException ex) {
super("Error executing batch request: " + request, ex);
this.request = request;
this.errorInfo = ErrorHandler.handler(ex);
}
}
@@ -20,12 +20,8 @@ package org.apache.fineract.commands.handler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
public interface NewCommandSourceHandler {
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
CommandProcessingResult processCommand(JsonCommand command);
}
@@ -30,6 +30,7 @@ import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.exception.CommandNotFoundException;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.useradministration.domain.AppUser;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
@@ -48,6 +49,15 @@ public class CommandSourceService {
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
public CommandSource saveInitial(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
return saveInitialInternal(wrapper, jsonCommand, maker, idempotencyKey);
}
public CommandSource saveInitialNoTransaction(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
return saveInitialInternal(wrapper, jsonCommand, maker, idempotencyKey);
}
@NotNull
private CommandSource saveInitialInternal(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
CommandSource initialCommandSource = getInitialCommandSource(wrapper, jsonCommand, maker, idempotencyKey);
if (initialCommandSource.getCommandJson() == null) {
@@ -57,7 +67,6 @@ public class CommandSourceService {
return commandSourceRepository.saveAndFlush(initialCommandSource);
}
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
public void saveFailed(CommandSource commandSource) {
commandSource.setStatus(ERROR.getValue());
commandSourceRepository.saveAndFlush(commandSource);
@@ -68,6 +77,10 @@ public class CommandSourceService {
return commandSourceRepository.saveAndFlush(commandSource);
}
public CommandSource saveResultNoTransaction(CommandSource commandSource) {
return commandSourceRepository.saveAndFlush(commandSource);
}
public ErrorInfo generateErrorException(Throwable t) {
if (t instanceof final RuntimeException e) {
return ErrorHandler.handler(e);
@@ -82,6 +95,11 @@ public class CommandSourceService {
idempotencyKey);
}
public CommandSource findCommandSourceNoTransaction(CommandWrapper wrapper, String idempotencyKey) {
return commandSourceRepository.findByActionNameAndEntityNameAndIdempotencyKey(wrapper.actionName(), wrapper.entityName(),
idempotencyKey);
}
private CommandSource getInitialCommandSource(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
CommandSource commandSourceResult;
if (jsonCommand.commandId() != null) {
@@ -44,6 +44,7 @@ import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDoma
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.domain.BatchRequestContextHolder;
import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
import org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessFailedException;
@@ -94,9 +95,13 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
exceptionWhenTheRequestAlreadyProcessed(wrapper, idempotencyKey);
// Store idempotency key to the request attribute
CommandSource savedCommandSource = commandSourceService.saveInitial(wrapper, command, context.authenticatedUser(wrapper),
idempotencyKey);
CommandSource savedCommandSource;
if (BatchRequestContextHolder.getEnclosingTransaction().isPresent()) {
savedCommandSource = commandSourceService.saveInitialNoTransaction(wrapper, command, context.authenticatedUser(wrapper),
idempotencyKey);
} else {
savedCommandSource = commandSourceService.saveInitial(wrapper, command, context.authenticatedUser(wrapper), idempotencyKey);
}
storeCommandToIdempotentFilter(savedCommandSource);
setIdempotencyKeyStoreFlag(true);
@@ -104,12 +109,23 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
try {
result = findCommandHandler(wrapper).processCommand(command);
} catch (Throwable t) { // NOSONAR
commandSourceService.saveFailed(commandSourceService.findCommandSource(wrapper, idempotencyKey));
CommandSource source;
if (BatchRequestContextHolder.getEnclosingTransaction().isPresent()) {
source = commandSourceService.findCommandSourceNoTransaction(wrapper, idempotencyKey);
} else {
source = commandSourceService.findCommandSource(wrapper, idempotencyKey);
}
commandSourceService.saveFailed(source);
publishHookErrorEvent(wrapper, command, t);
throw t;
}
CommandSource initialCommandSource;
if (BatchRequestContextHolder.getEnclosingTransaction().isPresent()) {
initialCommandSource = commandSourceService.findCommandSourceNoTransaction(wrapper, idempotencyKey);
} else {
initialCommandSource = commandSourceService.findCommandSource(wrapper, idempotencyKey);
}
CommandSource initialCommandSource = commandSourceService.findCommandSource(wrapper, idempotencyKey);
initialCommandSource.setResult(toApiJsonSerializer.serializeResult(result));
initialCommandSource.updateResourceId(result.getResourceId());
initialCommandSource.updateForAudit(result);
@@ -120,7 +136,11 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
}
initialCommandSource.setStatus(CommandProcessingResultType.PROCESSED.getValue());
commandSourceService.saveResult(initialCommandSource);
if (BatchRequestContextHolder.getEnclosingTransaction().isPresent()) {
commandSourceService.saveResultNoTransaction(initialCommandSource);
} else {
commandSourceService.saveResult(initialCommandSource);
}
if ((rollbackTransaction || result.isRollbackTransaction()) && !isApprovedByChecker) {
/*
@@ -187,7 +207,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
if (commandSourceResult.getIdempotencyKey() == null) {
commandSourceResult.setIdempotencyKey(idempotencyKeyGenerator.create());
}
commandSourceResult = commandSourceService.saveResult(commandSourceResult);
commandSourceResult = commandSourceService.saveResultNoTransaction(commandSourceResult);
return new CommandProcessingResultBuilder().withCommandId(commandSourceResult.getId())
.withEntityId(commandSourceResult.getResourceId()).build();
@@ -19,7 +19,9 @@
package org.apache.fineract.infrastructure.core.domain;
import java.util.Map;
import java.util.Optional;
import org.springframework.core.NamedThreadLocal;
import org.springframework.transaction.TransactionStatus;
public final class BatchRequestContextHolder {
@@ -27,6 +29,14 @@ public final class BatchRequestContextHolder {
private static final ThreadLocal<Map<String, Object>> batchAttributes = new NamedThreadLocal<>("BatchAttributesForProcessing");
private static final ThreadLocal<Optional<TransactionStatus>> enclosingTransaction = new NamedThreadLocal<>("EnclosingTransaction") {
@Override
protected Optional<TransactionStatus> initialValue() {
return Optional.empty();
}
};
/**
* True if the batch attributes are set
*
@@ -36,6 +46,15 @@ public final class BatchRequestContextHolder {
return batchAttributes.get() != null;
}
/**
* True if the batch attributes are set and the enclosing transaction is set to true
*
* @return
*/
public static Optional<TransactionStatus> getEnclosingTransaction() {
return enclosingTransaction.get();
}
/**
* Set the batch attributes for the current thread.
*
@@ -61,4 +80,13 @@ public final class BatchRequestContextHolder {
public static void resetRequestAttributes() {
batchAttributes.remove();
}
/**
* Set the enclosing transaction flag for the current thread.
*
* @param isEnclosingTransaction
*/
public static void setEnclosingTransaction(Optional<TransactionStatus> enclosingTransaction) {
BatchRequestContextHolder.enclosingTransaction.set(enclosingTransaction);
}
}
@@ -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.infrastructure.core.filters;
import io.github.resilience4j.core.functions.Either;
import org.apache.fineract.batch.domain.BatchRequest;
public interface BatchRequestPreprocessor {
Either<RuntimeException, BatchRequest> preprocess(BatchRequest batchRequest);
}
@@ -70,7 +70,7 @@ public class IdempotencyStoreFilter extends OncePerRequestFilter implements Batc
boolean isSuccessWithoutStored = isStoreIdempotencyKey(request) && commandId.isPresent() && isAllowedContentTypeResponse(response)
&& wrapper.getValue() != null;
if (isSuccessWithoutStored) {
storeCommandResult(response.getStatus(), Optional.ofNullable(wrapper.getValue())
storeCommandResult(false, response.getStatus(), Optional.ofNullable(wrapper.getValue())
.map(ContentCachingResponseWrapper::getContentAsByteArray).map(s -> new String(s, StandardCharsets.UTF_8)).orElse(null),
commandId);
}
@@ -79,11 +79,15 @@ public class IdempotencyStoreFilter extends OncePerRequestFilter implements Batc
}
}
private void storeCommandResult(int response, String body, Optional<Long> commandId) {
private void storeCommandResult(boolean batch, int response, String body, Optional<Long> commandId) {
commandSourceRepository.findById(commandId.get()).ifPresent(commandSource -> {
commandSource.setResultStatusCode(response);
commandSource.setResult(body);
commandSourceService.saveResult(commandSource);
if (batch) {
commandSourceService.saveResultNoTransaction(commandSource);
} else {
commandSourceService.saveResult(commandSource);
}
});
}
@@ -133,7 +137,7 @@ public class IdempotencyStoreFilter extends OncePerRequestFilter implements Batc
Optional<Long> commandId = getCommandId(null);
boolean isSuccessWithoutStored = isStoreIdempotencyKey(null) && commandId.isPresent();
if (isSuccessWithoutStored) {
storeCommandResult(result.getStatusCode(), result.getBody(), commandId);
storeCommandResult(true, result.getStatusCode(), result.getBody(), commandId);
}
return result;
}
@@ -19,6 +19,7 @@
package org.apache.fineract.infrastructure.jobs.filter;
import com.google.common.collect.Lists;
import io.github.resilience4j.core.functions.Either;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
@@ -30,13 +31,11 @@ import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.UriInfo;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate;
import org.apache.fineract.cob.service.InlineLoanCOBExecutorServiceImpl;
import org.apache.fineract.cob.service.LoanAccountLockService;
@@ -44,24 +43,27 @@ import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.filters.BatchFilter;
import org.apache.fineract.infrastructure.core.filters.BatchFilterChain;
import org.apache.fineract.infrastructure.core.filters.BatchRequestPreprocessor;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
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.exception.LoanNotFoundException;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepository;
import org.apache.fineract.useradministration.exception.UnAuthenticatedUserException;
import org.apache.http.HttpStatus;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
@RequiredArgsConstructor
public class LoanCOBApiFilter extends OncePerRequestFilter implements BatchFilter {
public class LoanCOBApiFilter extends OncePerRequestFilter implements BatchRequestPreprocessor {
private final GLIMAccountInfoRepository glimAccountInfoRepository;
private final LoanAccountLockService loanAccountLockService;
@@ -82,6 +84,8 @@ public class LoanCOBApiFilter extends OncePerRequestFilter implements BatchFilte
|| LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(s).find();
private static final String JOB_NAME = "INLINE_LOAN_COB";
private final PlatformTransactionManager transactionManager;
@RequiredArgsConstructor
@Getter
private static class LoanIdsHardLockedException extends RuntimeException {
@@ -107,15 +111,6 @@ public class LoanCOBApiFilter extends OncePerRequestFilter implements BatchFilte
response.setStatus(statusCode);
response.getWriter().write(message);
}
public BatchResponse toBatchResponse(BatchRequest request) {
BatchResponse response = new BatchResponse();
response.setStatusCode(statusCode);
response.setBody(message);
response.setHeaders(request.getHeaders());
response.setRequestId(request.getRequestId());
return response;
}
}
@Override
@@ -251,28 +246,27 @@ public class LoanCOBApiFilter extends OncePerRequestFilter implements BatchFilte
}
@Override
public BatchResponse doFilter(BatchRequest batchRequest, UriInfo uriInfo, BatchFilterChain chain) {
if (!isOnApiList("/" + batchRequest.getRelativeUrl(), batchRequest.getMethod())) {
return chain.serviceCall(batchRequest, uriInfo);
} else {
public Either<RuntimeException, BatchRequest> preprocess(BatchRequest batchRequest) {
TransactionTemplate tr = new TransactionTemplate(transactionManager);
tr.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED);
return tr.execute(status -> {
try {
boolean bypassUser = isBypassUser();
if (bypassUser) {
return chain.serviceCall(batchRequest, uriInfo);
} else {
try {
if (isOnApiList("/" + batchRequest.getRelativeUrl(), batchRequest.getMethod())) {
boolean bypassUser = isBypassUser();
if (!bypassUser) {
List<Long> result = calculateRelevantLoanIds("/" + batchRequest.getRelativeUrl());
if (!result.isEmpty() && (isLoanSoftLocked(result) || isLoanBehind(result))) {
executeInlineCob(result);
}
return chain.serviceCall(batchRequest, uriInfo);
} catch (LoanIdsHardLockedException e) {
return Reject.reject(e.getLoanIdFromRequest(), HttpStatus.SC_CONFLICT).toBatchResponse(batchRequest);
}
}
} catch (UnAuthenticatedUserException e) {
return Reject.reject(null, HttpStatus.SC_UNAUTHORIZED).toBatchResponse(batchRequest);
} catch (LoanNotFoundException e) {
return Either.right(batchRequest);
} catch (RuntimeException e) {
return Either.left(e);
}
}
return Either.right(batchRequest);
});
}
}
@@ -158,7 +158,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
transactionDate, transactionAmount, paymentDetail, transactionBooleanValues, backdatedTxnsAllowedTill);
final Long toLoanAccountId = command.longValueOfParameterNamed(toAccountIdParamName);
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId);
Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(toLoanAccountId);
final Boolean isHolidayValidationDone = false;
final HolidayDetailDTO holidayDetailDto = null;
@@ -169,7 +169,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
final LoanTransaction loanRepaymentTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT,
toLoanAccount, transactionDate, transactionAmount, paymentDetail, null, externalId, isRecoveryRepayment,
chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
toLoanAccount = loanRepaymentTransaction.getLoan();
final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleSavingsToLoanTransfer(command,
fromSavingsAccount, toLoanAccount, withdrawal, loanRepaymentTransaction);
this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
@@ -340,6 +340,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(),
accountTransferDTO.getPaymentDetail(), null, externalId, isRecoveryRepayment, chargeRefundChargeType,
isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
toLoanAccount = loanTransaction.getLoan();
}
accountTransferDetails = this.accountTransferAssembler.assembleSavingsToLoanTransfer(accountTransferDTO, fromSavingsAccount,
@@ -2687,7 +2687,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
}
this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values());
}
updateLoanSummaryDerivedFields();
}
@@ -216,7 +216,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
* before the latest payment recorded against the loan)
***/
if (changedTransactionDetail != null) {
for (Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
// Trigger transaction replayed event
@@ -752,8 +753,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
}
@Override
public LoanTransaction foreCloseLoan(final Loan loan, final LocalDate foreClosureDate, final String noteText,
final ExternalId externalId, Map<String, Object> changes) {
public LoanTransaction foreCloseLoan(Loan loan, final LocalDate foreClosureDate, final String noteText, final ExternalId externalId,
Map<String, Object> changes) {
if (loan.isChargedOff() && foreClosureDate.isBefore(loan.getChargedOffOnDate())) {
throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: "
+ loan.getId()
@@ -836,14 +837,14 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
changes.put("eventAmount", payPrincipal.getAmount().negate());
if (changedTransactionDetail != null) {
for (Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
// Trigger transaction replayed event
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
}
saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
if (StringUtils.isNotBlank(noteText)) {
changes.put("note", noteText);
@@ -52,6 +52,7 @@ import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.guarantor.GuarantorConstants;
import org.apache.fineract.portfolio.loanaccount.guarantor.domain.Guarantor;
@@ -85,6 +86,7 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService {
private final SavingsAccountAssembler savingsAccountAssembler;
private final ConfigurationDomainService configurationDomainService;
private final ExternalIdFactory externalIdFactory;
private final LoanRepository loanRepository;
@PostConstruct
public void addListeners() {
@@ -232,28 +234,27 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService {
final AccountTransferDetails accountTransferDetails = null;
final String noteText = null;
final SavingsAccount toSavingsAccount = null;
Long loanId = loan.getId();
for (Guarantor guarantor : existGuarantorList) {
final List<GuarantorFundingDetails> fundingDetails = guarantor.getGuarantorFundDetails();
for (GuarantorFundingDetails guarantorFundingDetails : fundingDetails) {
Loan freshLoan = loanRepository.findById(loanId).orElseThrow();
if (guarantorFundingDetails.getStatus().isActive()) {
final SavingsAccount fromSavingsAccount = guarantorFundingDetails.getLinkedSavingsAccount();
final Long fromAccountId = fromSavingsAccount.getId();
releaseLoanIds.put(loanId, guarantorFundingDetails.getId());
try {
BigDecimal remainingAmount = guarantorFundingDetails.getAmountRemaining();
if (loan.getGuaranteeAmount().compareTo(loan.getPrincipal().getAmount()) > 0) {
remainingAmount = remainingAmount.multiply(loan.getPrincipal().getAmount()).divide(loan.getGuaranteeAmount(),
MoneyHelper.getRoundingMode());
if (freshLoan.getGuaranteeAmount().compareTo(freshLoan.getPrincipal().getAmount()) > 0) {
remainingAmount = remainingAmount.multiply(freshLoan.getPrincipal().getAmount())
.divide(freshLoan.getGuaranteeAmount(), MoneyHelper.getRoundingMode());
}
ExternalId externalId = externalIdFactory.create();
AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, remainingAmount, fromAccountType,
toAccountType, fromAccountId, toAccountId, description, locale, fmt, paymentDetail, fromTransferType,
toTransferType, chargeId, loanInstallmentNumber, transferType, accountTransferDetails, noteText, externalId,
loan, toSavingsAccount, fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck);
null, null, fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck);
transferAmount(accountTransferDTO);
} finally {
releaseLoanIds.remove(loanId);
@@ -451,15 +451,21 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
// update the status of the request
loanRescheduleRequest.approve(appUser, approvedOnDate);
/***
* TODO Vishwas Batch save is giving me a HibernateOptimisticLockingFailureException, looping and saving for
* the time being, not a major issue for now as this loop is entered only in edge cases (when a adjustment
* is made before the latest payment recorded against the loan)
***/
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
// Trigger transaction replayed event
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
}
// update the loan object
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
// update the loan object
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
@@ -243,7 +243,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
if (isAppliedOnBackDate && loan.isFeeCompoundingEnabledForInterestRecalculation()) {
runScheduleRecalculation(loan, recalculateFrom);
loan = runScheduleRecalculation(loan, recalculateFrom);
reprocessRequired = false;
}
this.loanWritePlatformService.updateOriginalSchedule(loan);
@@ -252,14 +252,15 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions();
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
// Trigger transaction replayed event
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
}
loan = loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
}
loan = loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
@@ -760,7 +761,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
if (runInterestRecalculation && loan.isFeeCompoundingEnabledForInterestRecalculation()) {
runScheduleRecalculation(loan, recalculateFrom);
loan = runScheduleRecalculation(loan, recalculateFrom);
reprocessRequired = false;
}
this.loanWritePlatformService.updateOriginalSchedule(loan);
@@ -772,11 +773,13 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings()
.entrySet()) {
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
// Trigger transaction replayed event
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
}
loan = loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
@@ -1112,22 +1115,24 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
}
}
public void runScheduleRecalculation(final Loan loan, final LocalDate recalculateFrom) {
public Loan runScheduleRecalculation(Loan loan, final LocalDate recalculateFrom) {
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
final List<Long> existingTransactionIds = loan.findExistingTransactionIds();
ScheduleGeneratorDTO generatorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
ChangedTransactionDetail changedTransactionDetail = loan
.handleRegenerateRepaymentScheduleWithInterestRecalculation(generatorDTO);
this.loanRepositoryWrapper.save(loan);
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
// Trigger transaction replayed event
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
}
loan = loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
}
return loan;
}
private JsonCommand adaptLoanChargeRefundCommandForFurtherRepaymentProcessing(JsonCommand command, BigDecimal fullRefundAbleAmount) {
@@ -105,9 +105,7 @@ import org.apache.fineract.portfolio.account.domain.AccountAssociationsRepositor
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction;
import org.apache.fineract.portfolio.account.domain.AccountTransferTransaction;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionPriority;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
@@ -223,7 +221,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService;
private final LoanReadPlatformService loanReadPlatformService;
private final FromJsonHelper fromApiJsonHelper;
private final AccountTransferRepository accountTransferRepository;
private final CalendarRepository calendarRepository;
private final LoanScheduleHistoryWritePlatformService loanScheduleHistoryWritePlatformService;
private final LoanApplicationCommandFromApiJsonHelper loanApplicationCommandFromApiJsonHelper;
@@ -460,13 +457,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (!changes.isEmpty()) {
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
// Trigger transaction replayed event
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
}
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
final String noteText = command.stringValueOfParameterNamed("note");
@@ -745,8 +741,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings()
.entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
// Trigger transaction replayed event
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
@@ -949,7 +945,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (!txnExternalId.isEmpty()) {
changes.put(LoanApiConstants.externalIdParameterName, txnExternalId);
}
final Loan loan = this.loanAssembler.assembleFrom(loanId);
Loan loan = this.loanAssembler.assembleFrom(loanId);
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
final Boolean isHolidayValidationDone = false;
final HolidayDetailDTO holidayDetailDto = null;
@@ -958,7 +954,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(repaymentTransactionType, loan, transactionDate,
transactionAmount, paymentDetail, noteText, txnExternalId, isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer,
holidayDetailDto, isHolidayValidationDone);
loan = loanTransaction.getLoan();
// Update loan transaction on repayment.
if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) {
Set<LoanCollateralManagement> loanCollateralManagements = loan.getLoanCollateralManagements();
@@ -1156,7 +1152,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
***/
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
// Trigger transaction replayed event
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
@@ -1391,7 +1388,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
// Trigger transaction replayed event
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
@@ -2171,7 +2169,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
public CommandProcessingResult undoWriteOff(Long loanId) {
final Loan loan = this.loanAssembler.assembleFrom(loanId);
Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
@@ -2193,13 +2191,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
existingReversedTransactionIds, scheduleGeneratorDTO);
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
// Trigger transaction replayed event
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
}
saveLoanWithDataIntegrityViolationChecks(loan);
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
@@ -2340,9 +2338,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
if (command.entityId() != null && changedTransactionDetail != null) {
for (Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
// Trigger transaction replayed event
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
@@ -2400,12 +2398,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
// Trigger transaction replayed event
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
}
saveLoanWithDataIntegrityViolationChecks(loan);
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccountDomainService.recalculateAccruals(loan);
@@ -2441,14 +2440,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
}
private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) {
final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId);
if (transferTransaction != null) {
transferTransaction.updateToLoanTransaction(newLoanTransaction);
this.accountTransferRepository.save(transferTransaction);
}
}
private void createLoanScheduleArchive(final Loan loan, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO);