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