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