FINERACT-1927: Incorrect exception info in BatchAPI response

This commit is contained in:
jmarta
2023-09-27 19:13:26 +02:00
committed by Adam Saghy
parent f452ce2cad
commit 32da7db35d
55 changed files with 1399 additions and 1134 deletions
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.batch.command;
import static jakarta.ws.rs.HttpMethod.DELETE;
import static jakarta.ws.rs.HttpMethod.GET;
import static jakarta.ws.rs.HttpMethod.POST;
import static jakarta.ws.rs.HttpMethod.PUT;
@@ -207,7 +208,9 @@ public class CommandStrategyProvider {
.method(PUT).build(), "updateDatatableEntryOneToOneCommandStrategy");
commandStrategies.put(CommandContext
.resource("v1\\/datatables\\/" + ALPHANUMBERIC_WITH_UNDERSCORE_REGEX + "\\/" + NUMBER_REGEX + "\\/" + NUMBER_REGEX)
.method(PUT).build(), "updateDatatableEntryOneToManyCommandStrategy");
.method(DELETE).build(), "deleteDatatableEntryOneToManyCommandStrategy");
commandStrategies.put(CommandContext.resource("v1\\/datatables\\/" + ALPHANUMBERIC_WITH_UNDERSCORE_REGEX + "\\/" + NUMBER_REGEX)
.method(DELETE).build(), "deleteDatatableEntryOneToOneCommandStrategy");
commandStrategies.put(CommandContext
.resource("v1\\/datatables\\/" + ALPHANUMBERIC_WITH_UNDERSCORE_REGEX + "\\/" + NUMBER_REGEX + OPTIONAL_QUERY_PARAM_REGEX)
.method(GET).build(), "getDatatableEntryByAppTableIdCommandStrategy");
@@ -20,10 +20,13 @@ package org.apache.fineract.batch.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class ClientDetailsNotFoundException extends AbstractPlatformDomainRuleException {
public class BatchReferenceInvalidException extends AbstractPlatformDomainRuleException {
public ClientDetailsNotFoundException() {
super("validation.msg.batch.jlg.no.clients.defined", "No Client details found", "");
public BatchReferenceInvalidException() {
super("validation.msg.batch.root.invalid", "Root request not found");
}
public BatchReferenceInvalidException(Long reference) {
super("validation.msg.batch.reference.invalid", "Referenced request not found", reference);
}
}
@@ -21,17 +21,31 @@ package org.apache.fineract.batch.exception;
import static org.springframework.core.ResolvableType.forClassWithGenerics;
import com.google.gson.Gson;
import jakarta.persistence.PersistenceException;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import java.text.ParseException;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.naming.AuthenticationException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.fortuna.ical4j.validate.ValidationException;
import org.apache.commons.collections4.SetUtils;
import org.apache.fineract.batch.domain.Header;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.exceptionmapper.DefaultExceptionMapper;
import org.apache.fineract.infrastructure.core.exceptionmapper.FineractExceptionMapper;
import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.core.NestedRuntimeException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.stereotype.Component;
/**
@@ -55,7 +69,8 @@ public final class ErrorHandler {
private static final Gson JSON_HELPER = GoogleGsonSerializerHelper.createGsonBuilder(true).create();
private <T extends RuntimeException> ExceptionMapper<T> findMostSpecificExceptionHandler(T exception) {
@NotNull
public <T extends RuntimeException> ExceptionMapper<T> findMostSpecificExceptionHandler(T exception) {
Class<?> clazz = exception.getClass();
do {
Set<String> exceptionMappers = createSet(ctx.getBeanNamesForType(forClassWithGenerics(ExceptionMapper.class, clazz)));
@@ -71,6 +86,60 @@ public final class ErrorHandler {
return (ExceptionMapper<T>) defaultExceptionMapper;
}
/**
* Returns an object of ErrorInfo type containing the information regarding the raised error.
*
* @param exception
* @return ErrorInfo
*/
public ErrorInfo handle(@NotNull RuntimeException exception) {
ExceptionMapper<RuntimeException> exceptionMapper = findMostSpecificExceptionHandler(exception);
Response response = exceptionMapper.toResponse(exception);
MultivaluedMap<String, Object> headers = response.getHeaders();
Set<Header> batchHeaders = headers == null ? null
: headers.keySet().stream().map(e -> new Header(e, response.getHeaderString(e))).collect(Collectors.toSet());
return new ErrorInfo(response.getStatus(), ((FineractExceptionMapper) exceptionMapper).errorCode(),
JSON_HELPER.toJson(response.getEntity()), batchHeaders);
}
public RuntimeException getMappable(@NotNull Throwable thr) {
return getMappable(thr, null, null, null);
}
public RuntimeException getMappable(@NotNull Throwable t, String msgCode, String defaultMsg, String param,
final Object... defaultMsgArgs) {
String msg = defaultMsg == null ? t.getMessage() : defaultMsg;
param = param == null ? "unknown" : param;
if (t instanceof NestedRuntimeException nre) {
Throwable cause = nre.getMostSpecificCause();
msg = cause.getMessage();
if (nre instanceof NonTransientDataAccessException) {
msgCode = msgCode == null ? "error.msg." + param + ".data.integrity.issue" : msgCode;
return new PlatformDataIntegrityException(msgCode, msg, param, defaultMsgArgs);
}
}
if (t instanceof ValidationException) {
msgCode = msgCode == null ? "error.msg.validation." + param + ".error" : msgCode;
return new PlatformApiDataValidationException(List.of(ApiParameterError.parameterError(msgCode, msg, param, defaultMsgArgs)));
}
if (t instanceof PersistenceException) {
msgCode = msgCode == null ? "error.msg.persistence." + param + ".error" : msgCode;
return new PlatformDataIntegrityException(msgCode, msg, param, defaultMsgArgs);
}
if (t instanceof AuthenticationException) {
msgCode = msgCode == null ? "error.msg.authentication." + param + ".error" : msgCode;
return new PlatformDataIntegrityException(msgCode, msg, param, defaultMsgArgs);
}
if (t instanceof RuntimeException re) {
return re;
}
if (t instanceof ParseException) {
msgCode = msgCode == null ? "error.msg.parse." + param + ".error" : msgCode;
return new PlatformDataIntegrityException(msgCode, msg, param, defaultMsgArgs);
}
return new RuntimeException(msg, t);
}
private <T> Set<T> createSet(T[] array) {
if (array == null) {
return Set.of();
@@ -78,17 +147,4 @@ public final class ErrorHandler {
return Set.of(array);
}
}
/**
* Returns an object of ErrorInfo type containing the information regarding the raised error.
*
* @param exception
* @return ErrorInfo
*/
public ErrorInfo handle(final RuntimeException exception) {
ExceptionMapper<RuntimeException> exceptionMapper = findMostSpecificExceptionHandler(exception);
Response response = exceptionMapper.toResponse(exception);
return new ErrorInfo(response.getStatus(), ((FineractExceptionMapper) exceptionMapper).errorCode(),
JSON_HELPER.toJson(response.getEntity()));
}
}
@@ -18,6 +18,15 @@
*/
package org.apache.fineract.batch.exception;
import static lombok.AccessLevel.PROTECTED;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.batch.domain.Header;
/**
* Provides members to hold the basic information about the exceptions raised in commandStrategy classes.
*
@@ -25,84 +34,14 @@ package org.apache.fineract.batch.exception;
*
* @see ErrorHandler
*/
@Getter
@Setter(PROTECTED)
@NoArgsConstructor(access = PROTECTED)
@AllArgsConstructor
public final class ErrorInfo {
private Integer statusCode;
private Integer errorCode;
private String message;
/**
* Constructor to initialize the members of this class.
*
* @param statusCode
* @param errorCode
* @param message
*/
public ErrorInfo(final Integer statusCode, final Integer errorCode, final String message) {
this.statusCode = statusCode;
this.errorCode = errorCode;
this.message = message;
}
/**
* Constructor so JSON serialization will work with out special Serialiazer
*/
ErrorInfo() {
}
/**
* Getter method to provide the statusCode for an object of this type.
*
* @return Integer
*/
public Integer getStatusCode() {
return this.statusCode;
}
/**
* Setter method to set the statusCode for an object of this type.
*
* @param statusCode
*/
public void setStatusCode(final Integer statusCode) {
this.statusCode = statusCode;
}
/**
* Getter method to provide the errorCode for an object of this type.
*
* @return Integer
*/
public Integer getErrorCode() {
return this.errorCode;
}
/**
* Setter method to set the errorCode for an object of this type.
*
* @param errorCode
*/
public void setErrorCode(final Integer errorCode) {
this.errorCode = errorCode;
}
/**
* Getter method to provide the message of the error for an object of this type.
*
* @return String
*/
public String getMessage() {
return this.message;
}
/**
* Setter method to set the message of the error for an object of this type.
*
* @param message
*/
public void setMessage(final String message) {
this.message = message;
}
private Set<Header> headers;
}
@@ -18,23 +18,23 @@
*/
package org.apache.fineract.batch.service;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import static org.apache.http.HttpStatus.SC_OK;
import com.google.gson.Gson;
import com.jayway.jsonpath.JsonPathException;
import io.github.resilience4j.core.functions.Either;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.UriInfo;
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.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -47,25 +47,25 @@ import org.apache.fineract.batch.command.CommandStrategyProvider;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.batch.domain.Header;
import org.apache.fineract.batch.exception.ClientDetailsNotFoundException;
import org.apache.fineract.batch.exception.BatchReferenceInvalidException;
import org.apache.fineract.batch.exception.ErrorHandler;
import org.apache.fineract.batch.exception.ErrorInfo;
import org.apache.fineract.batch.service.ResolutionHelper.BatchRequestNode;
import org.apache.fineract.infrastructure.core.domain.BatchRequestContextHolder;
import org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessFailedException;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessSucceedException;
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.ConcurrencyFailureException;
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.TransactionStatus;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.transaction.support.TransactionTemplate;
/**
@@ -94,6 +94,64 @@ public class BatchApiServiceImpl implements BatchApiService {
@PersistenceContext
private final EntityManager entityManager;
/**
* 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) {
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(Function.identity()::apply, () -> handleBatchRequests(true, requestList, uriInfo));
}
/**
* 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 {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionConfigurator.accept(transactionTemplate);
return transactionTemplate.execute(status -> {
BatchRequestContextHolder.setEnclosingTransaction(Optional.of(status));
try {
responseList.addAll(request.get());
return responseList;
} catch (BatchExecutionException ex) {
status.setRollbackOnly();
return List.of(buildErrorResponse(ex.getCause(), ex.getRequest()));
} catch (RuntimeException ex) {
status.setRollbackOnly();
return buildErrorResponses(ex, responseList);
}
});
} catch (TransactionException | NonTransientDataAccessException ex) {
return buildErrorResponses(ex, responseList);
}
}
/**
* Returns the response list by getting a proper {@link org.apache.fineract.batch.command.CommandStrategy}.
* execute() method of acquired commandStrategy is then provided with the separate Request.
@@ -104,29 +162,24 @@ public class BatchApiServiceImpl implements BatchApiService {
*/
private List<BatchResponse> handleBatchRequests(boolean enclosingTransaction, final List<BatchRequest> requestList,
final UriInfo uriInfo) {
final List<BatchResponse> responseList = new ArrayList<>(requestList.size());
final List<BatchRequestNode> batchRequestNodes = this.resolutionHelper.getDependingRequests(requestList);
if (batchRequestNodes.isEmpty()) {
final BatchResponse response = new BatchResponse();
ErrorInfo ex = errorHandler.handle(new ClientDetailsNotFoundException());
response.setStatusCode(500);
response.setBody(ex.getMessage());
responseList.add(response);
return responseList;
final List<BatchRequestNode> rootNodes;
try {
rootNodes = this.resolutionHelper.buildNodesTree(requestList);
} catch (BatchReferenceInvalidException e) {
return List.of(buildErrorResponse(e));
}
for (BatchRequestNode rootNode : batchRequestNodes) {
final ArrayList<BatchResponse> responseList = new ArrayList<>(requestList.size());
for (BatchRequestNode rootNode : rootNodes) {
if (enclosingTransaction) {
this.callRequestRecursive(rootNode.getRequest(), rootNode, responseList, uriInfo, enclosingTransaction);
} else {
List<BatchResponse> localResponseList = new ArrayList<>();
ArrayList<BatchResponse> localResponseList = new ArrayList<>();
this.callRequestRecursive(rootNode.getRequest(), rootNode, localResponseList, uriInfo, enclosingTransaction);
responseList.addAll(localResponseList);
}
}
Collections.sort(responseList, Comparator.comparing(BatchResponse::getRequestId));
responseList.sort(Comparator.comparing(BatchResponse::getRequestId));
return responseList;
}
@@ -143,7 +196,7 @@ public class BatchApiServiceImpl implements BatchApiService {
*/
private void callRequestRecursive(BatchRequest request, BatchRequestNode requestNode, List<BatchResponse> responseList, UriInfo uriInfo,
boolean enclosingTransaction) {
// 1. run current node
// run current node
BatchResponse response;
if (enclosingTransaction) {
response = executeRequest(request, uriInfo);
@@ -154,54 +207,28 @@ public class BatchApiServiceImpl implements BatchApiService {
response = transactionResponse.get(0);
}
responseList.add(response);
if (response.getStatusCode() != null && response.getStatusCode() == 200) {
requestNode.getChildRequests().forEach(childNode -> {
if (response.getStatusCode() != null && response.getStatusCode() == SC_OK) {
// run child nodes
requestNode.getChildNodes().forEach(childNode -> {
BatchRequest childRequest = childNode.getRequest();
BatchRequest resolvedChildRequest;
try {
resolvedChildRequest = this.resolutionHelper.resoluteRequest(childNode.getRequest(), response);
} catch (JsonPathException jsonPathException) {
BatchResponse childResponse = new BatchResponse().setRequestId(childNode.getRequest().getRequestId())
.setHeaders(childNode.getRequest().getHeaders()).setStatusCode(BAD_REQUEST.value())
.setBody(jsonPathException.getMessage());
responseList.add(childResponse);
resolvedChildRequest = this.resolutionHelper.resolveRequest(childRequest, response);
} catch (JsonPathException jpex) {
responseList.add(buildErrorResponse(jpex, childRequest));
return;
} catch (RuntimeException ex) {
throw new BatchExecutionException(childNode.getRequest(), ex, errorHandler.handle(ex));
throw new BatchExecutionException(childRequest, ex);
}
callRequestRecursive(resolvedChildRequest, childNode, responseList, uriInfo, enclosingTransaction);
});
} else {
responseList.addAll(parentRequestFailedRecursive(request, requestNode));
responseList.addAll(parentRequestFailedRecursive(request, requestNode, response, true));
}
// 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
*
@@ -215,7 +242,7 @@ public class BatchApiServiceImpl implements BatchApiService {
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(), errorHandler.handle(preprocessorResult.getLeft()));
throw new BatchExecutionException(request, preprocessorResult.getLeft());
} else {
request = preprocessorResult.get();
}
@@ -224,13 +251,8 @@ public class BatchApiServiceImpl implements BatchApiService {
.map(list -> list.stream().collect(Collectors.toMap(Header::getName, Header::getValue)))
.orElse(Collections.emptyMap())));
BatchCallHandler callHandler = new BatchCallHandler(this.batchFilters, commandStrategy::execute);
if (BatchRequestContextHolder.getEnclosingTransaction().isPresent()) {
if (BatchRequestContextHolder.getEnclosingTransaction().get().isRollbackOnly()) {
BatchResponse br = new BatchResponse();
br.setBody("Parent request was erroneous!");
br.setRequestId(request.getRequestId());
return br;
}
Optional<TransactionStatus> transaction = BatchRequestContextHolder.getEnclosingTransaction();
if (transaction.isPresent()) {
entityManager.flush();
}
final BatchResponse rootResponse = callHandler.serviceCall(request, uriInfo);
@@ -238,9 +260,9 @@ public class BatchApiServiceImpl implements BatchApiService {
request.getRelativeUrl());
return rootResponse;
} catch (AbstractIdempotentCommandException idempotentException) {
return handleIdempotentRequests(idempotentException, request);
return buildErrorResponse(idempotentException, request);
} catch (RuntimeException ex) {
throw new BatchExecutionException(request, ex, errorHandler.handle(ex));
throw new BatchExecutionException(request, ex);
} finally {
BatchRequestContextHolder.resetRequestAttributes();
}
@@ -266,126 +288,97 @@ public class BatchApiServiceImpl implements BatchApiService {
}
/**
* Return the idempotent response when idempotent exception raised
* All requests recursively are set to status 409 if the parent request fails.
*
* @param idempotentException
* the idempotent exception
* @param request
* the called request
* @return
* the current request
* @param requestNode
* the current request node
* @return {@code BatchResponse} list of the generated batch responses
*/
private BatchResponse handleIdempotentRequests(AbstractIdempotentCommandException idempotentException, BatchRequest request) {
BatchResponse response = new BatchResponse();
response.setRequestId(request.getRequestId());
response.setHeaders(Optional.ofNullable(request.getHeaders()).orElse(new HashSet<>()));
response.getHeaders().add(new Header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER, "true"));
response.setBody(idempotentException.getResponse());
if (idempotentException instanceof IdempotentCommandProcessSucceedException) {
response.setStatusCode(200);
} else if (idempotentException instanceof IdempotentCommandProcessUnderProcessingException) {
response.setStatusCode(409);
} else if (idempotentException instanceof IdempotentCommandProcessFailedException) {
response.setStatusCode(((IdempotentCommandProcessFailedException) idempotentException).getStatusCode());
private List<BatchResponse> parentRequestFailedRecursive(BatchRequest request, BatchRequestNode requestNode, BatchResponse response,
boolean root) {
List<BatchResponse> responseList = new ArrayList<>();
if (root) {
BatchRequestContextHolder.getEnclosingTransaction().ifPresent(TransactionExecution::setRollbackOnly);
} else {
response.setStatusCode(500);
responseList.add(buildErrorResponse(request.getRequestId(), response.getStatusCode(), response.getBody(), null));
}
return response;
}
/**
* 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) {
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));
requestNode.getChildNodes().forEach(
childNode -> responseList.addAll(parentRequestFailedRecursive(childNode.getRequest(), childNode, response, false)));
return responseList;
}
@NotNull
private List<BatchResponse> createErrorResponse(List<BatchResponse> responseList, int statusCode) {
BatchResponse errResponse = new BatchResponse();
for (BatchResponse res : responseList) {
if (res.getStatusCode() == null || !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) {
if (res.getStatusCode() != null) {
statusCode = res.getStatusCode();
} else {
statusCode = Status.INTERNAL_SERVER_ERROR.getStatusCode();
}
}
break;
}
}
errResponse.setStatusCode(statusCode);
return Arrays.asList(errResponse);
}
private List<BatchResponse> callInTransaction(Supplier<List<BatchResponse>> request) {
return callInTransaction(Function.identity()::apply, request);
private BatchResponse buildErrorResponse(@NotNull Throwable ex) {
return buildErrorResponse(ex, null);
}
/**
* Helper method to run the command in transaction
* Return the response when any exception raised
*
* @param ex
* the exception
* @param request
* the enclosing supplier of the command
* @param transactionConfigurator
* consumer to configure the transaction behavior and isolation
* @return
* the called request
*/
private List<BatchResponse> callInTransaction(Consumer<TransactionTemplate> transactionConfigurator,
Supplier<List<BatchResponse>> request) {
List<BatchResponse> responseList = new ArrayList<>();
try {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionConfigurator.accept(transactionTemplate);
return transactionTemplate.execute(status -> {
BatchRequestContextHolder.setEnclosingTransaction(Optional.of(status));
try {
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.handle(ex);
BatchResponse errResponse = new BatchResponse();
errResponse.setStatusCode(e.getStatusCode());
errResponse.setBody(e.getMessage());
return Arrays.asList(errResponse);
}
});
} catch (TransactionException | NonTransientDataAccessException ex) {
ErrorInfo e = errorHandler.handle(ex);
return createErrorResponse(responseList, e.getStatusCode());
private BatchResponse buildErrorResponse(Throwable ex, BatchRequest request) {
Long requestId = null;
Integer statusCode = null;
String body = null;
Set<Header> headers = new HashSet<>();
if (ex != null) {
ErrorInfo errorInfo = errorHandler.handle(errorHandler.getMappable(ex));
statusCode = errorInfo.getStatusCode();
body = errorInfo.getMessage();
headers = Optional.ofNullable(errorInfo.getHeaders()).orElse(new HashSet<>());
}
if (request != null) {
requestId = request.getRequestId();
if (request.getHeaders() != null) {
headers.addAll(request.getHeaders());
}
}
return buildErrorResponse(requestId, statusCode, body, headers);
}
@NotNull
private List<BatchResponse> buildErrorResponses(Throwable ex, @NotNull List<BatchResponse> responseList) {
BatchResponse response = responseList.isEmpty() ? null
: responseList.stream().filter(e -> e.getStatusCode() == null || e.getStatusCode() != SC_OK).findFirst()
.orElse(responseList.get(responseList.size() - 1));
if (response != null && response.getStatusCode() == SC_OK && ex instanceof TransactionSystemException tse) {
ex = new ConcurrencyFailureException(tse.getMessage(), tse.getCause());
}
Long requestId = null;
Integer statusCode = null;
String body = null;
Set<Header> headers = new HashSet<>();
if (ex != null) {
ErrorInfo errorInfo = errorHandler.handle(errorHandler.getMappable(ex));
statusCode = errorInfo.getStatusCode();
body = errorInfo.getMessage();
headers = Optional.ofNullable(errorInfo.getHeaders()).orElse(new HashSet<>());
}
if (response != null) {
requestId = response.getRequestId();
if (response.getStatusCode() == null || response.getStatusCode() != SC_OK) {
if (response.getStatusCode() != null) {
statusCode = response.getStatusCode();
}
body = "Transaction is being rolled back. First erroneous request: \n" + new Gson().toJson(response);
}
if (response.getHeaders() != null) {
headers.addAll(response.getHeaders());
}
}
return List.of(buildErrorResponse(requestId, statusCode, body, headers));
}
private BatchResponse buildErrorResponse(Long requestId, Integer statusCode, String body, Set<Header> headers) {
return new BatchResponse().setRequestId(requestId).setStatusCode(statusCode == null ? SC_INTERNAL_SERVER_ERROR : statusCode)
.setBody(body == null ? "Request with id " + requestId + " was erroneous!" : body).setHeaders(headers);
}
}
@@ -20,17 +20,14 @@ package org.apache.fineract.batch.service;
import lombok.Getter;
import org.apache.fineract.batch.domain.BatchRequest;
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, ErrorInfo errorInfo) {
public BatchExecutionException(BatchRequest request, RuntimeException ex) {
super("Error executing batch request: " + request, ex);
this.request = request;
this.errorInfo = errorInfo;
}
}
@@ -30,6 +30,7 @@ import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.batch.exception.BatchReferenceInvalidException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.springframework.stereotype.Component;
@@ -55,23 +56,19 @@ public class ResolutionHelper {
private BatchRequest request;
private final List<BatchRequestNode> childRequests = new ArrayList<>();
public BatchRequestNode() {
public BatchRequestNode(BatchRequest request) {
this.request = request;
}
public BatchRequest getRequest() {
return this.request;
}
public void setRequest(BatchRequest request) {
this.request = request;
}
public List<BatchRequestNode> getChildRequests() {
public List<BatchRequestNode> getChildNodes() {
return this.childRequests;
}
public void addChildRequest(final BatchRequestNode batchRequest) {
public void addChildNode(final BatchRequestNode batchRequest) {
this.childRequests.add(batchRequest);
}
@@ -84,35 +81,37 @@ public class ResolutionHelper {
* different list is identified with a "Key" which is the "requestId" of the request at topmost level in dependency
* hierarchy of that particular list.
*
* @param batchRequests
* @param requests
* @return List&lt;ArrayList&lt;BatchRequestNode&gt;&gt;
*/
public List<BatchRequestNode> getDependingRequests(final List<BatchRequest> batchRequests) {
final List<BatchRequestNode> rootRequests = new ArrayList<>();
for (BatchRequest batchRequest : batchRequests) {
if (batchRequest.getReference() == null) {
final BatchRequestNode node = new BatchRequestNode();
node.setRequest(batchRequest);
rootRequests.add(node);
public List<BatchRequestNode> buildNodesTree(final List<BatchRequest> requests) {
final List<BatchRequestNode> rootNodes = new ArrayList<>();
for (BatchRequest request : requests) {
if (request.getReference() == null) {
final BatchRequestNode node = new BatchRequestNode(request);
rootNodes.add(node);
} else {
this.addDependingRequest(batchRequest, rootRequests);
if (!addDependingRequest(request, rootNodes)) {
throw new BatchReferenceInvalidException(request.getReference());
}
}
}
return rootRequests;
return rootNodes;
}
private void addDependingRequest(final BatchRequest batchRequest, final List<BatchRequestNode> parentRequests) {
for (BatchRequestNode batchRequestNode : parentRequests) {
if (batchRequestNode.getRequest().getRequestId().equals(batchRequest.getReference())) {
final BatchRequestNode dependingRequest = new BatchRequestNode();
dependingRequest.setRequest(batchRequest);
batchRequestNode.addChildRequest(dependingRequest);
private boolean addDependingRequest(final BatchRequest request, final List<BatchRequestNode> parentNodes) {
for (BatchRequestNode parentNode : parentNodes) {
if (parentNode.getRequest().getRequestId().equals(request.getReference())) {
final BatchRequestNode childNode = new BatchRequestNode(request);
parentNode.addChildNode(childNode);
return true;
} else {
addDependingRequest(batchRequest, batchRequestNode.getChildRequests());
if (addDependingRequest(request, parentNode.getChildNodes())) {
return true;
}
}
}
return false;
}
/**
@@ -123,34 +122,27 @@ public class ResolutionHelper {
* @param parentResponse
* @return BatchRequest
*/
public BatchRequest resoluteRequest(final BatchRequest request, final BatchResponse parentResponse) {
// Create a duplicate request
final BatchRequest br = request;
public BatchRequest resolveRequest(final BatchRequest request, final BatchResponse parentResponse) {
final ReadContext responseCtx = JsonPath.parse(parentResponse.getBody());
// Gets the body from current Request as a JsonObject
final JsonObject jsonRequestBody = this.fromJsonHelper.parse(request.getBody()).getAsJsonObject();
JsonObject jsonResultBody = new JsonObject();
// Iterate through each element in the requestBody to find dependent
// parameter
for (Map.Entry<String, JsonElement> element : jsonRequestBody.entrySet()) {
final String key = element.getKey();
final JsonElement value = resolveDependentVariables(element, responseCtx);
jsonResultBody.add(key, value);
String requestBody = request.getBody();
if (requestBody != null) {
final JsonObject jsonRequestBody = this.fromJsonHelper.parse(requestBody).getAsJsonObject();
JsonObject jsonResultBody = new JsonObject();
// Iterate through each element in the requestBody to find dependent
// parameter
for (Map.Entry<String, JsonElement> element : jsonRequestBody.entrySet()) {
final String key = element.getKey();
final JsonElement value = resolveDependentVariables(element, responseCtx);
jsonResultBody.add(key, value);
}
// Set the body after dependency resolution
request.setBody(jsonResultBody.toString());
}
// Set the body after dependency resolution
br.setBody(jsonResultBody.toString());
// Also check the relativeUrl for any dependency resolution
String relativeUrl = request.getRelativeUrl();
if (relativeUrl.contains("$.")) {
String queryParams = "";
if (relativeUrl.contains("?")) {
queryParams = relativeUrl.substring(relativeUrl.indexOf("?"));
@@ -158,24 +150,21 @@ public class ResolutionHelper {
}
final Iterable<String> parameters = Splitter.on('/').split(relativeUrl);
for (String parameter : parameters) {
if (parameter.contains("$.")) {
final String resParamValue = responseCtx.read(parameter).toString();
relativeUrl = relativeUrl.replace(parameter, resParamValue);
br.setRelativeUrl(relativeUrl + queryParams);
request.setRelativeUrl(relativeUrl + queryParams);
}
}
}
return br;
return request;
}
private JsonElement resolveDependentVariables(final Map.Entry<String, JsonElement> entryElement, final ReadContext responseCtx) {
JsonElement value = null;
JsonElement value;
final JsonElement element = entryElement.getValue();
if (element.isJsonObject()) {
final JsonObject jsObject = element.getAsJsonObject();
value = processJsonObject(jsObject, responseCtx);
@@ -202,16 +191,13 @@ public class ResolutionHelper {
}
private JsonArray processJsonArray(final JsonArray elementArray, final ReadContext responseCtx) {
JsonArray valueArr = new JsonArray();
for (JsonElement element : elementArray) {
if (element.isJsonObject()) {
final JsonObject jsObject = element.getAsJsonObject();
valueArr.add(processJsonObject(jsObject, responseCtx));
}
}
return valueArr;
}
@@ -123,16 +123,6 @@ public class CommandSource extends AbstractPersistableCustom {
@Column(name = "result_status_code")
private Integer resultStatusCode;
public static CommandSource fullEntryFrom(final CommandWrapper wrapper, final JsonCommand command, final AppUser maker,
String idempotencyKey, Integer status) {
return new CommandSource(wrapper.actionName(), wrapper.entityName(), wrapper.getHref(), command.entityId(), command.subentityId(),
command.json(), maker, idempotencyKey, status);
}
protected CommandSource() {
//
}
private CommandSource(final String actionName, final String entityName, final String href, final Long resourceId,
final Long subResourceId, final String commandSerializedAsJson, final AppUser maker, final String idempotencyKey,
final Integer status) {
@@ -148,6 +138,26 @@ public class CommandSource extends AbstractPersistableCustom {
this.idempotencyKey = idempotencyKey;
}
public static CommandSource fullEntryFrom(final CommandWrapper wrapper, final JsonCommand command, final AppUser maker,
String idempotencyKey, Integer status) {
CommandSource commandSource = new CommandSource(wrapper.actionName(), wrapper.entityName(), wrapper.getHref(), command.entityId(),
command.subentityId(), command.json(), maker, idempotencyKey, status);
commandSource.officeId = wrapper.getOfficeId();
commandSource.groupId = command.getGroupId();
commandSource.clientId = command.getClientId();
commandSource.loanId = command.getLoanId();
commandSource.savingsId = command.getSavingsId();
commandSource.productId = command.getProductId();
commandSource.transactionId = command.getTransactionId();
commandSource.creditBureauId = command.getCreditBureauId();
commandSource.organisationCreditBureauId = command.getOrganisationCreditBureauId();
return commandSource;
}
protected CommandSource() {
//
}
public Long getCreditBureauId() {
return this.creditBureauId;
}
@@ -160,31 +170,27 @@ public class CommandSource extends AbstractPersistableCustom {
return this.organisationCreditBureauId;
}
public String getJobName() {
return this.jobName;
}
public void setOrganisationCreditBureauId(Long organisationCreditBureauId) {
this.organisationCreditBureauId = organisationCreditBureauId;
}
public void markAsChecked(final AppUser checker) {
this.checker = checker;
this.checkedOnDate = DateUtils.getOffsetDateTimeOfTenantWithMostPrecision();
this.status = CommandProcessingResultType.PROCESSED.getValue();
public String getJobName() {
return this.jobName;
}
public void markAsRejected(final AppUser checker) {
this.checker = checker;
this.checkedOnDate = DateUtils.getOffsetDateTimeOfTenantWithMostPrecision();
this.status = CommandProcessingResultType.REJECTED.getValue();
public Long getResourceId() {
return this.resourceId;
}
public void updateResourceId(final Long resourceId) {
public void setResourceId(final Long resourceId) {
this.resourceId = resourceId;
}
public void updateSubResourceId(final Long subResourceId) {
public Long getSubResourceId() {
return this.subResourceId;
}
public void setSubResourceId(final Long subResourceId) {
this.subResourceId = subResourceId;
}
@@ -196,14 +202,6 @@ public class CommandSource extends AbstractPersistableCustom {
this.commandAsJson = json;
}
public Long resourceId() {
return this.resourceId;
}
public Long subResourceId() {
return this.subResourceId;
}
public String getActionName() {
return this.actionName;
}
@@ -216,36 +214,6 @@ public class CommandSource extends AbstractPersistableCustom {
return this.actionName + "_" + this.entityName;
}
public Long getResourceId() {
return this.resourceId;
}
public Long getSubResourceId() {
return this.subResourceId;
}
public void markAsAwaitingApproval() {
this.status = CommandProcessingResultType.AWAITING_APPROVAL.getValue();
}
public boolean isMarkedAsAwaitingApproval() {
return this.status.equals(CommandProcessingResultType.AWAITING_APPROVAL.getValue());
}
public void updateForAudit(final CommandProcessingResult result) {
this.officeId = result.getOfficeId();
this.groupId = result.getGroupId();
this.clientId = result.getClientId();
this.loanId = result.getLoanId();
this.savingsId = result.getSavingsId();
this.productId = result.getProductId();
this.transactionId = result.getTransactionId();
this.resourceId = result.getResourceId();
this.resourceExternalId = result.getResourceExternalId();
this.subResourceId = result.getSubResourceId();
this.subResourceExternalId = result.getSubResourceExternalId();
}
public String getResourceGetUrl() {
return this.resourceGetUrl;
}
@@ -296,7 +264,7 @@ public class CommandSource extends AbstractPersistableCustom {
return this.transactionId;
}
public void updateTransaction(final String transactionId) {
public void setTransactionId(final String transactionId) {
this.transactionId = transactionId;
}
@@ -324,6 +292,10 @@ public class CommandSource extends AbstractPersistableCustom {
this.status = status;
}
public void setStatus(CommandProcessingResultType status) {
setStatus(status == null ? null : status.getValue());
}
public Integer getResultStatusCode() {
return resultStatusCode;
}
@@ -331,4 +303,38 @@ public class CommandSource extends AbstractPersistableCustom {
public void setResultStatusCode(Integer resultStatusCode) {
this.resultStatusCode = resultStatusCode;
}
public void markAsAwaitingApproval() {
this.status = CommandProcessingResultType.AWAITING_APPROVAL.getValue();
}
public boolean isMarkedAsAwaitingApproval() {
return this.status.equals(CommandProcessingResultType.AWAITING_APPROVAL.getValue());
}
public void markAsChecked(final AppUser checker) {
this.checker = checker;
this.checkedOnDate = DateUtils.getOffsetDateTimeOfTenantWithMostPrecision();
this.status = CommandProcessingResultType.PROCESSED.getValue();
}
public void markAsRejected(final AppUser checker) {
this.checker = checker;
this.checkedOnDate = DateUtils.getOffsetDateTimeOfTenantWithMostPrecision();
this.status = CommandProcessingResultType.REJECTED.getValue();
}
public void updateForAudit(final CommandProcessingResult result) {
this.officeId = result.getOfficeId();
this.groupId = result.getGroupId();
this.clientId = result.getClientId();
this.loanId = result.getLoanId();
this.savingsId = result.getSavingsId();
this.productId = result.getProductId();
this.transactionId = result.getTransactionId();
this.resourceId = result.getResourceId();
this.resourceExternalId = result.getResourceExternalId();
this.subResourceId = result.getSubResourceId();
this.subResourceExternalId = result.getSubResourceExternalId();
}
}
@@ -302,6 +302,10 @@ public class CommandWrapper {
return this.taskPermissionName;
}
public Long getOfficeId() {
return officeId;
}
public Long getGroupId() {
return this.groupId;
}
@@ -18,7 +18,6 @@
*/
package org.apache.fineract.commands.service;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.ERROR;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.UNDER_PROCESSING;
import lombok.RequiredArgsConstructor;
@@ -48,46 +47,48 @@ public class CommandSourceService {
private final CommandSourceRepository commandSourceRepository;
private final ErrorHandler errorHandler;
@NotNull
@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);
public CommandSource saveInitialNewTransaction(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
return saveInitial(wrapper, jsonCommand, maker, idempotencyKey);
}
@NotNull
private CommandSource saveInitialInternal(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
CommandSource initialCommandSource = getInitialCommandSource(wrapper, jsonCommand, maker, idempotencyKey);
@Transactional(propagation = Propagation.REQUIRED)
public CommandSource saveInitialSameTransaction(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
return saveInitial(wrapper, jsonCommand, maker, idempotencyKey);
}
@NotNull
private CommandSource saveInitial(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
CommandSource initialCommandSource = getInitialCommandSource(wrapper, jsonCommand, maker, idempotencyKey);
if (initialCommandSource.getCommandJson() == null) {
initialCommandSource.setCommandJson("{}");
}
return commandSourceRepository.saveAndFlush(initialCommandSource);
}
public void saveFailed(CommandSource commandSource) {
commandSource.setStatus(ERROR.getValue());
commandSourceRepository.saveAndFlush(commandSource);
}
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
public CommandSource saveResult(CommandSource commandSource) {
public CommandSource saveResultNewTransaction(@NotNull CommandSource commandSource) {
return saveResult(commandSource);
}
@Transactional(propagation = Propagation.REQUIRED)
public CommandSource saveResultSameTransaction(@NotNull CommandSource commandSource) {
return saveResult(commandSource);
}
@NotNull
private CommandSource saveResult(@NotNull CommandSource commandSource) {
return commandSourceRepository.saveAndFlush(commandSource);
}
public CommandSource saveResultNoTransaction(CommandSource commandSource) {
return commandSourceRepository.saveAndFlush(commandSource);
public ErrorInfo generateErrorInfo(Throwable t) {
return errorHandler.handle(errorHandler.getMappable(t));
}
public ErrorInfo generateErrorException(Throwable t) {
if (t instanceof final RuntimeException e) {
return errorHandler.handle(e);
} else {
return new ErrorInfo(500, 9999, "{\"Exception\": " + t.toString() + "}");
}
public CommandSource getCommandSource(Long commandSourceId) {
return commandSourceRepository.findById(commandSourceId).orElseThrow(() -> new CommandNotFoundException(commandSourceId));
}
public CommandSource findCommandSource(CommandWrapper wrapper, String idempotencyKey) {
@@ -82,15 +82,15 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
validateIsUpdateAllowed();
final CommandWrapper wrapper = CommandWrapper.fromExistingCommand(makerCheckerId, commandSourceInput.getActionName(),
commandSourceInput.getEntityName(), commandSourceInput.resourceId(), commandSourceInput.subResourceId(),
commandSourceInput.getEntityName(), commandSourceInput.getResourceId(), commandSourceInput.getSubResourceId(),
commandSourceInput.getResourceGetUrl(), commandSourceInput.getProductId(), commandSourceInput.getOfficeId(),
commandSourceInput.getGroupId(), commandSourceInput.getClientId(), commandSourceInput.getLoanId(),
commandSourceInput.getSavingsId(), commandSourceInput.getTransactionId(), commandSourceInput.getCreditBureauId(),
commandSourceInput.getOrganisationCreditBureauId(), commandSourceInput.getIdempotencyKey());
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(commandSourceInput.getCommandJson());
final JsonCommand command = JsonCommand.fromExistingCommand(makerCheckerId, commandSourceInput.getCommandJson(), parsedCommand,
this.fromApiJsonHelper, commandSourceInput.getEntityName(), commandSourceInput.resourceId(),
commandSourceInput.subResourceId(), commandSourceInput.getGroupId(), commandSourceInput.getClientId(),
this.fromApiJsonHelper, commandSourceInput.getEntityName(), commandSourceInput.getResourceId(),
commandSourceInput.getSubResourceId(), commandSourceInput.getGroupId(), commandSourceInput.getClientId(),
commandSourceInput.getLoanId(), commandSourceInput.getSavingsId(), commandSourceInput.getTransactionId(),
commandSourceInput.getResourceGetUrl(), commandSourceInput.getProductId(), commandSourceInput.getCreditBureauId(),
commandSourceInput.getOrganisationCreditBureauId(), commandSourceInput.getJobName());
@@ -21,6 +21,7 @@ package org.apache.fineract.commands.service;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.ERROR;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.PROCESSED;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.UNDER_PROCESSING;
import static org.apache.http.HttpStatus.SC_OK;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
@@ -32,6 +33,7 @@ import java.util.Map;
import java.util.function.Function;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.batch.exception.ErrorHandler;
import org.apache.fineract.batch.exception.ErrorInfo;
import org.apache.fineract.commands.domain.CommandProcessingResultType;
import org.apache.fineract.commands.domain.CommandSource;
@@ -79,6 +81,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
private final IdempotencyKeyResolver idempotencyKeyResolver;
private final IdempotencyKeyGenerator idempotencyKeyGenerator;
private final CommandSourceService commandSourceService;
private final ErrorHandler errorHandler;
private final FineractRequestContextHolder fineractRequestContextHolder;
private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson();
@@ -90,90 +93,95 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
// Do not store the idempotency key because of the exception handling
setIdempotencyKeyStoreFlag(false);
final boolean rollbackTransaction = configurationDomainService.isMakerCheckerEnabledForTask(wrapper.taskPermissionName());
String idempotencyKey = idempotencyKeyResolver.resolve(wrapper);
exceptionWhenTheRequestAlreadyProcessed(wrapper, idempotencyKey);
Long commandId = (Long) fineractRequestContextHolder.getAttribute(COMMAND_SOURCE_ID, null);
boolean isRetry = commandId != null;
// Store idempotency key to the request attribute
CommandSource savedCommandSource;
if (BatchRequestContextHolder.getEnclosingTransaction().isPresent()) {
savedCommandSource = commandSourceService.saveInitialNoTransaction(wrapper, command, context.authenticatedUser(wrapper),
idempotencyKey);
CommandSource commandSource = null;
String idempotencyKey;
if (isRetry) {
commandSource = commandSourceService.getCommandSource(commandId);
idempotencyKey = commandSource.getIdempotencyKey();
} else {
savedCommandSource = commandSourceService.saveInitial(wrapper, command, context.authenticatedUser(wrapper), idempotencyKey);
idempotencyKey = idempotencyKeyResolver.resolve(wrapper);
}
exceptionWhenTheRequestAlreadyProcessed(wrapper, idempotencyKey, isRetry);
boolean sameTransaction = BatchRequestContextHolder.getEnclosingTransaction().isPresent();
if (commandSource == null) {
AppUser user = context.authenticatedUser(wrapper);
commandSource = sameTransaction ? commandSourceService.saveInitialSameTransaction(wrapper, command, user, idempotencyKey)
: commandSourceService.saveInitialNewTransaction(wrapper, command, user, idempotencyKey);
storeCommandIdInContext(commandSource); // Store command id as a request attribute
}
storeCommandToIdempotentFilter(savedCommandSource);
setIdempotencyKeyStoreFlag(true);
final CommandProcessingResult result;
try {
result = findCommandHandler(wrapper).processCommand(command);
} catch (Throwable t) { // NOSONAR
CommandSource source = commandSourceService.findCommandSource(wrapper, idempotencyKey);
commandSourceService.saveFailed(source);
publishHookErrorEvent(wrapper, command, t);
ErrorInfo errorInfo = commandSourceService.generateErrorInfo(t);
commandSource.setResultStatusCode(errorInfo.getStatusCode());
commandSource.setResult(errorInfo.getMessage());
commandSource.setStatus(ERROR);
commandSource = sameTransaction ? commandSourceService.saveResultSameTransaction(commandSource)
: commandSourceService.saveResultNewTransaction(commandSource);
publishHookErrorEvent(wrapper, command, errorInfo);
throw t;
}
CommandSource initialCommandSource = commandSourceService.findCommandSource(wrapper, idempotencyKey);
initialCommandSource.setResult(toApiJsonSerializer.serializeResult(result));
initialCommandSource.updateResourceId(result.getResourceId());
initialCommandSource.updateForAudit(result);
commandSource.updateForAudit(result);
commandSource.setResult(toApiJsonSerializer.serializeResult(result));
commandSource.setResultStatusCode(SC_OK);
commandSource.setStatus(PROCESSED);
boolean rollBack = (rollbackTransaction || result.isRollbackTransaction()) && !isApprovedByChecker;
if (result.hasChanges() && !rollBack) {
initialCommandSource.setCommandJson(toApiJsonSerializer.serializeResult(result.getChanges()));
boolean isRollback = !isApprovedByChecker && (result.isRollbackTransaction()
|| configurationDomainService.isMakerCheckerEnabledForTask(wrapper.taskPermissionName()));
// TODO: this should be removed, can not override audit information (and maker-checker does not work)
if (!isRollback && result.hasChanges()) {
commandSource.setCommandJson(toApiJsonSerializer.serializeResult(result.getChanges()));
}
initialCommandSource.setStatus(CommandProcessingResultType.PROCESSED.getValue());
if (BatchRequestContextHolder.getEnclosingTransaction().isPresent()) {
commandSourceService.saveResultNoTransaction(initialCommandSource);
} else {
commandSourceService.saveResult(initialCommandSource);
}
commandSource = commandSourceService.saveResultSameTransaction(commandSource);
if ((rollbackTransaction || result.isRollbackTransaction()) && !isApprovedByChecker) {
if (isRollback) {
/*
* JournalEntry will generate a new transactionId every time. Updating the transactionId with old
* transactionId, because as there are no entries are created with new transactionId, will throw an error
* when checker approves the transaction
*/
initialCommandSource.updateTransaction(command.getTransactionId());
/*
* Update CommandSource json data with JsonCommand json data, line 77 and 81 may update the json data
*/
initialCommandSource.setCommandJson(command.json());
throw new RollbackTransactionAsCommandIsNotApprovedByCheckerException(initialCommandSource);
commandSource.setTransactionId(command.getTransactionId());
// TODO: this should be removed together with lines 133-135
commandSource.setCommandJson(command.json()); // Set back CommandSource json data
throw new RollbackTransactionAsCommandIsNotApprovedByCheckerException(commandSource);
}
result.setRollbackTransaction(null);
result.setRollbackTransaction(null);
publishHookEvent(wrapper.entityName(), wrapper.actionName(), command, result);
return result;
}
private void storeCommandToIdempotentFilter(CommandSource savedCommandSource) {
private void storeCommandIdInContext(CommandSource savedCommandSource) {
if (savedCommandSource.getId() == null) {
throw new IllegalStateException("Command source not saved");
}
saveCommandToRequest(savedCommandSource);
}
private void saveCommandToRequest(CommandSource savedCommandSource) {
// Idempotency filters and retry need this
fineractRequestContextHolder.setAttribute(COMMAND_SOURCE_ID, savedCommandSource.getId());
}
private void publishHookErrorEvent(CommandWrapper wrapper, JsonCommand command, Throwable t) {
ErrorInfo ex = commandSourceService.generateErrorException(t);
publishHookEvent(wrapper.entityName(), wrapper.actionName(), command, gson.toJson(ex));
private void publishHookErrorEvent(CommandWrapper wrapper, JsonCommand command, ErrorInfo errorInfo) {
publishHookEvent(wrapper.entityName(), wrapper.actionName(), command, gson.toJson(errorInfo));
}
private void exceptionWhenTheRequestAlreadyProcessed(CommandWrapper wrapper, String idempotencyKey) {
private void exceptionWhenTheRequestAlreadyProcessed(CommandWrapper wrapper, String idempotencyKey, boolean retry) {
CommandSource existingCommand = commandSourceService.findCommandSource(wrapper, idempotencyKey);
if (existingCommand != null) {
idempotentExceptionByStatus(UNDER_PROCESSING, existingCommand,
command -> new IdempotentCommandProcessUnderProcessingException(wrapper));
idempotentExceptionByStatus(ERROR, existingCommand, command -> new IdempotentCommandProcessFailedException(wrapper, command));
if (!retry) {
idempotentExceptionByStatus(ERROR, existingCommand,
command -> new IdempotentCommandProcessFailedException(wrapper, command));
}
idempotentExceptionByStatus(PROCESSED, existingCommand,
command -> new IdempotentCommandProcessSucceedException(wrapper, command.getResult(), command.getResultStatusCode()));
}
@@ -192,24 +200,23 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
@Transactional
@Override
public CommandProcessingResult logCommand(CommandSource commandSourceResult) {
commandSourceResult.markAsAwaitingApproval();
if (commandSourceResult.getIdempotencyKey() == null) {
commandSourceResult.setIdempotencyKey(idempotencyKeyGenerator.create());
public CommandProcessingResult logCommand(CommandSource commandSource) {
commandSource.markAsAwaitingApproval();
if (commandSource.getIdempotencyKey() == null) {
commandSource.setIdempotencyKey(idempotencyKeyGenerator.create());
}
commandSourceResult = commandSourceService.saveResultNoTransaction(commandSourceResult);
commandSource = commandSourceService.saveResultSameTransaction(commandSource);
return new CommandProcessingResultBuilder().withCommandId(commandSourceResult.getId())
.withEntityId(commandSourceResult.getResourceId()).build();
return new CommandProcessingResultBuilder().withCommandId(commandSource.getId()).withEntityId(commandSource.getResourceId())
.build();
}
@SuppressWarnings("unused")
private CommandProcessingResult fallbackExecuteCommand(Exception e) throws Exception {
private CommandProcessingResult fallbackExecuteCommand(Exception e) {
if (e instanceof RollbackTransactionAsCommandIsNotApprovedByCheckerException ex) {
return logCommand(ex.getCommandSourceResult());
}
throw e;
throw errorHandler.getMappable(e);
}
private NewCommandSourceHandler findCommandHandler(final CommandWrapper wrapper) {
@@ -278,7 +285,6 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
}
private void publishHookEvent(final String entityName, final String actionName, JsonCommand command, final Object result) {
try {
final AppUser appUser = context.authenticatedUser(CommandWrapper.wrap(actionName, entityName, null, null));
@@ -18,16 +18,26 @@
*/
package org.apache.fineract.infrastructure.core.data;
import static lombok.AccessLevel.PROTECTED;
import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
import static org.apache.http.HttpStatus.SC_CONFLICT;
import static org.apache.http.HttpStatus.SC_FORBIDDEN;
import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import static org.apache.http.HttpStatus.SC_METHOD_NOT_ALLOWED;
import static org.apache.http.HttpStatus.SC_NOT_FOUND;
import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE;
import static org.apache.http.HttpStatus.SC_UNAUTHORIZED;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.gson.Gson;
import jakarta.ws.rs.core.Response.Status;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
/**
*
*/
@Getter
@Setter(PROTECTED)
public class ApiGlobalErrorResponse {
/**
@@ -58,176 +68,125 @@ public class ApiGlobalErrorResponse {
private List<ApiParameterError> errors = new ArrayList<>();
protected ApiGlobalErrorResponse() {}
public static ApiGlobalErrorResponse create(int statusCode, String msgCode, String developerMessage, String defaultUserMessage,
List<ApiParameterError> errors) {
ApiGlobalErrorResponse response = new ApiGlobalErrorResponse();
response.setHttpStatusCode(String.valueOf(statusCode));
response.setUserMessageGlobalisationCode(msgCode);
response.setDeveloperMessage(developerMessage);
response.setDefaultUserMessage(defaultUserMessage);
response.setErrors(errors);
return response;
}
public static ApiGlobalErrorResponse create(int statusCode, String msgCode, String developerMessage, String defaultUserMessage) {
return create(statusCode, msgCode, developerMessage, defaultUserMessage, null);
}
public static ApiGlobalErrorResponse unAuthenticated() {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("401");
globalErrorResponse.setDeveloperMessage("Invalid authentication details were passed in api request.");
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.not.authenticated");
globalErrorResponse.setDefaultUserMessage("Unauthenticated. Please login.");
return globalErrorResponse;
return create(SC_UNAUTHORIZED, "error.msg.not.authenticated", "Invalid authentication details were passed in api request.",
"Unauthenticated. Please login.");
}
public static ApiGlobalErrorResponse invalidTenantIdentifier() {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("401");
globalErrorResponse.setDeveloperMessage("Invalid tenant details were passed in api request.");
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.invalid.tenant.identifier");
globalErrorResponse.setDefaultUserMessage("Invalid tenant identifier provided with request.");
return globalErrorResponse;
return create(SC_UNAUTHORIZED, "error.msg.invalid.tenant.identifier", "Invalid tenant details were passed in api request.",
"Invalid tenant identifier provided with request.");
}
public static ApiGlobalErrorResponse invalidInstanceTypeMethod(final String method) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode(Status.METHOD_NOT_ALLOWED.toString());
globalErrorResponse.setDeveloperMessage("Invalid instance type called in api request for the method " + method);
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.invalid.instance.type");
globalErrorResponse.setDefaultUserMessage("Invalid method " + method + " used with request to this instance type.");
return globalErrorResponse;
return create(SC_METHOD_NOT_ALLOWED, "error.msg.invalid.instance.type",
"Invalid instance type called in api request for the method " + method,
"Invalid method " + method + " used with request to this instance type.");
}
public static ApiGlobalErrorResponse loanIsLocked(final Long loanId) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode(Status.CONFLICT.toString());
globalErrorResponse.setDeveloperMessage("Loan is locked by the COB job. Loan ID: " + loanId);
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.loan.locked");
globalErrorResponse.setDefaultUserMessage("Loan is locked by the COB job. Loan ID: \" + loanId");
String msg = "Loan is locked by the COB job. Loan ID: " + loanId;
return create(SC_CONFLICT, "error.msg.loan.locked", msg, msg);
}
return globalErrorResponse;
public static ApiGlobalErrorResponse conflict(String type, String identifier) {
String details = "";
if (type == null) {
type = "unknown";
} else {
details = " on " + type;
}
if (identifier != null) {
details += " [" + identifier + ']';
}
String msg = "The server is currently unable to handle the request due to concurrent modification" + details + ", please try again";
return create(SC_CONFLICT, "error.msg.platform.service." + type + ".conflict", msg, msg);
}
public static ApiGlobalErrorResponse unAuthorized(final String defaultUserMessage) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("403");
globalErrorResponse.setDeveloperMessage(
"The user associated with credentials passed on this request does not have sufficient privileges to perform this action.");
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.not.authorized");
globalErrorResponse.setDefaultUserMessage("Insufficient privileges to perform this action.");
String msgCode = "error.msg.not.authorized";
final List<ApiParameterError> errors = List.of(ApiParameterError.generalError(msgCode, defaultUserMessage));
final List<ApiParameterError> errors = new ArrayList<>();
errors.add(ApiParameterError.generalError("error.msg.not.authorized", defaultUserMessage));
globalErrorResponse.setErrors(errors);
return globalErrorResponse;
return create(SC_FORBIDDEN, msgCode,
"The user associated with credentials passed on this request does not have sufficient privileges to perform this action.",
"Insufficient privileges to perform this action.", errors);
}
public static ApiGlobalErrorResponse domainRuleViolation(final String globalisationMessageCode, final String defaultUserMessage,
final Object... defaultUserMessageArgs) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("403");
globalErrorResponse.setDeveloperMessage("Request was understood but caused a domain rule violation.");
globalErrorResponse.setUserMessageGlobalisationCode("validation.msg.domain.rule.violation");
globalErrorResponse.setDefaultUserMessage("Errors contain reason for domain rule violation.");
final List<ApiParameterError> errors = new ArrayList<>();
errors.add(ApiParameterError.generalError(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs));
globalErrorResponse.setErrors(errors);
return globalErrorResponse;
}
public static ApiGlobalErrorResponse notFound(final String globalisationMessageCode, final String defaultUserMessage,
final Object... defaultUserMessageArgs) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("404");
globalErrorResponse.setDeveloperMessage("The requested resource is not available.");
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.resource.not.found");
globalErrorResponse.setDefaultUserMessage("The requested resource is not available.");
final List<ApiParameterError> errors = new ArrayList<>();
errors.add(ApiParameterError.resourceIdentifierNotFound(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs));
globalErrorResponse.setErrors(errors);
return globalErrorResponse;
return create(SC_FORBIDDEN, "validation.msg.domain.rule.violation", "Request was understood but caused a domain rule violation.",
"Errors contain reason for domain rule violation.", errors);
}
public static ApiGlobalErrorResponse dataIntegrityError(final String globalisationMessageCode, final String defaultUserMessage,
final String parameterName, final Object... defaultUserMessageArgs) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("403");
globalErrorResponse.setDeveloperMessage("The request caused a data integrity issue to be fired by the database.");
globalErrorResponse.setUserMessageGlobalisationCode(globalisationMessageCode);
globalErrorResponse.setDefaultUserMessage(defaultUserMessage);
final List<ApiParameterError> errors = new ArrayList<>();
errors.add(ApiParameterError.parameterError(globalisationMessageCode, defaultUserMessage, parameterName, defaultUserMessageArgs));
globalErrorResponse.setErrors(errors);
return globalErrorResponse;
return create(SC_FORBIDDEN, globalisationMessageCode, "The request caused a data integrity issue to be fired by the database.",
defaultUserMessage, errors);
}
public static ApiGlobalErrorResponse notFound(final String globalisationMessageCode, final String defaultUserMessage,
final Object... defaultUserMessageArgs) {
String msg = "The requested resource is not available.";
final List<ApiParameterError> errors = new ArrayList<>();
errors.add(ApiParameterError.resourceIdentifierNotFound(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs));
return create(SC_NOT_FOUND, "error.msg.resource.not.found", msg, msg, errors);
}
public static ApiGlobalErrorResponse badClientRequest(final String globalisationMessageCode, final String defaultUserMessage,
final List<ApiParameterError> errors) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("400");
globalErrorResponse
.setDeveloperMessage("The request was invalid. This typically will happen due to validation errors which are provided.");
globalErrorResponse.setUserMessageGlobalisationCode(globalisationMessageCode);
globalErrorResponse.setDefaultUserMessage(defaultUserMessage);
globalErrorResponse.setErrors(errors);
return globalErrorResponse;
}
public static ApiGlobalErrorResponse jobIsDisabled(final String globalisationMessageCode, final String defaultUserMessage) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode(Status.FORBIDDEN.toString());
globalErrorResponse.setDeveloperMessage(defaultUserMessage);
globalErrorResponse.setUserMessageGlobalisationCode(globalisationMessageCode);
globalErrorResponse.setDefaultUserMessage(defaultUserMessage);
return globalErrorResponse;
return create(SC_BAD_REQUEST, globalisationMessageCode,
"The request was invalid. This typically will happen due to validation errors which are provided.", defaultUserMessage,
errors);
}
public static ApiGlobalErrorResponse badClientRequest(final String globalisationMessageCode, final String defaultUserMessage) {
return badClientRequest(globalisationMessageCode, defaultUserMessage, Collections.emptyList());
}
public static ApiGlobalErrorResponse jobIsDisabled(final String globalisationMessageCode, final String defaultUserMessage) {
return create(SC_FORBIDDEN, globalisationMessageCode, defaultUserMessage, defaultUserMessage);
}
public static ApiGlobalErrorResponse serverSideError(final String globalisationMessageCode, final String defaultUserMessage,
final Object... defaultUserMessageArgs) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("500");
globalErrorResponse.setDeveloperMessage("An unexpected error occured on the platform server.");
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.platform.server.side.error");
globalErrorResponse.setDefaultUserMessage("An unexpected error occured on the platform server.");
String msg = "An unexpected error occured on the platform server.";
final List<ApiParameterError> errors = new ArrayList<>();
errors.add(ApiParameterError.generalError(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs));
globalErrorResponse.setErrors(errors);
return globalErrorResponse;
return create(SC_INTERNAL_SERVER_ERROR, "error.msg.platform.server.side.error", msg, msg, errors);
}
public static ApiGlobalErrorResponse serviceUnavailable(final String globalisationMessageCode, final String defaultUserMessage,
final Object... defaultUserMessageArgs) {
final ApiGlobalErrorResponse globalErrorResponse = new ApiGlobalErrorResponse();
globalErrorResponse.setHttpStatusCode("503");
globalErrorResponse.setDeveloperMessage("The server is currently unable to handle the request , please try after some time.");
globalErrorResponse.setUserMessageGlobalisationCode("error.msg.platform.service.unavailable");
globalErrorResponse.setDefaultUserMessage("The server is currently unable to handle the request , please try after some time.");
String msg = "The server is currently unable to handle the request , please try after some time.";
final List<ApiParameterError> errors = new ArrayList<>();
errors.add(ApiParameterError.generalError(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs));
globalErrorResponse.setErrors(errors);
return globalErrorResponse;
}
protected ApiGlobalErrorResponse() {}
public ApiGlobalErrorResponse(final List<ApiParameterError> errors) {
this.errors = errors;
return create(SC_SERVICE_UNAVAILABLE, "error.msg.platform.service.unavailable", msg, msg, errors);
}
@JsonProperty("errors")
@@ -235,42 +194,6 @@ public class ApiGlobalErrorResponse {
return this.errors;
}
public void setErrors(final List<ApiParameterError> errors) {
this.errors = errors;
}
public String getDeveloperMessage() {
return this.developerMessage;
}
public void setDeveloperMessage(final String developerMessage) {
this.developerMessage = developerMessage;
}
public String getHttpStatusCode() {
return this.httpStatusCode;
}
public void setHttpStatusCode(final String httpStatusCode) {
this.httpStatusCode = httpStatusCode;
}
public String getDefaultUserMessage() {
return this.defaultUserMessage;
}
public void setDefaultUserMessage(final String defaultUserMessage) {
this.defaultUserMessage = defaultUserMessage;
}
public String getUserMessageGlobalisationCode() {
return this.userMessageGlobalisationCode;
}
public void setUserMessageGlobalisationCode(final String userMessageGlobalisationCode) {
this.userMessageGlobalisationCode = userMessageGlobalisationCode;
}
public String toJson() {
final Gson gson = new Gson();
return gson.toJson(this);
@@ -19,7 +19,6 @@
package org.apache.fineract.infrastructure.core.data;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import lombok.Getter;
import lombok.ToString;
@@ -51,12 +50,43 @@ public class CommandProcessingResult implements Serializable {
private final ExternalId resourceExternalId;
private final ExternalId subResourceExternalId;
public static CommandProcessingResult fromCommandProcessingResult(CommandProcessingResult commandResult) {
return new CommandProcessingResult(commandResult.commandId, commandResult.officeId, commandResult.groupId, commandResult.clientId,
commandResult.loanId, commandResult.savingsId, commandResult.resourceIdentifier, commandResult.resourceId,
commandResult.transactionId, commandResult.changes, commandResult.productId, commandResult.gsimId, commandResult.glimId,
commandResult.creditBureauReportData, commandResult.rollbackTransaction, commandResult.subResourceId,
commandResult.resourceExternalId, commandResult.subResourceExternalId);
private CommandProcessingResult(final Long commandId, final Long officeId, final Long groupId, final Long clientId, final Long loanId,
final Long savingsId, final String resourceIdentifier, final Long resourceId, final String transactionId,
final Map<String, Object> changes, final Long productId, final Long gsimId, final Long glimId,
final Map<String, Object> creditBureauReportData, Boolean rollbackTransaction, final Long subResourceId,
final ExternalId resourceExternalId, final ExternalId subResourceExternalId) {
this.commandId = commandId;
this.officeId = officeId;
this.groupId = groupId;
this.clientId = clientId;
this.loanId = loanId;
this.savingsId = savingsId;
this.resourceIdentifier = resourceIdentifier;
this.resourceId = resourceId;
this.changes = changes;
this.transactionId = transactionId;
this.productId = productId;
this.gsimId = gsimId;
this.glimId = glimId;
this.creditBureauReportData = creditBureauReportData;
this.rollbackTransaction = rollbackTransaction;
this.subResourceId = subResourceId;
this.resourceExternalId = resourceExternalId;
this.subResourceExternalId = subResourceExternalId;
}
protected CommandProcessingResult(final Long resourceId, final Long officeId, final Long commandId, final Map<String, Object> changes,
Long clientId) {
this(commandId, officeId, null, clientId, null, null, resourceId == null ? null : resourceId.toString(), resourceId, null, changes,
null, null, null, null, null, null, ExternalId.empty(), ExternalId.empty());
}
protected CommandProcessingResult(final Long resourceId, final Long officeId, final Long commandId, final Map<String, Object> changes) {
this(resourceId, officeId, commandId, changes, null);
}
protected CommandProcessingResult(final Long resourceId) {
this(resourceId, null, null, null);
}
public static CommandProcessingResult fromCommandProcessingResult(CommandProcessingResult commandResult, final Long resourceId) {
@@ -67,6 +97,10 @@ public class CommandProcessingResult implements Serializable {
commandResult.resourceExternalId, commandResult.subResourceExternalId);
}
public static CommandProcessingResult fromCommandProcessingResult(CommandProcessingResult commandResult) {
return fromCommandProcessingResult(commandResult, commandResult.getResourceId());
}
public static CommandProcessingResult fromDetails(final Long commandId, final Long officeId, final Long groupId, final Long clientId,
final Long loanId, final Long savingsId, final String resourceIdentifier, final Long entityId, final Long gsimId,
final Long glimId, final Map<String, Object> creditBureauReportData, final String transactionId,
@@ -81,21 +115,16 @@ public class CommandProcessingResult implements Serializable {
return new CommandProcessingResult(null, null, commandId, null);
}
public static CommandProcessingResult resourceResult(final Long resourceId, final Long commandId) {
return new CommandProcessingResult(resourceId, null, commandId, null);
}
public static CommandProcessingResult resourceResult(final Long resourceId, final Long commandId, final Map<String, Object> changes) {
return new CommandProcessingResult(resourceId, null, commandId, changes);
}
public static CommandProcessingResult subResourceResult(final Long resourceId, final Long subResourceId, final Long commandId) {
return new CommandProcessingResult(resourceId, subResourceId, commandId, null);
public static CommandProcessingResult resourceResult(final Long resourceId, final Long commandId) {
return new CommandProcessingResult(resourceId, null, commandId, null);
}
public static CommandProcessingResult subResourceResult(final Long resourceId, final Long subResourceId, final Long commandId,
final Map<String, Object> changes) {
return new CommandProcessingResult(resourceId, subResourceId, commandId, changes);
public static CommandProcessingResult resourceResult(final Long resourceId) {
return new CommandProcessingResult(resourceId);
}
public static CommandProcessingResult withChanges(final Long resourceId, final Map<String, Object> changes) {
@@ -106,88 +135,6 @@ public class CommandProcessingResult implements Serializable {
return new CommandProcessingResult(null, null, null, null);
}
/*
* Deprecated
*/
public CommandProcessingResult(final Long entityId) {
if (entityId != null) {
this.resourceIdentifier = entityId.toString();
} else {
this.resourceIdentifier = null;
}
this.resourceId = entityId;
this.officeId = null;
this.groupId = null;
this.clientId = null;
this.loanId = null;
this.savingsId = null;
this.transactionId = null;
this.changes = new HashMap<>();
this.productId = null;
this.gsimId = null;
this.glimId = null;
this.creditBureauReportData = null;
this.subResourceId = null;
this.resourceExternalId = ExternalId.empty();
this.subResourceExternalId = ExternalId.empty();
}
private CommandProcessingResult(final Long commandId, final Long officeId, final Long groupId, final Long clientId, final Long loanId,
final Long savingsId, final String resourceIdentifier, final Long resourceId, final String transactionId,
final Map<String, Object> changesOnly, final Long productId, final Long gsimId, final Long glimId,
final Map<String, Object> creditBureauReportData, Boolean rollbackTransaction, final Long subResourceId,
final ExternalId resourceExternalId, final ExternalId subResourceExternalId) {
this.commandId = commandId;
this.officeId = officeId;
this.groupId = groupId;
this.clientId = clientId;
this.loanId = loanId;
this.savingsId = savingsId;
this.resourceIdentifier = resourceIdentifier;
this.resourceId = resourceId;
this.changes = changesOnly;
this.transactionId = transactionId;
this.productId = productId;
this.gsimId = gsimId;
this.glimId = glimId;
this.creditBureauReportData = creditBureauReportData;
this.rollbackTransaction = rollbackTransaction;
this.subResourceId = subResourceId;
this.resourceExternalId = resourceExternalId;
this.subResourceExternalId = subResourceExternalId;
}
protected CommandProcessingResult(final Long resourceId, final Long officeId, final Long commandId,
final Map<String, Object> changesOnly) {
if (resourceId != null) {
this.resourceIdentifier = resourceId.toString();
} else {
this.resourceIdentifier = null;
}
this.resourceId = resourceId;
this.officeId = officeId;
this.groupId = null;
this.clientId = null;
this.loanId = null;
this.savingsId = null;
this.transactionId = null;
this.commandId = commandId;
this.changes = changesOnly;
this.productId = null;
this.gsimId = null;
this.glimId = null;
this.creditBureauReportData = null;
this.subResourceId = null;
this.resourceExternalId = ExternalId.empty();
this.subResourceExternalId = ExternalId.empty();
}
protected CommandProcessingResult(final Long resourceId, final Long officeId, final Long commandId,
final Map<String, Object> changesOnly, long clientId) {
this(commandId, officeId, null, clientId, null, null, null, resourceId, null, changesOnly, null, null, null, null, null, null,
ExternalId.empty(), ExternalId.empty());
}
public void setOfficeId(final Long officeId) {
this.officeId = officeId;
}
@@ -0,0 +1,60 @@
/**
* 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.exceptionmapper;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
import org.springframework.context.annotation.Scope;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Component;
/**
* An {@link ExceptionMapper} to map {@link ObjectOptimisticLockingFailureException} thrown by platform into a HTTP API
* friendly format.
*/
@Provider
@Component
@Scope("singleton")
@Slf4j
public class ConcurrencyFailureExceptionMapper implements FineractExceptionMapper, ExceptionMapper<ConcurrencyFailureException> {
@Override
public Response toResponse(final ConcurrencyFailureException exception) {
log.warn("Exception: {}, Message: {}", exception.getClass().getName(), exception.getMessage());
String type = "unknown";
String identifier = "unknown";
if (exception instanceof ObjectOptimisticLockingFailureException olex) {
type = olex.getPersistentClassName();
identifier = olex.getIdentifier() == null ? null : String.valueOf(olex.getIdentifier());
}
final ApiGlobalErrorResponse dataIntegrityError = ApiGlobalErrorResponse.conflict(type, identifier);
return Response.status(Status.CONFLICT).entity(dataIntegrityError).type(MediaType.APPLICATION_JSON).build();
}
@Override
public int errorCode() {
return 4009;
}
}
@@ -0,0 +1,68 @@
/**
* 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.exceptionmapper;
import static org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessFailedException;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessSucceedException;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessUnderProcessingException;
import org.eclipse.persistence.exceptions.OptimisticLockException;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
* An {@link ExceptionMapper} to map {@link OptimisticLockException} thrown by platform into a HTTP API friendly format.
*/
@Provider
@Component
@Scope("singleton")
@Slf4j
public class IdempotentCommandExceptionMapper implements FineractExceptionMapper, ExceptionMapper<AbstractIdempotentCommandException> {
@Override
public Response toResponse(final AbstractIdempotentCommandException exception) {
log.warn("Processing {} request: {}", exception.getClass().getName(), exception.getMessage());
Status status = null;
if (exception instanceof IdempotentCommandProcessSucceedException pse) {
status = Status.fromStatusCode(pse.getStatusCode());
} else if (exception instanceof IdempotentCommandProcessUnderProcessingException) {
status = Status.CONFLICT;
} else if (exception instanceof IdempotentCommandProcessFailedException pfe) {
status = Status.fromStatusCode(pfe.getStatusCode());
}
if (status == null) {
status = Status.INTERNAL_SERVER_ERROR;
}
return Response.status(status).entity(exception.getResponse()).header(IDEMPOTENT_CACHE_HEADER, "true")
.type(MediaType.APPLICATION_JSON).build();
}
@Override
public int errorCode() {
return 4209;
}
}
@@ -1,45 +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.core.exceptionmapper;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessSucceedException;
import org.springframework.stereotype.Component;
@Provider
@Component
@Slf4j
public class IdempotentCommandProcessSucceedExceptionMapper implements ExceptionMapper<IdempotentCommandProcessSucceedException> {
@Override
public Response toResponse(final IdempotentCommandProcessSucceedException exception) {
log.debug("Idempotent processing success request: {}", exception.getMessage());
Response.ResponseBuilder responseBuilder = Response.status(exception.getStatusCode());
if (exception.getResponse() != null) {
responseBuilder.entity(exception.getResponse());
}
return responseBuilder.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER, "true").type(MediaType.APPLICATION_JSON)
.build();
}
}
@@ -1,44 +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.core.exceptionmapper;
import static org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessUnderProcessingException;
import org.springframework.stereotype.Component;
@Provider
@Component
@Slf4j
public class IdempotentCommandProcessUnderProcessingExceptionMapper
implements ExceptionMapper<IdempotentCommandProcessUnderProcessingException> {
@Override
public Response toResponse(final IdempotentCommandProcessUnderProcessingException exception) {
log.debug("Idempotent under processing request: {}", exception.getMessage());
return Response.status(Status.CONFLICT).entity(exception.getResponse()).header(IDEMPOTENT_CACHE_HEADER, "true")
.type(MediaType.APPLICATION_JSON).build();
}
}
@@ -24,20 +24,33 @@ import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessFailedException;
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
import org.eclipse.persistence.exceptions.OptimisticLockException;
import org.springframework.context.annotation.Scope;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Component;
/**
* An {@link ExceptionMapper} to map {@link ObjectOptimisticLockingFailureException} thrown by platform into a HTTP API
* friendly format.
*/
@Provider
@Component
@Scope("singleton")
@Slf4j
public class IdempotentCommandProcessFailedExceptionMapper implements ExceptionMapper<IdempotentCommandProcessFailedException> {
public class OptimisticLockExceptionMapper implements FineractExceptionMapper, ExceptionMapper<OptimisticLockException> {
@Override
public Response toResponse(final IdempotentCommandProcessFailedException exception) {
log.debug("Idempotent processing failed request: {}", exception.getMessage());
Status statusCode = Status.fromStatusCode(exception.getStatusCode());
return Response.status(statusCode).entity(exception.getResponse())
.header(AbstractIdempotentCommandException.IDEMPOTENT_CACHE_HEADER, "true").type(MediaType.APPLICATION_JSON).build();
public Response toResponse(final OptimisticLockException exception) {
log.warn("Exception: {}, Message: {}", exception.getClass().getName(), exception.getMessage());
String type = exception.getQuery() == null ? "unknown" : "query";
String identifier = "unknown";
final ApiGlobalErrorResponse dataIntegrityError = ApiGlobalErrorResponse.conflict(type, identifier);
return Response.status(Status.CONFLICT).entity(dataIntegrityError).type(MediaType.APPLICATION_JSON).build();
}
@Override
public int errorCode() {
return 4009;
}
}
@@ -43,9 +43,9 @@ public class IdempotencyStoreBatchFilter implements BatchFilter {
.setAttribute(SynchronousCommandProcessingService.IDEMPOTENCY_KEY_ATTRIBUTE, idempotentKey));
BatchResponse result = chain.serviceCall(batchRequest, uriInfo);
Optional<Long> commandId = helper.getCommandId(null);
boolean isSuccessWithoutStored = helper.isStoreIdempotencyKey(null) && commandId.isPresent();
boolean isSuccessWithoutStored = commandId.isPresent() && helper.isStoreIdempotencyKey(null);
if (isSuccessWithoutStored) {
helper.storeCommandResult(true, result.getStatusCode(), result.getBody(), commandId);
helper.storeCommandResult(result.getStatusCode(), result.getBody(), commandId.get());
}
return result;
}
@@ -56,12 +56,12 @@ public class IdempotencyStoreFilter extends OncePerRequestFilter {
filterChain.doFilter(request, wrapper.getValue() != null ? wrapper.getValue() : response);
Optional<Long> commandId = helper.getCommandId(request);
boolean isSuccessWithoutStored = helper.isStoreIdempotencyKey(request) && commandId.isPresent()
&& helper.isAllowedContentTypeResponse(response) && wrapper.getValue() != null;
boolean isSuccessWithoutStored = commandId.isPresent() && wrapper.getValue() != null && helper.isStoreIdempotencyKey(request)
&& helper.isAllowedContentTypeResponse(response);
if (isSuccessWithoutStored) {
helper.storeCommandResult(false, response.getStatus(), Optional.ofNullable(wrapper.getValue())
helper.storeCommandResult(response.getStatus(), Optional.ofNullable(wrapper.getValue())
.map(ContentCachingResponseWrapper::getContentAsByteArray).map(s -> new String(s, StandardCharsets.UTF_8)).orElse(null),
commandId);
commandId.get());
}
if (wrapper.getValue() != null) {
wrapper.getValue().copyBodyToResponse();
@@ -25,6 +25,7 @@ import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandSourceRepository;
import org.apache.fineract.commands.service.CommandSourceService;
import org.apache.fineract.commands.service.SynchronousCommandProcessingService;
import org.apache.fineract.infrastructure.core.domain.BatchRequestContextHolder;
import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
import org.springframework.stereotype.Component;
@@ -36,15 +37,13 @@ public class IdempotencyStoreHelper {
private final CommandSourceService commandSourceService;
private final FineractRequestContextHolder fineractRequestContextHolder;
public void storeCommandResult(boolean batch, int response, String body, Optional<Long> commandId) {
commandSourceRepository.findById(commandId.get()).ifPresent(commandSource -> {
public void storeCommandResult(Integer response, String body, Long commandId) {
commandSourceRepository.findById(commandId).ifPresent(commandSource -> {
boolean sameTransaction = BatchRequestContextHolder.getEnclosingTransaction().isPresent();
commandSource.setResultStatusCode(response);
commandSource.setResult(body);
if (batch) {
commandSourceService.saveResultNoTransaction(commandSource);
} else {
commandSourceService.saveResult(commandSource);
}
commandSource = sameTransaction ? commandSourceService.saveResultSameTransaction(commandSource)
: commandSourceService.saveResultNewTransaction(commandSource);
});
}
@@ -32,7 +32,7 @@ import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.ActionContext;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.exceptionmapper.IdempotentCommandProcessFailedExceptionMapper;
import org.apache.fineract.infrastructure.core.exceptionmapper.IdempotentCommandExceptionMapper;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -50,7 +50,7 @@ public class IdempotencyCommandProcessFailedExceptionTest {
@Test
public void testInconsistentStatus() {
IdempotentCommandProcessFailedExceptionMapper mapper = new IdempotentCommandProcessFailedExceptionMapper();
IdempotentCommandExceptionMapper mapper = new IdempotentCommandExceptionMapper();
CommandWrapper command = new CommandWrapper(null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null);
CommandSource source = CommandSource.fullEntryFrom(command, JsonCommand.from("{}"), null, "dummy-key", null);
@@ -32,9 +32,9 @@ class LoanIdsHardLockedExceptionMapperTest {
Response response = exceptionMapper.toResponse(new LoanIdsHardLockedException(123L));
Assertions.assertEquals(4090, exceptionMapper.errorCode());
Assertions.assertEquals("{\"developerMessage\":\"Loan is locked by the COB job. Loan ID: 123\","
+ "\"httpStatusCode\":\"Conflict\"," + "\"defaultUserMessage\":\"Loan is locked by the COB job. Loan ID: \\\" + loanId\","
+ "\"userMessageGlobalisationCode\":\"error.msg.loan.locked\"," + "\"errors\":[]}", response.getEntity());
Assertions.assertEquals("{\"developerMessage\":\"Loan is locked by the COB job. Loan ID: 123\"," + "\"httpStatusCode\":\"409\","
+ "\"defaultUserMessage\":\"Loan is locked by the COB job. Loan ID: 123\","
+ "\"userMessageGlobalisationCode\":\"error.msg.loan.locked\"}", response.getEntity());
Assertions.assertEquals(409, response.getStatus());
Assertions.assertEquals(MediaType.APPLICATION_JSON, response.getMediaType().toString());
}
@@ -48,23 +48,11 @@ public class ApplySavingsCommandStrategy implements CommandStrategy {
@Override
public BatchResponse execute(BatchRequest request, @SuppressWarnings("unused") UriInfo uriInfo) {
// Calls 'submitApplication' function from 'SavingsAccountsApiResource' to Apply Savings to an existing client
String responseBody = savingsAccountsApiResource.submitApplication(request.getBody());
final BatchResponse response = new BatchResponse();
final String responseBody;
response.setRequestId(request.getRequestId());
response.setHeaders(request.getHeaders());
// Calls 'submitApplication' function from
// 'SavingsAccountsApiResource' to Apply Savings to an existing
// client
responseBody = savingsAccountsApiResource.submitApplication(request.getBody());
response.setStatusCode(HttpStatus.SC_OK);
// Sets the body of the response after savings is successfully
// applied
response.setBody(responseBody);
return response;
// Sets the body of the response after savings is successfully applied
return new BatchResponse().setRequestId(request.getRequestId()).setStatusCode(HttpStatus.SC_OK).setBody(responseBody)
.setHeaders(request.getHeaders());
}
}
@@ -0,0 +1,74 @@
/**
* 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.command.internal;
import static org.apache.fineract.batch.command.CommandStrategyUtils.relativeUrlWithoutVersion;
import com.google.common.base.Splitter;
import jakarta.ws.rs.core.UriInfo;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.batch.command.CommandStrategy;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.infrastructure.dataqueries.api.DatatablesApiResource;
import org.apache.http.HttpStatus;
import org.springframework.stereotype.Component;
/**
* Implements {@link CommandStrategy} and deletes a new datatable entry for a given entity. It passes the contents of
* the body from the BatchRequest to {@link DatatablesApiResource} and gets back the response. This class will also
* catch any errors raised by {@link DatatablesApiResource} and map those errors to appropriate status codes in
* BatchResponse.
*
* @see CommandStrategy
* @see BatchRequest
* @see BatchResponse
*/
@Component
@RequiredArgsConstructor
public class DeleteDatatableEntryOneToManyCommandStrategy implements CommandStrategy {
private final DatatablesApiResource datatablesApiResource;
@Override
public BatchResponse execute(BatchRequest request, @SuppressWarnings("unused") UriInfo uriInfo) {
final BatchResponse response = new BatchResponse();
final String responseBody;
response.setRequestId(request.getRequestId());
response.setHeaders(request.getHeaders());
final List<String> pathParameters = Splitter.on('/').splitToList(relativeUrlWithoutVersion(request));
// Pluck out the datatable name & entityId out of the relative path
final String datatableName = pathParameters.get(1);
final Long entityId = Long.parseLong(pathParameters.get(2));
final Long entryId = Long.parseLong(pathParameters.get(3));
// Calls 'deleteDatatableEntry' function from 'DatatablesApiResource' to delete a datatable entry on an existing
// entity
responseBody = datatablesApiResource.deleteDatatableEntry(datatableName, entityId, entryId);
response.setStatusCode(HttpStatus.SC_OK);
// Sets the body of the response after datatable entry is successfully deleted created
response.setBody(responseBody);
return response;
}
}
@@ -0,0 +1,73 @@
/**
* 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.command.internal;
import static org.apache.fineract.batch.command.CommandStrategyUtils.relativeUrlWithoutVersion;
import com.google.common.base.Splitter;
import jakarta.ws.rs.core.UriInfo;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.batch.command.CommandStrategy;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.infrastructure.dataqueries.api.DatatablesApiResource;
import org.apache.http.HttpStatus;
import org.springframework.stereotype.Component;
/**
* Implements {@link CommandStrategy} and deletes a new datatable entry for a given entity. It passes the contents of
* the body from the BatchRequest to {@link DatatablesApiResource} and gets back the response. This class will also
* catch any errors raised by {@link DatatablesApiResource} and map those errors to appropriate status codes in
* BatchResponse.
*
* @see CommandStrategy
* @see BatchRequest
* @see BatchResponse
*/
@Component
@RequiredArgsConstructor
public class DeleteDatatableEntryOneToOneCommandStrategy implements CommandStrategy {
private final DatatablesApiResource datatablesApiResource;
@Override
public BatchResponse execute(BatchRequest request, @SuppressWarnings("unused") UriInfo uriInfo) {
final BatchResponse response = new BatchResponse();
final String responseBody;
response.setRequestId(request.getRequestId());
response.setHeaders(request.getHeaders());
final List<String> pathParameters = Splitter.on('/').splitToList(relativeUrlWithoutVersion(request));
// Pluck out the datatable name & entityId out of the relative path
final String datatableName = pathParameters.get(1);
final Long entityId = Long.parseLong(pathParameters.get(2));
// Calls 'deleteDatatableEntry' function from 'DatatablesApiResource' to delete a datatable entry on an existing
// entity
responseBody = datatablesApiResource.deleteDatatableEntries(datatableName, entityId);
response.setStatusCode(HttpStatus.SC_OK);
// Sets the body of the response after datatable entry is successfully deleted created
response.setBody(responseBody);
return response;
}
}
@@ -43,14 +43,9 @@ public class GetSavingsAccountByIdCommandStrategy implements CommandStrategy {
@Override
public BatchResponse execute(BatchRequest batchRequest, UriInfo uriInfo) {
final MutableUriInfo parameterizedUriInfo = new MutableUriInfo(uriInfo);
final BatchResponse response = new BatchResponse();
response.setRequestId(batchRequest.getRequestId());
response.setHeaders(batchRequest.getHeaders());
final String relativeUrl = relativeUrlWithoutVersion(batchRequest);
final Long savingsAccountId;
final long savingsAccountId;
Map<String, String> queryParameters = new HashMap<>();
if (relativeUrl.indexOf('?') > 0) {
savingsAccountId = Long.parseLong(StringUtils.substringBetween(relativeUrl, "/", "?"));
@@ -74,10 +69,7 @@ public class GetSavingsAccountByIdCommandStrategy implements CommandStrategy {
final String responseBody = savingsAccountsApiResource.retrieveOne(savingsAccountId,
Boolean.parseBoolean(staffInSelectedOfficeOnly), chargeStatus, uriInfo);
response.setStatusCode(HttpStatus.SC_OK);
response.setBody(responseBody);
return response;
return new BatchResponse().setRequestId(batchRequest.getRequestId()).setStatusCode(HttpStatus.SC_OK).setBody(responseBody)
.setHeaders(batchRequest.getHeaders());
}
}
@@ -42,10 +42,6 @@ public class SavingsAccountAdjustTransactionCommandStrategy implements CommandSt
@Override
public BatchResponse execute(BatchRequest batchRequest, UriInfo uriInfo) {
final BatchResponse response = new BatchResponse();
response.setRequestId(batchRequest.getRequestId());
response.setHeaders(batchRequest.getHeaders());
String relativeUrl = relativeUrlWithoutVersion(batchRequest);
final List<String> pathParameters;
String command = null;
@@ -63,10 +59,7 @@ public class SavingsAccountAdjustTransactionCommandStrategy implements CommandSt
final String responseBody = savingsAccountTransactionsApiResource.adjustTransaction(savingsAccountId, transactionId, command,
batchRequest.getBody());
response.setStatusCode(HttpStatus.SC_OK);
response.setBody(responseBody);
return response;
return new BatchResponse().setRequestId(batchRequest.getRequestId()).setStatusCode(HttpStatus.SC_OK).setBody(responseBody)
.setHeaders(batchRequest.getHeaders());
}
}
@@ -41,10 +41,6 @@ public class SavingsAccountTransactionCommandStrategy implements CommandStrategy
@Override
public BatchResponse execute(BatchRequest batchRequest, UriInfo uriInfo) {
final BatchResponse response = new BatchResponse();
response.setRequestId(batchRequest.getRequestId());
response.setHeaders(batchRequest.getHeaders());
String relativeUrl = relativeUrlWithoutVersion(batchRequest);
final List<String> pathParameters = Splitter.on('/').splitToList(relativeUrl);
String command = null;
@@ -56,10 +52,7 @@ public class SavingsAccountTransactionCommandStrategy implements CommandStrategy
Long savingsAccountId = Long.parseLong(pathParameters.get(1));
final String responseBody = savingsAccountTransactionsApiResource.transaction(savingsAccountId, command, batchRequest.getBody());
response.setStatusCode(HttpStatus.SC_OK);
response.setBody(responseBody);
return response;
return new BatchResponse().setRequestId(batchRequest.getRequestId()).setStatusCode(HttpStatus.SC_OK).setBody(responseBody)
.setHeaders(batchRequest.getHeaders());
}
}
@@ -126,7 +126,7 @@ public class DocumentManagementApiResource {
final DocumentCommand documentCommand = new DocumentCommand(null, null, entityType, entityId, name, fileDetails.getFileName(),
fileSize, bodyPart.getMediaType().toString(), description, null);
final Long documentId = this.documentWritePlatformService.createDocument(documentCommand, inputStream);
return this.toApiJsonSerializer.serialize(CommandProcessingResult.resourceResult(documentId, null));
return this.toApiJsonSerializer.serialize(CommandProcessingResult.resourceResult(documentId));
}
@PUT
@@ -201,7 +201,7 @@ public class ImagesApiResource {
public String deleteClientImage(@PathParam("entity") final String entityName, @PathParam("entityId") final Long entityId) {
validateEntityTypeforImage(entityName);
this.imageWritePlatformService.deleteImage(entityName, entityId);
return this.toApiJsonSerializer.serialize(new CommandProcessingResult(entityId));
return this.toApiJsonSerializer.serialize(CommandProcessingResult.resourceResult(entityId));
}
/*** Entities for document Management **/
@@ -133,7 +133,7 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr
this.documentRepository.saveAndFlush(documentForUpdate);
return new CommandProcessingResult(documentForUpdate.getId());
return CommandProcessingResult.resourceResult(documentForUpdate.getId());
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
LOG.error("Error occured.", dve);
throw new PlatformDataIntegrityException("error.msg.document.unknown.data.integrity.issue",
@@ -158,7 +158,7 @@ public class DocumentWritePlatformServiceJpaRepositoryImpl implements DocumentWr
final ContentRepository contentRepository = this.contentRepositoryFactory.getRepository(document.storageType());
contentRepository.deleteFile(document.getLocation());
return new CommandProcessingResult(document.getId());
return CommandProcessingResult.resourceResult(document.getId());
}
private void validateParentEntityType(final DocumentCommand documentCommand) {
@@ -103,7 +103,7 @@ public class ImageWritePlatformServiceJpaRepositoryImpl implements ImageWritePla
this.imageRepository.delete(image);
}
return new CommandProcessingResult(clientId);
return CommandProcessingResult.resourceResult(clientId);
}
/**
@@ -151,7 +151,7 @@ public class ImageWritePlatformServiceJpaRepositoryImpl implements ImageWritePla
}
this.imageRepository.save(image);
return new CommandProcessingResult(clientId);
return CommandProcessingResult.resourceResult(clientId);
}
private Image createImage(Image image, final String imageLocation, final StorageType storageType) {
@@ -164,7 +164,7 @@ public class ClientIdentifierWritePlatformServiceJpaRepositoryImpl implements Cl
.build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
handleClientIdentifierDataIntegrityViolation(documentTypeLabel, documentTypeId, documentKey, dve.getMostSpecificCause(), dve);
return new CommandProcessingResult(Long.valueOf(-1));
return CommandProcessingResult.resourceResult(-1L);
} catch (final PersistenceException dve) {
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
handleClientIdentifierDataIntegrityViolation(documentTypeLabel, documentTypeId, documentKey, throwable, dve);
@@ -138,7 +138,7 @@ public class CollateralWritePlatformServiceJpaRepositoryImpl implements Collater
.build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
handleCollateralDataIntegrityViolation(dve);
return new CommandProcessingResult(Long.valueOf(-1));
return CommandProcessingResult.resourceResult(-1L);
}
}
@@ -40,6 +40,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.batch.exception.ErrorHandler;
import org.apache.fineract.cob.exceptions.LoanAccountLockCannotBeOverruledException;
import org.apache.fineract.cob.service.LoanAccountLockService;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
@@ -193,7 +194,6 @@ import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.transfer.api.TransferApiConstants;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -247,7 +247,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final ExternalIdFactory externalIdFactory;
private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService;
private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
private final JdbcTemplate jdbcTemplate;
private final ErrorHandler errorHandler;
@Transactional
@Override
@@ -2426,13 +2426,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@SuppressWarnings("unused")
private void fallbackRecalculateInterest(Throwable t) {
// NOTE: allow caller to catch the exceptions
if (t instanceof RuntimeException re) {
throw re;
}
// NOTE: wrap throwable only if really necessary
throw new RuntimeException(t);
throw errorHandler.getMappable(t, null, null, "loan.recalculateinterest");
}
@Override
@@ -255,7 +255,7 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro
} catch (final DataIntegrityViolationException | JpaSystemException dve) {
handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
return new CommandProcessingResult((long) -1);
return CommandProcessingResult.resourceResult(-1L);
} catch (final PersistenceException dve) {
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
handleDataIntegrityIssues(command, throwable, dve);
@@ -125,7 +125,7 @@ public class RateWriteServiceImpl implements RateWriteService {
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
handleRateDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
return new CommandProcessingResult((long) -1);
return CommandProcessingResult.resourceResult(-1L);
} catch (final PersistenceException dve) {
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
handleRateDataIntegrityIssues(command, throwable, dve);
@@ -50,7 +50,6 @@ import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformS
import org.apache.fineract.infrastructure.core.api.ApiParameterHelper;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
@@ -66,10 +65,8 @@ import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformS
import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionSearchService;
import org.apache.fineract.portfolio.search.data.AdvancedQueryRequest;
import org.apache.fineract.portfolio.search.data.TransactionSearchRequest;
import org.springframework.dao.CannotAcquireLockException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Sort;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Component;
@Path("/v1/savingsaccounts/{savingsId}/transactions")
@@ -185,42 +182,33 @@ public class SavingsAccountTransactionsApiResource {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SavingsAccountTransactionsApiResourceSwagger.PostSavingsAccountTransactionsResponse.class))) })
public String transaction(@PathParam("savingsId") final Long savingsId, @QueryParam("command") final String commandParam,
final String apiRequestBodyAsJson) {
try {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
CommandProcessingResult result = null;
if (is(commandParam, "deposit")) {
final CommandWrapper commandRequest = builder.savingsAccountDeposit(savingsId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "gsimDeposit")) {
final CommandWrapper commandRequest = builder.gsimSavingsAccountDeposit(savingsId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "withdrawal")) {
final CommandWrapper commandRequest = builder.savingsAccountWithdrawal(savingsId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "postInterestAsOn")) {
final CommandWrapper commandRequest = builder.savingsAccountInterestPosting(savingsId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, SavingsApiConstants.COMMAND_HOLD_AMOUNT)) {
final CommandWrapper commandRequest = builder.holdAmount(savingsId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
if (result == null) {
//
throw new UnrecognizedQueryParamException("command", commandParam,
new Object[] { "deposit", "withdrawal", SavingsApiConstants.COMMAND_HOLD_AMOUNT });
}
return this.toApiJsonSerializer.serialize(result);
} catch (ObjectOptimisticLockingFailureException lockingFailureException) {
throw new PlatformDataIntegrityException("error.msg.savings.concurrent.operations",
"Concurrent Transactions being made on this savings account: " + lockingFailureException.getMessage(),
lockingFailureException);
} catch (CannotAcquireLockException cannotAcquireLockException) {
throw new PlatformDataIntegrityException("error.msg.savings.concurrent.operations.unable.to.acquire.lock",
"Unable to acquir lock for this transaction: " + cannotAcquireLockException.getMessage(), cannotAcquireLockException);
CommandProcessingResult result = null;
if (is(commandParam, "deposit")) {
final CommandWrapper commandRequest = builder.savingsAccountDeposit(savingsId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "gsimDeposit")) {
final CommandWrapper commandRequest = builder.gsimSavingsAccountDeposit(savingsId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "withdrawal")) {
final CommandWrapper commandRequest = builder.savingsAccountWithdrawal(savingsId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "postInterestAsOn")) {
final CommandWrapper commandRequest = builder.savingsAccountInterestPosting(savingsId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, SavingsApiConstants.COMMAND_HOLD_AMOUNT)) {
final CommandWrapper commandRequest = builder.holdAmount(savingsId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
if (result == null) {
//
throw new UnrecognizedQueryParamException("command", commandParam,
new Object[] { "deposit", "withdrawal", SavingsApiConstants.COMMAND_HOLD_AMOUNT });
}
return this.toApiJsonSerializer.serialize(result);
}
@POST
@@ -450,11 +450,11 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
.build();
} catch (final DataAccessException dve) {
handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
return new CommandProcessingResult((long) -1);
return CommandProcessingResult.resourceResult(-1L);
} catch (final PersistenceException dve) {
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
handleDataIntegrityIssues(command, throwable, dve);
return new CommandProcessingResult((long) -1);
return CommandProcessingResult.resourceResult(-1L);
}
}
@@ -521,11 +521,11 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
.build();
} catch (final DataAccessException dve) {
handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
return new CommandProcessingResult(Long.valueOf(-1));
return CommandProcessingResult.resourceResult(-1L);
} catch (final PersistenceException dve) {
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
handleDataIntegrityIssues(command, throwable, dve);
return new CommandProcessingResult(Long.valueOf(-1));
return CommandProcessingResult.resourceResult(-1L);
}
}
@@ -51,6 +51,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.batch.exception.ErrorHandler;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
@@ -165,6 +166,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
private final BusinessEventNotifierService businessEventNotifierService;
private final GSIMRepositoy gsimRepository;
private final SavingsAccountInterestPostingService savingsAccountInterestPostingService;
private final ErrorHandler errorHandler;
@Transactional
@Override
@@ -233,7 +235,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Override
public void processPostActiveActions(final SavingsAccount account, final DateTimeFormatter fmt, final Set<Long> existingTransactionIds,
final Set<Long> existingReversedTransactionIds) {
AppUser user = getAppUserIfPresent();
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
@@ -264,7 +265,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Transactional
@Override
public CommandProcessingResult gsimDeposit(final Long gsimId, final JsonCommand command) {
Long parentSavingId = gsimId;
// GroupSavingsIndividualMonitoringparentSavings=gsimRepository.findById(parentSavingId).get();
List<SavingsAccount> childSavings = this.savingAccountRepositoryWrapper.findByGsimId(gsimId);
@@ -276,7 +276,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
CommandProcessingResult result = null;
for (JsonElement element : savingsArray) {
result = deposit(element.getAsJsonObject().get("childAccountId").getAsLong(),
JsonCommand.fromExistingCommand(command, element));
}
@@ -286,7 +285,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Transactional
@Override
public CommandProcessingResult deposit(final Long savingsId, final JsonCommand command) {
this.context.authenticatedUser();
this.savingsAccountTransactionDataValidator.validate(command);
@@ -347,7 +345,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
.withSavingsId(savingsId) //
.with(changes) //
.build();
}
private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) {
@@ -420,7 +417,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Transactional
@Override
public CommandProcessingResult applyAnnualFee(final Long savingsAccountChargeId, final Long accountId) {
AppUser user = getAppUserIfPresent();
final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository
@@ -445,7 +441,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Transactional
@Override
public CommandProcessingResult calculateInterest(final Long savingsId) {
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
@@ -480,7 +475,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Override
@Transactional
public CommandProcessingResult postInterest(final JsonCommand command) {
Long savingsId = command.getSavingsId();
final boolean postInterestAs = command.booleanPrimitiveValueOfParameterNamed("isPostInterestAsOn");
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
@@ -491,13 +485,11 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account);
if (postInterestAs == true) {
if (postInterestAs) {
if (transactionDate == null) {
throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.VALID_DATE);
}
if (transactionDate.isBefore(account.accountSubmittedOrActivationDate())) {
if (DateUtils.isBefore(transactionDate, account.accountSubmittedOrActivationDate())) {
throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.ACTIVATION_DATE);
}
@@ -509,16 +501,14 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
}
for (SavingsAccountTransaction savingTransaction : savingTransactions) {
if (transactionDate.isBefore(savingTransaction.getDateOf())) {
if (DateUtils.isBefore(transactionDate, savingTransaction.getDateOf())) {
throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.LAST_TRANSACTION_DATE);
}
}
LocalDate today = DateUtils.getBusinessLocalDate();
if (transactionDate.isAfter(today)) {
if (DateUtils.isDateInTheFuture(transactionDate)) {
throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.FUTURE_DATE);
}
}
postInterest(account, postInterestAs, transactionDate, backdatedTxnsAllowedTill);
@@ -536,7 +526,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Override
public void postInterest(final SavingsAccount account, final boolean postInterestAs, final LocalDate transactionDate,
final boolean backdatedTxnsAllowedTill) {
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
@@ -757,7 +746,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Override
public CommandProcessingResult adjustSavingsTransaction(final Long savingsId, final Long transactionId, final JsonCommand command) {
AppUser user = getAppUserIfPresent();
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
@@ -939,12 +927,12 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
List<SavingsAccountTransaction> savingTransactions = account.getTransactions();
for (SavingsAccountTransaction savingTransaction : savingTransactions) {
if (savingTransaction.isInterestPosting() && savingTransaction.isNotReversed()
&& closedDate.isEqual(savingTransaction.getTransactionLocalDate())) {
&& DateUtils.isEqual(closedDate, savingTransaction.getTransactionLocalDate())) {
postInterestOnClosingDate = true;
break;
}
}
if (postInterestOnClosingDate == false) {
if (!postInterestOnClosingDate) {
throw new PostInterestClosingDateException();
}
}
@@ -997,7 +985,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Override
public SavingsAccountTransaction initiateSavingsTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate) {
AppUser user = getAppUserIfPresent();
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
@@ -1032,7 +1019,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Override
public SavingsAccountTransaction withdrawSavingsTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate) {
AppUser user = getAppUserIfPresent();
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
@@ -1072,7 +1058,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Override
public SavingsAccountTransaction acceptSavingsTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate,
final Office acceptedInOffice, final Staff fieldOfficer) {
AppUser user = getAppUserIfPresent();
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
@@ -1108,7 +1093,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Transactional
@Override
public CommandProcessingResult addSavingsAccountCharge(final JsonCommand command) {
this.context.authenticatedUser();
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
@@ -1233,7 +1217,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Transactional
@Override
public CommandProcessingResult waiveCharge(final Long savingsAccountId, final Long savingsAccountChargeId) {
AppUser user = getAppUserIfPresent();
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
@@ -1325,7 +1308,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Override
public CommandProcessingResult payCharge(final Long savingsAccountId, final Long savingsAccountChargeId, final JsonCommand command) {
AppUser user = getAppUserIfPresent();
this.savingsAccountChargeDataValidator.validatePayCharge(command.json());
@@ -1393,7 +1375,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MM yyyy").withZone(DateUtils.getDateTimeZoneOfTenant());
while (transactionDate.isAfter(savingsAccountCharge.getDueLocalDate()) && savingsAccountCharge.isNotFullyPaid()) {
while (savingsAccountCharge.isNotFullyPaid() && DateUtils.isBeforeBusinessDate(savingsAccountCharge.getDueLocalDate())) {
payCharge(savingsAccountCharge, transactionDate, savingsAccountCharge.amoutOutstanding(), fmt, user, false);
}
}
@@ -1402,13 +1384,8 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
private SavingsAccountData fallbackPostInterest(SavingsAccountData savingsAccountData, boolean postInterestAs,
LocalDate transactionDate, boolean backdatedTxnsAllowedTill, Throwable t) {
// NOTE: allow caller to catch the exceptions
if (t instanceof RuntimeException re) {
throw re;
}
// NOTE: wrap throwable only if really necessary
throw new RuntimeException(t);
throw errorHandler.getMappable(t, null, null, "savings.postinterest");
}
@Transactional
@@ -1817,7 +1794,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Transactional
@Override
public CommandProcessingResult releaseAmount(final Long savingsId, final Long savingsTransactionId) {
final AppUser submittedBy = this.context.authenticatedUser();
SavingsAccountTransaction holdTransaction = this.savingsAccountTransactionRepository
.findOneByIdAndSavingsAccountId(savingsTransactionId, savingsId);
@@ -1935,16 +1911,14 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
}
private void validateTransactionsForTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate) {
for (SavingsAccountTransaction transaction : savingsAccount.getTransactions()) {
if ((transaction.getTransactionLocalDate().isEqual(transferDate) && transaction.getTransactionLocalDate().isAfter(transferDate))
|| transaction.getTransactionLocalDate().isAfter(transferDate)) {
if ((DateUtils.isEqual(transferDate, transaction.getTransactionLocalDate())
&& DateUtils.isEqual(transferDate, transaction.getSubmittedOnDate()))
|| DateUtils.isBefore(transferDate, transaction.getTransactionLocalDate())) {
throw new GeneralPlatformDomainRuleException(TransferApiConstants.transferClientSavingsException,
TransferApiConstants.transferClientSavingsException, transaction.getCreatedDate(), transferDate);
}
}
}
private void validateReasonForHold(String reasonForBlock) {
@@ -397,7 +397,7 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
.build();
} catch (final DataAccessException dve) {
handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
return new CommandProcessingResult(Long.valueOf(-1));
return CommandProcessingResult.resourceResult(-1L);
} catch (final PersistenceException dve) {
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
handleDataIntegrityIssues(command, throwable, dve);
@@ -53,7 +53,7 @@ public class SelfServiceRegistrationApiResource {
@Produces({ MediaType.APPLICATION_JSON })
public String createSelfServiceUser(final String apiRequestBodyAsJson) {
AppUser user = this.selfServiceRegistrationWritePlatformService.createUser(apiRequestBodyAsJson);
return this.toApiJsonSerializer.serialize(CommandProcessingResult.resourceResult(user.getId(), null));
return this.toApiJsonSerializer.serialize(CommandProcessingResult.resourceResult(user.getId()));
}
}
@@ -287,7 +287,7 @@ resilience4j.retry.instances.executeCommand.max-attempts=${FINERACT_COMMAND_PROC
resilience4j.retry.instances.executeCommand.wait-duration=${FINERACT_COMMAND_PROCESSING_RETRY_WAIT_DURATION:1s}
resilience4j.retry.instances.executeCommand.enable-exponential-backoff=${FINERACT_COMMAND_PROCESSING_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
resilience4j.retry.instances.executeCommand.exponential-backoff-multiplier=${FINERACT_COMMAND_PROCESSING_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.executeCommand.retryExceptions=${FINERACT_COMMAND_PROCESSING_RETRY_EXCEPTIONS:org.springframework.dao.CannotAcquireLockException,org.springframework.orm.ObjectOptimisticLockingFailureException}
resilience4j.retry.instances.executeCommand.retryExceptions=${FINERACT_COMMAND_PROCESSING_RETRY_EXCEPTIONS:org.springframework.dao.CannotAcquireLockException,org.springframework.orm.ObjectOptimisticLockingFailureException,org.eclipse.persistence.exceptions.OptimisticLockException}
resilience4j.retry.instances.processJobDetailForExecution.max-attempts=${FINERACT_PROCESS_JOB_DETAIL_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.processJobDetailForExecution.wait-duration=${FINERACT_PROCESS_JOB_DETAIL_RETRY_WAIT_DURATION:1s}
@@ -298,10 +298,10 @@ resilience4j.retry.instances.recalculateInterest.max-attempts=${FINERACT_PROCESS
resilience4j.retry.instances.recalculateInterest.wait-duration=${FINERACT_PROCESS_RECALCULATE_INTEREST_RETRY_WAIT_DURATION:1s}
resilience4j.retry.instances.recalculateInterest.enable-exponential-backoff=${FINERACT_PROCESS_RECALCULATE_INTEREST_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
resilience4j.retry.instances.recalculateInterest.exponential-backoff-multiplier=${FINERACT_PROCESS_RECALCULATE_INTEREST_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.recalculateInterest.retryExceptions=${FINERACT_PROCESS_RECALCULATE_INTEREST_RETRY_EXCEPTIONS:org.springframework.dao.CannotAcquireLockException,org.springframework.orm.ObjectOptimisticLockingFailureException}
resilience4j.retry.instances.recalculateInterest.retryExceptions=${FINERACT_PROCESS_RECALCULATE_INTEREST_RETRY_EXCEPTIONS:org.springframework.dao.CannotAcquireLockException,org.springframework.orm.ObjectOptimisticLockingFailureException,org.eclipse.persistence.exceptions.OptimisticLockException}
resilience4j.retry.instances.postInterest.max-attempts=${FINERACT_PROCESS_POST_INTEREST_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.postInterest.wait-duration=${FINERACT_PROCESS_POST_INTEREST_RETRY_WAIT_DURATION:1s}
resilience4j.retry.instances.postInterest.enable-exponential-backoff=${FINERACT_PROCESS_POST_INTEREST_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
resilience4j.retry.instances.postInterest.exponential-backoff-multiplier=${FINERACT_PROCESS_POST_INTEREST_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.postInterest.retryExceptions=${FINERACT_PROCESS_POST_INTEREST_RETRY_EXCEPTIONS:org.springframework.dao.CannotAcquireLockException,org.springframework.orm.ObjectOptimisticLockingFailureException}
resilience4j.retry.instances.postInterest.retryExceptions=${FINERACT_PROCESS_POST_INTEREST_RETRY_EXCEPTIONS:org.springframework.dao.CannotAcquireLockException,org.springframework.orm.ObjectOptimisticLockingFailureException,org.eclipse.persistence.exceptions.OptimisticLockException}
@@ -20,6 +20,7 @@ package org.apache.fineract.commands.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import io.cucumber.java8.En;
import io.github.resilience4j.retry.RetryRegistry;
@@ -33,11 +34,14 @@ import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.exception.RollbackTransactionAsCommandIsNotApprovedByCheckerException;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.CannotAcquireLockException;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.test.util.ReflectionTestUtils;
public class CommandServiceStepDefinitions implements En {
@@ -55,16 +59,24 @@ public class CommandServiceStepDefinitions implements En {
private RetryEvent retryEvent;
private AtomicInteger counter = new AtomicInteger();
public CommandServiceStepDefinitions() {
Given("/^A command source write service$/", () -> {
this.commandSourceWritePlatformService = new DummyCommandSourceWriteService(processAndLogCommandService);
this.command = new DummyCommand();
FineractRequestContextHolder contextHolder = Mockito.spy(FineractRequestContextHolder.class);
ReflectionTestUtils.setField(processAndLogCommandService, "fineractRequestContextHolder", contextHolder);
Mockito.when(contextHolder.getAttribute(any(), any())).thenThrow(new CannotAcquireLockException("BLOW IT UP!!!"))
.thenThrow(new ObjectOptimisticLockingFailureException("Dummy", new RuntimeException("BLOW IT UP!!!")))
.thenThrow(new RollbackTransactionAsCommandIsNotApprovedByCheckerException(new DummyCommandSource()));
this.retryRegistry.retry("executeCommand").getEventPublisher().onRetry(event -> {
log.warn("... retry event: {}", event);
counter.incrementAndGet();
CommandServiceStepDefinitions.this.retryEvent = event;
});
});
When("/^The user executes the command via a command write service with exceptions$/", () -> {
@@ -84,47 +96,21 @@ public class CommandServiceStepDefinitions implements En {
});
Then("/^The command processing service execute function should be called 3 times$/", () -> {
assertEquals(3, command.getCount());
assertEquals(2, counter.get());
});
}
public static class DummyCommand extends CommandWrapper {
private AtomicInteger counter = new AtomicInteger();
public DummyCommand() {
super(null, null, null, null, null, null, null, null, null, null, "{}", null, null, null, null, null, null,
UUID.randomUUID().toString());
}
@Override
public String taskPermissionName() {
// NOTE: simulating a failure scenario that triggers retries; using this function, because it is the first
// called in the command processing service
int step = counter.incrementAndGet();
log.warn("Round: {}", step);
if (step == 1) {
throw new CannotAcquireLockException("BLOW IT UP!!!");
} else if (step == 2) {
throw new ObjectOptimisticLockingFailureException("Dummy", new RuntimeException("BLOW IT UP!!!"));
} else if (step == 3) {
throw new RollbackTransactionAsCommandIsNotApprovedByCheckerException(new DummyCommandSource());
}
return "dummy";
}
@Override
public String actionName() {
return "dummy";
}
public int getCount() {
return counter.get();
}
}
public static class DummyCommandSourceWriteService implements PortfolioCommandSourceWritePlatformService {
@@ -74,7 +74,7 @@ public class CommandSourceServiceTest {
ThreadLocalContextUtil.setTenant(ft);
String idk = "idk";
underTest.saveInitial(wrapper, jsonCommand, appUser, idk);
underTest.saveInitialNewTransaction(wrapper, jsonCommand, appUser, idk);
ArgumentCaptor<CommandSource> commandSourceArgumentCaptor = ArgumentCaptor.forClass(CommandSource.class);
Mockito.verify(commandSourceRepository).saveAndFlush(commandSourceArgumentCaptor.capture());
@@ -97,7 +97,7 @@ public class CommandSourceServiceTest {
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "t1", "n1", ZoneId.systemDefault().toString(), null));
CommandSource actual = underTest.saveInitial(wrapper, jsonCommand, appUser, "idk");
CommandSource actual = underTest.saveInitialNewTransaction(wrapper, jsonCommand, appUser, "idk");
ArgumentCaptor<CommandSource> commandSourceArgumentCaptor = ArgumentCaptor.forClass(CommandSource.class);
Mockito.verify(commandSourceRepository).saveAndFlush(commandSourceArgumentCaptor.capture());
@@ -108,9 +108,10 @@ public class CommandSourceServiceTest {
@Test
public void testGenerateErrorException() {
Mockito.when(errorHandler.getMappable(any())).thenAnswer(i -> i.getArguments()[0]);
Mockito.when(errorHandler.handle(any(CodeNotFoundException.class)))
.thenReturn(new ErrorInfo(404, 1001, "Code with name `foo` does not exist"));
ErrorInfo result = underTest.generateErrorException(new CodeNotFoundException("foo"));
.thenReturn(new ErrorInfo(404, 1001, "Code with name `foo` does not exist", null));
ErrorInfo result = underTest.generateErrorInfo(new CodeNotFoundException("foo"));
Assertions.assertEquals(404, result.getStatusCode());
Assertions.assertEquals(1001, result.getErrorCode());
Assertions.assertTrue(result.getMessage().contains("Code with name `foo` does not exist"));
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.commands.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -85,7 +86,6 @@ public class SynchronousCommandProcessingServiceTest {
@Test
public void testExecuteCommandSuccess() {
CommandWrapper commandWrapper = Mockito.mock(CommandWrapper.class);
when(commandWrapper.isDatatableResource()).thenReturn(false);
when(commandWrapper.isNoteResource()).thenReturn(false);
@@ -103,19 +103,24 @@ public class SynchronousCommandProcessingServiceTest {
String idk = "idk";
when(idempotencyKeyResolver.resolve(commandWrapper)).thenReturn(idk);
CommandSource commandSource = Mockito.mock(CommandSource.class);
when(commandSourceService.findCommandSource(commandWrapper, idk)).thenReturn(null).thenReturn(commandSource);
long commandId = 1L;
when(commandSource.getId()).thenReturn(commandId);
when(commandSourceService.findCommandSource(commandWrapper, idk)).thenReturn(null);
when(commandSourceService.getCommandSource(commandId)).thenReturn(commandSource);
AppUser appUser = Mockito.mock(AppUser.class);
when(commandSourceService.saveInitial(commandWrapper, jsonCommand, appUser, idk)).thenReturn(commandSource);
when(commandSourceService.saveInitialNewTransaction(commandWrapper, jsonCommand, appUser, idk)).thenReturn(commandSource);
when(commandSourceService.saveResultSameTransaction(commandSource)).thenReturn(commandSource);
when(commandSource.getStatus()).thenReturn(CommandProcessingResultType.PROCESSED.getValue());
when(context.authenticatedUser(Mockito.any(CommandWrapper.class))).thenReturn(appUser);
CommandProcessingResult actualCommandProcessingResult = underTest.executeCommand(commandWrapper, jsonCommand, false);
verify(commandSourceService).saveInitial(commandWrapper, jsonCommand, appUser, idk);
verify(commandSource).setStatus(CommandProcessingResultType.PROCESSED.getValue());
verify(commandSourceService).saveResult(commandSource);
verify(commandSourceService).saveInitialNewTransaction(commandWrapper, jsonCommand, appUser, idk);
assertEquals(CommandProcessingResultType.PROCESSED.getValue(), commandSource.getStatus());
verify(commandSourceService).saveResultSameTransaction(commandSource);
Assertions.assertEquals(commandProcessingResult, actualCommandProcessingResult);
assertEquals(commandProcessingResult, actualCommandProcessingResult);
}
@Test
@@ -142,7 +147,7 @@ public class SynchronousCommandProcessingServiceTest {
AppUser appUser = Mockito.mock(AppUser.class);
when(context.authenticatedUser(Mockito.any(CommandWrapper.class))).thenReturn(appUser);
when(commandSourceService.saveInitial(commandWrapper, jsonCommand, appUser, idk)).thenReturn(commandSource);
when(commandSourceService.saveInitialNewTransaction(commandWrapper, jsonCommand, appUser, idk)).thenReturn(commandSource);
CommandSource initialCommandSource = Mockito.mock(CommandSource.class);
@@ -152,7 +157,7 @@ public class SynchronousCommandProcessingServiceTest {
underTest.executeCommand(commandWrapper, jsonCommand, false);
});
verify(commandSourceService).saveInitial(commandWrapper, jsonCommand, appUser, idk);
verify(commandSourceService).generateErrorException(runtimeException);
verify(commandSourceService).saveInitialNewTransaction(commandWrapper, jsonCommand, appUser, idk);
verify(commandSourceService).generateErrorInfo(runtimeException);
}
}
@@ -47,7 +47,7 @@ public class InvalidInstanceTypeMethodExceptionMapperTest {
assertEquals(Response.Status.METHOD_NOT_ALLOWED.getStatusCode(), response.getStatus());
assertNotNull(response.getEntity());
final ApiGlobalErrorResponse errorResponse = (ApiGlobalErrorResponse) response.getEntity();
assertEquals(String.valueOf(Response.Status.METHOD_NOT_ALLOWED.getReasonPhrase()), errorResponse.getHttpStatusCode());
assertEquals(String.valueOf(Response.Status.METHOD_NOT_ALLOWED.getStatusCode()), errorResponse.getHttpStatusCode());
assertEquals("Invalid instance type called in api request for the method POST", errorResponse.getDeveloperMessage());
assertEquals("error.msg.invalid.instance.type", errorResponse.getUserMessageGlobalisationCode());
assertEquals("Invalid method POST used with request to this instance type.", errorResponse.getDefaultUserMessage());
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.integrationtests;
import static org.apache.http.HttpStatus.SC_FORBIDDEN;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -45,6 +46,7 @@ import org.apache.commons.lang3.RandomUtils;
import org.apache.fineract.batch.command.internal.AdjustTransactionCommandStrategy;
import org.apache.fineract.batch.command.internal.CreateTransactionLoanCommandStrategy;
import org.apache.fineract.batch.command.internal.GetDatatableEntryByAppTableIdAndDataTableIdCommandStrategy;
import org.apache.fineract.batch.command.internal.GetTransactionByIdCommandStrategy;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
@@ -207,7 +209,7 @@ public class BatchApiTest {
// Asserts that all the transactions have been successfully rolled back
Assertions.assertEquals(1, response.size());
Assertions.assertEquals((long) 500, (long) response.get(0).getStatusCode(), "Verify Status code 500");
Assertions.assertEquals(SC_FORBIDDEN, response.get(0).getStatusCode(), "Verify Status code 500");
}
/**
@@ -1074,7 +1076,7 @@ public class BatchApiTest {
* Test for the successful disbursement and get loan. A '200' status code is expected on successful responses.
*
* @see org.apache.fineract.batch.command.internal.DisburseLoanCommandStrategy
* @see org.apache.fineract.batch.command.internal.GetTransactionByIdCommandStrategy
* @see GetTransactionByIdCommandStrategy
*/
@Test
public void shouldReturnOkStatusOnSuccessfulDisbursementAndGetTransaction() {
@@ -1432,7 +1434,7 @@ public class BatchApiTest {
assertNotNull(datatableEntryResourceId, "ERROR IN CREATING THE ENTITY DATATABLE RECORD");
// Create datatable entry batch request
final BatchRequest createDatatableEntryRequest = BatchHelper.createDatatableEntryRequest(loanId, datatableName,
final BatchRequest createDatatableEntryRequest = BatchHelper.createDatatableEntryRequest(loanId.toString(), datatableName,
Arrays.asList(columnName1, columnName2));
// Update datatable entry batch request
@@ -2166,7 +2168,7 @@ public class BatchApiTest {
Assertions.assertEquals(2L, updateResponse.getRequestId());
Assertions.assertEquals(HttpStatus.SC_BAD_REQUEST, updateResponse.getStatusCode(),
"Verify Status Code 400 for update datatable entry");
MatcherAssert.assertThat(updateResponse.getBody(), containsString("No results for path"));
MatcherAssert.assertThat(updateResponse.getBody(), containsString("The referenced JSON path is invalid"));
}
/**
@@ -2211,7 +2213,7 @@ public class BatchApiTest {
assertNotNull(datatableEntryResourceId, "ERROR IN CREATING THE ENTITY DATATABLE RECORD");
// Create datatable entry batch request
final BatchRequest createDatatableEntryRequest = BatchHelper.createDatatableEntryRequest(loanId, datatableName,
final BatchRequest createDatatableEntryRequest = BatchHelper.createDatatableEntryRequest(loanId.toString(), datatableName,
Arrays.asList(columnName1, columnName2));
// Get datatable entries batch request
@@ -2655,8 +2655,7 @@ public class ClientSavingsIntegrationTest {
this.savingsAccountHelper.addChargesForSavings(savingsId, chargeIdOne, false, BigDecimal.valueOf(withdrawalChargeOne));
Integer withdrawTransactionIdOne = (Integer) this.savingsAccountHelper.withdrawalFromSavingsAccountWithPaymentType(savingsId,
withdrawalAmountOne, SavingsAccountHelper.TRANSACTION_DATE, paymentTypeIdOne.toString(),
CommonConstants.RESPONSE_RESOURCE_ID);
withdrawalAmountOne, SavingsAccountHelper.TRANSACTION_DATE, paymentTypeIdOne, CommonConstants.RESPONSE_RESOURCE_ID);
Float balance = Float.parseFloat("8990");
// Withdraw charge from paymentType 1 is 10 So balance should be 10,000(deposit)-1000(wd)-"10(charge)" = 8990
@@ -2678,8 +2677,7 @@ public class ClientSavingsIntegrationTest {
this.savingsAccountHelper.addChargesForSavings(savingsId, chargeIdTwo, false, BigDecimal.valueOf(withdrawalChargeTwo));
Integer withdrawTransactionIdTwo = (Integer) this.savingsAccountHelper.withdrawalFromSavingsAccountWithPaymentType(savingsId,
withdrawalAmountTwo, SavingsAccountHelper.TRANSACTION_DATE, paymentTypeIdTwo.toString(),
CommonConstants.RESPONSE_RESOURCE_ID);
withdrawalAmountTwo, SavingsAccountHelper.TRANSACTION_DATE, paymentTypeIdTwo, CommonConstants.RESPONSE_RESOURCE_ID);
Float balanceAfterChargeTwo = Float.parseFloat("6970");
// Withdraw charge from paymentType 2 is 20 So balance should be 8990(balance)-2000(wd)-"20(charge)" = 6970
@@ -18,21 +18,43 @@
*/
package org.apache.fineract.integrationtests;
import static org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper.PAYMENT_TYPE_ID;
import static org.apache.fineract.integrationtests.common.system.DatatableHelper.addDatatableColumn;
import static org.apache.http.HttpStatus.SC_CONFLICT;
import static org.apache.http.HttpStatus.SC_FORBIDDEN;
import static org.apache.http.HttpStatus.SC_OK;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.gson.Gson;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
import io.restassured.path.json.JsonPath;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.batch.domain.Header;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.integrationtests.common.BatchHelper;
import org.apache.fineract.integrationtests.common.BusinessDateHelper;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.CommonConstants;
@@ -41,14 +63,19 @@ import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper;
import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper;
import org.apache.fineract.integrationtests.common.savings.SavingsStatusChecker;
import org.apache.fineract.integrationtests.common.savings.SavingsTransactionData;
import org.apache.fineract.integrationtests.common.system.DatatableHelper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@SuppressWarnings({ "rawtypes" })
public class SavingsAccountTransactionTest {
private static final Logger log = LoggerFactory.getLogger(SavingsAccountTransactionTest.class);
public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
public static final String DEFAULT_DATE_FORMAT = "dd MMMM yyyy";
final String startDateString = "03 June 2023";
@@ -56,23 +83,27 @@ public class SavingsAccountTransactionTest {
final String withdrawDateString = "10 June 2023";
private ResponseSpecification responseSpec;
private ResponseSpecification concurrentResponseSpec;
private RequestSpecification requestSpec;
private SavingsProductHelper savingsProductHelper;
private SavingsAccountHelper savingsAccountHelper;
private DatatableHelper datatableHelper;
@BeforeEach
public void setup() {
Utils.initializeRESTAssured();
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(SC_OK).build();
this.concurrentResponseSpec = new ResponseSpecBuilder().expectStatusCode(anyOf(is(SC_OK), is(SC_CONFLICT))).build();
this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
this.savingsProductHelper = new SavingsProductHelper();
this.datatableHelper = new DatatableHelper(this.requestSpec, this.responseSpec);
}
@Test
public void verifySavingsTransactionSubmittedOnDateAndTransactionDate() throws JsonProcessingException {
LocalDate today = LocalDate.now(ZoneId.systemDefault());
LocalDate today = Utils.getLocalDateOfTenant();
try {
enableBusinessDate(requestSpec, responseSpec, true);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, today);
@@ -81,17 +112,106 @@ public class SavingsAccountTransactionTest {
LocalDate withdrawDate = Utils.getDateAsLocalDate(withdrawDateString);
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec, startDateString);
Assertions.assertNotNull(clientID);
assertNotNull(clientID);
final Integer savingsId = createSavingsAccountDailyPosting(clientID, startDateString);
Assertions.assertNotNull(savingsId);
assertNotNull(savingsId);
performSavingsTransaction(savingsId, "100", depositDate, true);
performSavingsTransaction(savingsId, "50", withdrawDate, false);
} finally {
enableBusinessDate(requestSpec, responseSpec, false);
}
}
@Test
public void testConcurrentSavingsTransactions() {
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec);
ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID);
final Integer savingsProductId = createSavingsProductDailyPosting();
assertNotNull(savingsProductId);
final Integer savingsId = this.savingsAccountHelper.applyForSavingsApplication(clientID, savingsProductId, ACCOUNT_TYPE_INDIVIDUAL);
this.savingsAccountHelper.approveSavings(savingsId);
HashMap savingsStatusHashMap = this.savingsAccountHelper.activateSavings(savingsId);
SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap);
SavingsAccountHelper concurrentHelper = new SavingsAccountHelper(requestSpec, concurrentResponseSpec);
String transactionDate = SavingsAccountHelper.TRANSACTION_DATE;
String transactionAmount = "10";
ExecutorService executor = Executors.newFixedThreadPool(30);
for (int i = 0; i < 10; i++) {
log.info("Starting concurrent transaction number {}", i);
SavingsTransactionData transactionData = SavingsTransactionData.builder().transactionDate(transactionDate)
.transactionAmount(transactionAmount).paymentTypeId(PAYMENT_TYPE_ID).note("note_" + i).build();
Runnable worker = new TransactionExecutor(concurrentHelper, savingsId, transactionData);
executor.execute(worker);
}
executor.shutdown();
// Wait until all threads are finish
while (!executor.isTerminated()) {
}
log.info("\nFinished all threads");
}
@Test
public void testConcurrentSavingsBatchTransactions() {
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec);
ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID);
final Integer savingsProductId = createSavingsProductDailyPosting();
assertNotNull(savingsProductId);
final Integer savingsId = this.savingsAccountHelper.applyForSavingsApplication(clientID, savingsProductId, ACCOUNT_TYPE_INDIVIDUAL);
this.savingsAccountHelper.approveSavings(savingsId);
HashMap savingsStatusHashMap = this.savingsAccountHelper.activateSavings(savingsId);
SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap);
// creating datatable for client entity
final HashMap<String, Object> columnMap = new HashMap<>();
String datatableName = Utils.uniqueRandomStringGenerator("savings_transaction" + "_", 5).toLowerCase();
columnMap.put("datatableName", datatableName);
columnMap.put("apptableName", "m_savings_account_transaction");
columnMap.put("multiRow", false);
String string1 = "string1";
String string2 = "string2";
List<String> columnNames = List.of(string1, string2);
final List<HashMap<String, Object>> datatableColumnsList = new ArrayList<>();
addDatatableColumn(datatableColumnsList, string1, "String", false, 10, null);
addDatatableColumn(datatableColumnsList, string2, "String", true, 12, null);
columnMap.put("columns", datatableColumnsList);
String datatableJson = new Gson().toJson(columnMap);
this.datatableHelper.createDatatable(datatableJson, "");
SavingsAccountHelper batchWithTransactionHelper = new SavingsAccountHelper(requestSpec, concurrentResponseSpec);
SavingsAccountHelper batchWithoutTransactionHelper = new SavingsAccountHelper(requestSpec,
new ResponseSpecBuilder().expectStatusCode(anyOf(is(SC_OK), is(SC_CONFLICT), is(SC_FORBIDDEN))).build());
String transactionDate = SavingsAccountHelper.TRANSACTION_DATE;
String transactionAmount = "10";
ExecutorService executor = Executors.newFixedThreadPool(30);
for (int i = 0; i < 10; i++) {
log.info("Starting concurrent transaction number {}", i);
SavingsTransactionData transactionData = SavingsTransactionData.builder().transactionDate(transactionDate)
.transactionAmount(transactionAmount).paymentTypeId(PAYMENT_TYPE_ID).note("note_" + i).build();
Runnable workerWithTransaction = new TransactionExecutor(batchWithTransactionHelper, savingsId, transactionData, true,
datatableName, columnNames);
executor.execute(workerWithTransaction);
Runnable workerWithoutTransaction = new TransactionExecutor(batchWithoutTransactionHelper, savingsId, transactionData, false,
datatableName, columnNames);
executor.execute(workerWithoutTransaction);
}
executor.shutdown();
// Wait until all threads are finish
while (!executor.isTerminated()) {
}
this.datatableHelper.deleteDatatable(datatableName);
log.info("\nFinished all threads");
}
private void enableBusinessDate(RequestSpecification requestSpec, ResponseSpecification responseSpec, boolean enable) {
@@ -106,17 +226,17 @@ public class SavingsAccountTransactionTest {
: (Integer) this.savingsAccountHelper.withdrawalFromSavingsAccount(savingsId, amount, withdrawDateString,
CommonConstants.RESPONSE_RESOURCE_ID);
Assertions.assertNotNull(transactionId);
assertNotNull(transactionId);
HashMap transaction = savingsAccountHelper.getSavingsTransaction(savingsId, transactionId);
Assertions.assertNotNull(transaction);
assertNotNull(transaction);
assertEquals(transactionId, (Integer) transaction.get("id"), "Check Savings " + transactionType + " Transaction");
LocalDate transactionDateFromResponse = extractLocalDate(transaction, "date");
assertTrue(transactionDate.compareTo(transactionDateFromResponse) == 0,
assertTrue(DateUtils.isEqual(transactionDate, transactionDateFromResponse),
"Transaction Date check for Savings " + transactionType + " Transaction");
LocalDate submittedOnDate = extractLocalDate(transaction, "submittedOnDate");
assertTrue(LocalDate.now(ZoneId.systemDefault()).compareTo(submittedOnDate) == 0,
assertTrue(DateUtils.isEqual(submittedOnDate, Utils.getLocalDateOfTenant()),
"Submitted On Date check for Savings " + transactionType + " Transaction");
}
@@ -129,10 +249,10 @@ public class SavingsAccountTransactionTest {
private Integer createSavingsAccountDailyPosting(final Integer clientID, final String startDate) {
final Integer savingsProductID = createSavingsProductDailyPosting();
Assertions.assertNotNull(savingsProductID);
assertNotNull(savingsProductID);
final Integer savingsId = this.savingsAccountHelper.applyForSavingsApplicationOnDate(clientID, savingsProductID,
ACCOUNT_TYPE_INDIVIDUAL, startDate);
Assertions.assertNotNull(savingsId);
assertNotNull(savingsId);
HashMap savingsStatusHashMap = this.savingsAccountHelper.approveSavingsOnDate(savingsId, startDate);
SavingsStatusChecker.verifySavingsIsApproved(savingsStatusHashMap);
savingsStatusHashMap = this.savingsAccountHelper.activateSavingsAccount(savingsId, startDate);
@@ -146,11 +266,108 @@ public class SavingsAccountTransactionTest {
return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec);
}
public static class TransactionExecutor implements Runnable {
private SavingsAccountHelper savingsHelper;
private final Integer savingsId;
SavingsTransactionData transactionData;
boolean batch;
boolean enclosingTransaction;
String datatableName;
List<String> columnNames;
private TransactionExecutor(SavingsAccountHelper savingsHelper, Integer savingsId, SavingsTransactionData transactionData,
boolean batch, boolean enclosingTransaction, String datatableName, List<String> columnNames) {
this.savingsId = savingsId;
this.savingsHelper = savingsHelper;
this.transactionData = transactionData;
this.batch = batch;
this.enclosingTransaction = enclosingTransaction;
this.datatableName = datatableName;
this.columnNames = columnNames;
}
TransactionExecutor(SavingsAccountHelper savingsAccountHelper, Integer savingsId, SavingsTransactionData transactionData) {
this(savingsAccountHelper, savingsId, transactionData, false, false, null, null);
}
TransactionExecutor(SavingsAccountHelper batchHelper, Integer savingsId, SavingsTransactionData transactionData,
boolean enclosingTransaction, String datatableName, List<String> columnNames) {
this(batchHelper, savingsId, transactionData, true, enclosingTransaction, datatableName, columnNames);
}
@Override
public void run() {
log.info("Details of passed concurrent transaction, details (date, amount, note, savingsId) are {},{},{},{}",
transactionData.getTransactionDate(), transactionData.getTransactionAmount(), transactionData.getNote(), savingsId);
if (batch) {
final BatchRequest depositRequest = BatchHelper.depositSavingAccount(1L, savingsId.longValue(), transactionData);
Set<Header> headers = Optional.ofNullable(depositRequest.getHeaders()).orElse(new HashSet<>(1));
headers.add(new Header("Idempotency-Key", UUID.randomUUID().toString()));
depositRequest.setHeaders(headers);
BatchRequest addEntryRequest = BatchHelper.createDatatableEntryRequest("$.resourceId", datatableName, columnNames);
addEntryRequest.setReference(1L);
BatchRequest deleteEntryRequest = BatchHelper.deleteDatatableEntryRequest("$.transactionId", datatableName, null);
final BatchRequest withdrawRequest = BatchHelper.withdrawSavingAccount(2L, savingsId.longValue(), transactionData);
headers = Optional.ofNullable(withdrawRequest.getHeaders()).orElse(new HashSet<>(1));
headers.add(new Header("Idempotency-Key", UUID.randomUUID().toString()));
withdrawRequest.setHeaders(headers);
String json = BatchHelper.toJsonString(Arrays.asList(depositRequest, addEntryRequest, deleteEntryRequest, withdrawRequest));
RequestSpecification requestSpec = savingsHelper.getRequestSpec();
ResponseSpecification responseSpec = savingsHelper.getResponseSpec();
final List<BatchResponse> responses = enclosingTransaction
? BatchHelper.postBatchRequestsWithEnclosingTransaction(requestSpec, responseSpec, json)
: BatchHelper.postBatchRequestsWithoutEnclosingTransaction(requestSpec, responseSpec, json);
assertNotNull(responses);
if (enclosingTransaction) {
Integer statusCode1 = responses.get(0).getStatusCode();
assertNotNull(statusCode1);
assertTrue(SC_OK == statusCode1 || SC_CONFLICT == statusCode1);
if (SC_OK == statusCode1) {
assertEquals(4, responses.size());
Integer statusCode4 = responses.get(3).getStatusCode();
assertNotNull(statusCode4);
assertEquals(SC_OK, statusCode4);
} else {
assertEquals(1, responses.size());
}
} else {
assertEquals(4, responses.size());
Integer statusCode1 = responses.get(0).getStatusCode();
assertNotNull(statusCode1);
assertTrue(SC_OK == statusCode1 || SC_CONFLICT == statusCode1);
Integer statusCode4 = responses.get(3).getStatusCode();
assertNotNull(statusCode4);
assertTrue(SC_OK == statusCode1 ? (SC_OK == statusCode4 || SC_CONFLICT == statusCode4) : SC_FORBIDDEN == statusCode4);
}
} else {
String json = transactionData.getJson();
String response = (String) this.savingsHelper.depositToSavingsAccount(savingsId, json, null);
boolean success = checkConcurrentResponse(response);
if (success) {
response = (String) this.savingsHelper.withdrawalFromSavingsAccount(savingsId, json, null);
checkConcurrentResponse(response);
}
}
}
private static boolean checkConcurrentResponse(String response) {
assertNotNull(response);
JsonPath res = JsonPath.from(response);
String statusCode = (String) res.get("httpStatusCode");
if (statusCode == null) {
assertNotNull(res.get(CommonConstants.RESPONSE_RESOURCE_ID));
return true;
}
assertEquals(String.valueOf(SC_CONFLICT), statusCode);
return false;
}
}
// Reset configuration fields
@AfterEach
public void tearDown() {
GlobalConfigurationHelper.resetAllDefaultGlobalConfigurations(this.requestSpec, this.responseSpec);
GlobalConfigurationHelper.verifyAllDefaultGlobalConfigurations(this.requestSpec, this.responseSpec);
}
}
@@ -34,8 +34,11 @@ import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.fineract.batch.command.internal.CreateTransactionLoanCommandStrategy;
import org.apache.fineract.batch.command.internal.GetTransactionByExternalIdCommandStrategy;
import org.apache.fineract.batch.command.internal.GetTransactionByIdCommandStrategy;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.integrationtests.common.savings.SavingsTransactionData;
import org.junit.jupiter.api.Assertions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -1018,8 +1021,7 @@ public final class BatchHelper {
}
/**
* Creates and returns a {@link org.apache.fineract.batch.command.internal.GetTransactionByIdCommandStrategy}
* request with given requestId and reference.
* Creates and returns a {@link GetTransactionByIdCommandStrategy} request with given requestId and reference.
*
* @param requestId
* the request id
@@ -1049,9 +1051,8 @@ public final class BatchHelper {
}
/**
* Creates and returns a
* {@link org.apache.fineract.batch.command.internal.GetTransactionByExternalIdCommandStrategy} request with given
* requestId and reference.
* Creates and returns a {@link GetTransactionByExternalIdCommandStrategy} request with given requestId and
* reference.
*
* @param requestId
* the request id
@@ -1247,17 +1248,18 @@ public final class BatchHelper {
/**
* Creates and returns a batch request to create datatable entry.
*
* @param loanId
* the loan id
* @param entityId
* the entity id
* @param datatableName
* the name of datatable
* @param columnNames
* the column names
* @return the {@link BatchRequest}
*/
public static BatchRequest createDatatableEntryRequest(final Long loanId, final String datatableName, final List<String> columnNames) {
public static BatchRequest createDatatableEntryRequest(final String entityId, final String datatableName,
final List<String> columnNames) {
final BatchRequest br = new BatchRequest();
final String relativeUrl = String.format("v1/datatables/%s/%s", datatableName, loanId);
final String relativeUrl = String.format("v1/datatables/%s/%s", datatableName, entityId);
final Map<String, Object> datatableEntryMap = new HashMap<>();
datatableEntryMap.putAll(columnNames.stream().collect(Collectors.toMap(v -> v, v -> Utils.randomStringGenerator("VAL_", 3))));
final String datatableEntryRequestJsonString = new Gson().toJson(datatableEntryMap);
@@ -1302,6 +1304,18 @@ public final class BatchHelper {
return br;
}
public static BatchRequest deleteDatatableEntryRequest(final String entityId, final String datatableName,
final String datatableEntryId) {
final BatchRequest br = new BatchRequest();
final String relativeUrl = datatableEntryId == null ? String.format("v1/datatables/%s/%s", datatableName, entityId)
: String.format("v1/datatables/%s/%s/%s", datatableName, entityId, datatableEntryId);
br.setRequestId(4570L);
br.setReference(4569L);
br.setRelativeUrl(relativeUrl);
br.setMethod(HttpMethod.DELETE);
return br;
}
/**
* Creates and returns a batch request to create an adjust transaction request.
*
@@ -1461,7 +1475,6 @@ public final class BatchHelper {
*
* @param datatableName
* @param resourceId
* @param subResourceId
* @param columnName
* @param columnValue
* @return
@@ -1544,51 +1557,66 @@ public final class BatchHelper {
* Creates and returns a {@link org.apache.fineract.batch.command.internal.SavingsAccountTransactionCommandStrategy}
* request with given request ID.
*
*
* @param requestId
* the request ID
* @param reference
* teh reference
* the reference
* @return BatchRequest the created {@link BatchRequest}
*/
public static BatchRequest depositSavingAccount(final Long requestId, final Long reference) {
final BatchRequest br = new BatchRequest();
br.setRequestId(requestId);
br.setReference(reference);
br.setRelativeUrl("v1/savingsaccounts/$.id/transactions?command=deposit");
br.setMethod(HttpMethod.POST);
final LocalDate transactionDate = LocalDate.now(ZoneId.systemDefault());
final String transactionDateString = transactionDate.format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
br.setBody(String.format("{\"locale\": \"en\", \"dateFormat\": \"dd MMMM yyyy\", "
+ "\"transactionDate\": \"%s\", \"transactionAmount\": \"100\", \"paymentTypeId\": \"1\"}", transactionDateString));
return br;
String json = String.format("{\"locale\": \"en\", \"dateFormat\": \"dd MMMM yyyy\", "
+ "\"transactionDate\": \"%s\", \"transactionAmount\": \"100\", \"paymentTypeId\": \"1\"}", transactionDateString);
return commandSavingAccount(requestId, null, reference, json, "deposit");
}
/**
* Creates and returns a {@link org.apache.fineract.batch.command.internal.SavingsAccountTransactionCommandStrategy}
* request with given request ID.
*
*
* @param requestId
* the request ID
* @param reference
* teh reference
* the reference
* @return BatchRequest the created {@link BatchRequest}
*/
public static BatchRequest withdrawSavingAccount(final Long requestId, final Long reference) {
final LocalDate transactionDate = LocalDate.now(ZoneId.systemDefault());
final String transactionDateString = transactionDate.format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
String json = String.format("{\"locale\": \"en\", \"dateFormat\": \"dd MMMM yyyy\", "
+ "\"transactionDate\": \"%s\", \"transactionAmount\": \"80\", \"paymentTypeId\": \"1\"}", transactionDateString);
return commandSavingAccount(requestId, null, reference, json, "withdrawal");
}
public static BatchRequest depositSavingAccount(final Long requestId, final Long savingsId, SavingsTransactionData transactionData) {
return commandSavingAccount(requestId, savingsId, transactionData, "deposit");
}
public static BatchRequest withdrawSavingAccount(final Long requestId, final Long savingsId, SavingsTransactionData transactionData) {
return commandSavingAccount(requestId, savingsId, transactionData, "withdrawal");
}
public static BatchRequest commandSavingAccount(Long requestId, Long savingsId, SavingsTransactionData transactionData,
String command) {
String json = transactionData.getJson();
return commandSavingAccount(requestId, savingsId, null, json, command);
}
public static BatchRequest commandSavingAccount(Long requestId, Long savingsId, Long reference, String body, String command) {
final BatchRequest br = new BatchRequest();
br.setRequestId(requestId);
br.setReference(reference);
br.setRelativeUrl("v1/savingsaccounts/$.id/transactions?command=withdrawal");
String id;
if (reference != null) {
br.setReference(reference);
id = "$.id";
} else {
id = savingsId.toString();
}
br.setRelativeUrl("v1/savingsaccounts/" + id + "/transactions?command=" + command);
br.setMethod(HttpMethod.POST);
final LocalDate transactionDate = LocalDate.now(ZoneId.systemDefault());
final String transactionDateString = transactionDate.format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
br.setBody(String.format("{\"locale\": \"en\", \"dateFormat\": \"dd MMMM yyyy\", "
+ "\"transactionDate\": \"%s\", \"transactionAmount\": \"80\", \"paymentTypeId\": \"1\"}", transactionDateString));
br.setBody(body);
return br;
}
@@ -97,7 +97,7 @@ public class SavingsAccountHelper extends IntegrationTest {
public static final String TRANSACTION_DATE_PLUS_ONE = "02 March 2013";
public static final String LAST_TRANSACTION_DATE = "01 March 2013";
public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
public static final Integer PAYMENT_TYPE_ID = 1;
public static final Long PAYMENT_TYPE_ID = 1L;
public static final String DATE_TIME_FORMAT = "dd MMMM yyyy HH:mm";
private static final Boolean IS_BLOCK = false;
@@ -107,6 +107,14 @@ public class SavingsAccountHelper extends IntegrationTest {
this.responseSpec = responseSpec;
}
public RequestSpecification getRequestSpec() {
return requestSpec;
}
public ResponseSpecification getResponseSpec() {
return responseSpec;
}
public static String getFutureDate() {
SimpleDateFormat sdf = new SimpleDateFormat(CommonConstants.DATE_FORMAT, Locale.US);
Calendar calendar = Calendar.getInstance();
@@ -255,21 +263,29 @@ public class SavingsAccountHelper extends IntegrationTest {
public Object depositToSavingsAccount(final Integer savingsID, final String amount, String date, String jsonAttributeToGetback) {
LOG.info("--------------------------------- SAVINGS TRANSACTION DEPOSIT --------------------------------");
return performSavingActions(createSavingsTransactionURL(DEPOSIT_SAVINGS_COMMAND, savingsID),
getSavingsTransactionJSON(amount, date), jsonAttributeToGetback);
return depositToSavingsAccount(savingsID, getSavingsTransactionJSON(amount, date), jsonAttributeToGetback);
}
public Object depositToSavingsAccount(final Integer savingsID, final String jsonBody, String jsonAttributeToGetback) {
LOG.info("--------------------------------- SAVINGS TRANSACTION DEPOSIT --------------------------------");
return performSavingActions(createSavingsTransactionURL(DEPOSIT_SAVINGS_COMMAND, savingsID), jsonBody, jsonAttributeToGetback);
}
public Object withdrawalFromSavingsAccount(final Integer savingsId, final String amount, String date, String jsonAttributeToGetback) {
LOG.info("\n--------------------------------- SAVINGS TRANSACTION WITHDRAWAL --------------------------------");
return performSavingActions(createSavingsTransactionURL(WITHDRAW_SAVINGS_COMMAND, savingsId),
getSavingsTransactionJSON(amount, date), jsonAttributeToGetback);
return withdrawalFromSavingsAccount(savingsId, getSavingsTransactionJSON(amount, date), jsonAttributeToGetback);
}
public Object withdrawalFromSavingsAccountWithPaymentType(final Integer savingsId, final String amount, String date, String paymentType,
public Object withdrawalFromSavingsAccountWithPaymentType(final Integer savingsId, final String amount, String date, Long paymentTypeId,
String jsonAttributeToGetback) {
LOG.info("\n--------------------------------- SAVINGS TRANSACTION WITHDRAWAL WITH PAYMENT TYPE--------------------------------");
return performSavingActions(createSavingsTransactionURL(WITHDRAW_SAVINGS_COMMAND, savingsId),
getSavingsTransactionPaymentTypeJSON(amount, date, paymentType), jsonAttributeToGetback);
return withdrawalFromSavingsAccount(savingsId, getSavingsTransactionPaymentTypeJSON(amount, date, paymentTypeId),
jsonAttributeToGetback);
}
public Object withdrawalFromSavingsAccount(final Integer savingsId, final String jsonBody, String jsonAttributeToGetback) {
LOG.info("\n--------------------------------- SAVINGS TRANSACTION WITHDRAWAL --------------------------------");
return performSavingActions(createSavingsTransactionURL(WITHDRAW_SAVINGS_COMMAND, savingsId), jsonBody, jsonAttributeToGetback);
}
public Object payChargeToSavingsAccount(final Integer savingsID, final Integer chargeId, final String amount, String date,
@@ -473,52 +489,22 @@ public class SavingsAccountHelper extends IntegrationTest {
}
private String getSavingsTransactionJSON(final String amount, final String transactionDate) {
final HashMap<String, Object> map = new HashMap<>();
map.put("locale", CommonConstants.LOCALE);
map.put("dateFormat", CommonConstants.DATE_FORMAT);
map.put("transactionDate", transactionDate);
map.put("transactionAmount", amount);
map.put("paymentTypeId", PAYMENT_TYPE_ID);
String savingsAccountWithdrawalJson = new Gson().toJson(map);
LOG.info(savingsAccountWithdrawalJson);
return savingsAccountWithdrawalJson;
return SavingsTransactionData.builder().transactionDate(transactionDate).transactionAmount(amount).paymentTypeId(PAYMENT_TYPE_ID)
.build().getJson();
}
private String getSavingsTransactionJSON(final String amount, final String transactionDate, final boolean isBulk) {
final HashMap<String, String> map = new HashMap<>();
map.put("locale", CommonConstants.LOCALE);
map.put("dateFormat", CommonConstants.DATE_FORMAT);
map.put("transactionDate", transactionDate);
map.put("transactionAmount", amount);
map.put("isBulk", String.valueOf(isBulk));
String savingsAccountWithdrawalJson = new Gson().toJson(map);
LOG.info(savingsAccountWithdrawalJson);
return savingsAccountWithdrawalJson;
return SavingsTransactionData.builder().transactionDate(transactionDate).transactionAmount(amount).isBulk(isBulk).build().getJson();
}
private String getLienSavingsTransactionJSON(final String amount, final String transactionDate, final Boolean lienAllowed) {
final HashMap<String, Object> map = new HashMap<>();
map.put("locale", CommonConstants.LOCALE);
map.put("dateFormat", CommonConstants.DATE_FORMAT);
map.put("transactionDate", transactionDate);
map.put("transactionAmount", amount);
map.put("lienAllowed", lienAllowed);
map.put("reasonForBlock", "unUsualActivity");
String savingsAccountWithdrawalJson = new Gson().toJson(map);
LOG.info(savingsAccountWithdrawalJson);
return savingsAccountWithdrawalJson;
return SavingsTransactionData.builder().transactionDate(transactionDate).transactionAmount(amount).lienAllowed(lienAllowed)
.reasonForBlock("unUsualActivity").build().getJson();
}
private String getSavingsTransactionPaymentTypeJSON(final String amount, final String transactionDate, final String paymentTypeId) {
final HashMap<String, String> map = new HashMap<>();
map.put("locale", CommonConstants.LOCALE);
map.put("dateFormat", CommonConstants.DATE_FORMAT);
map.put("transactionDate", transactionDate);
map.put("transactionAmount", amount);
map.put("paymentTypeId", paymentTypeId);
String savingsAccountWithdrawalJson = new Gson().toJson(map);
LOG.info(savingsAccountWithdrawalJson);
return savingsAccountWithdrawalJson;
private String getSavingsTransactionPaymentTypeJSON(final String amount, final String transactionDate, final Long paymentTypeId) {
return SavingsTransactionData.builder().transactionDate(transactionDate).transactionAmount(amount).paymentTypeId(paymentTypeId)
.build().getJson();
}
private String getCalculatedInterestForSavingsApplicationAsJSON() {
@@ -0,0 +1,57 @@
/**
* 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.savings;
import com.google.gson.Gson;
import java.util.HashMap;
import lombok.Builder;
import lombok.Getter;
import org.apache.fineract.integrationtests.common.CommonConstants;
@Builder
@Getter
public class SavingsTransactionData {
private String locale;
private String dateFormat;
private String transactionDate;
private String transactionAmount;
private Long paymentTypeId;
private String withdrawnOnDate;
private String note;
private Boolean isBulk;
private Boolean lienAllowed;
private String reasonForBlock;
public String getJson() {
final HashMap<String, Object> map = new HashMap<>();
map.put("locale", locale == null ? CommonConstants.LOCALE : locale);
map.put("dateFormat", dateFormat == null ? CommonConstants.DATE_FORMAT : dateFormat);
map.put("transactionDate", transactionDate);
map.put("transactionAmount", transactionAmount);
map.put("paymentTypeId", paymentTypeId);
map.put("withdrawnOnDate", withdrawnOnDate);
map.put("note", note);
map.put("isBulk", isBulk);
map.put("lienAllowed", lienAllowed);
map.put("reasonForBlock", reasonForBlock);
return new Gson().toJson(map);
}
}