From 32da7db35d6707946b52a57a0a99e74eaaa8c731 Mon Sep 17 00:00:00 2001 From: jmarta Date: Mon, 25 Sep 2023 07:28:32 +0200 Subject: [PATCH] FINERACT-1927: Incorrect exception info in BatchAPI response --- .../command/CommandStrategyProvider.java | 5 +- ...va => BatchReferenceInvalidException.java} | 9 +- .../batch/exception/ErrorHandler.java | 84 ++++- .../fineract/batch/exception/ErrorInfo.java | 89 +---- .../batch/service/BatchApiServiceImpl.java | 337 +++++++++--------- .../service/BatchExecutionException.java | 5 +- .../batch/service/ResolutionHelper.java | 100 +++--- .../commands/domain/CommandSource.java | 132 +++---- .../commands/domain/CommandWrapper.java | 4 + .../service/CommandSourceService.java | 49 +-- ...CommandSourceWritePlatformServiceImpl.java | 6 +- .../SynchronousCommandProcessingService.java | 116 +++--- .../core/data/ApiGlobalErrorResponse.java | 241 +++++-------- .../core/data/CommandProcessingResult.java | 143 +++----- .../ConcurrencyFailureExceptionMapper.java | 60 ++++ .../IdempotentCommandExceptionMapper.java | 68 ++++ ...tCommandProcessSucceedExceptionMapper.java | 45 --- ...ProcessUnderProcessingExceptionMapper.java | 44 --- ...ava => OptimisticLockExceptionMapper.java} | 29 +- .../filters/IdempotencyStoreBatchFilter.java | 4 +- .../core/filters/IdempotencyStoreFilter.java | 8 +- .../core/filters/IdempotencyStoreHelper.java | 13 +- ...encyCommandProcessFailedExceptionTest.java | 4 +- .../LoanIdsHardLockedExceptionMapperTest.java | 6 +- .../internal/ApplySavingsCommandStrategy.java | 22 +- ...atatableEntryOneToManyCommandStrategy.java | 74 ++++ ...DatatableEntryOneToOneCommandStrategy.java | 73 ++++ .../GetSavingsAccountByIdCommandStrategy.java | 14 +- ...countAdjustTransactionCommandStrategy.java | 11 +- ...ingsAccountTransactionCommandStrategy.java | 11 +- .../api/DocumentManagementApiResource.java | 2 +- .../api/ImagesApiResource.java | 2 +- ...WritePlatformServiceJpaRepositoryImpl.java | 4 +- ...WritePlatformServiceJpaRepositoryImpl.java | 4 +- ...WritePlatformServiceJpaRepositoryImpl.java | 2 +- ...WritePlatformServiceJpaRepositoryImpl.java | 2 +- ...WritePlatformServiceJpaRepositoryImpl.java | 11 +- ...WritePlatformServiceJpaRepositoryImpl.java | 2 +- .../rate/service/RateWriteServiceImpl.java | 2 +- ...SavingsAccountTransactionsApiResource.java | 62 ++-- ...WritePlatformServiceJpaRepositoryImpl.java | 8 +- ...WritePlatformServiceJpaRepositoryImpl.java | 52 +-- ...WritePlatformServiceJpaRepositoryImpl.java | 2 +- .../SelfServiceRegistrationApiResource.java | 2 +- .../src/main/resources/application.properties | 6 +- .../CommandServiceStepDefinitions.java | 42 +-- .../service/CommandSourceServiceTest.java | 9 +- ...nchronousCommandProcessingServiceTest.java | 25 +- ...InstanceTypeMethodExceptionMapperTest.java | 2 +- .../integrationtests/BatchApiTest.java | 12 +- .../ClientSavingsIntegrationTest.java | 6 +- .../SavingsAccountTransactionTest.java | 243 ++++++++++++- .../integrationtests/common/BatchHelper.java | 90 +++-- .../common/savings/SavingsAccountHelper.java | 78 ++-- .../savings/SavingsTransactionData.java | 57 +++ 55 files changed, 1399 insertions(+), 1134 deletions(-) rename fineract-core/src/main/java/org/apache/fineract/batch/exception/{ClientDetailsNotFoundException.java => BatchReferenceInvalidException.java} (73%) create mode 100644 fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/ConcurrencyFailureExceptionMapper.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandExceptionMapper.java delete mode 100644 fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessSucceedExceptionMapper.java delete mode 100644 fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessUnderProcessingExceptionMapper.java rename fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/{IdempotentCommandProcessFailedExceptionMapper.java => OptimisticLockExceptionMapper.java} (51%) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/DeleteDatatableEntryOneToManyCommandStrategy.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/DeleteDatatableEntryOneToOneCommandStrategy.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsTransactionData.java diff --git a/fineract-core/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java b/fineract-core/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java index c415dc876..3f5bdcf0c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java +++ b/fineract-core/src/main/java/org/apache/fineract/batch/command/CommandStrategyProvider.java @@ -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"); diff --git a/fineract-core/src/main/java/org/apache/fineract/batch/exception/ClientDetailsNotFoundException.java b/fineract-core/src/main/java/org/apache/fineract/batch/exception/BatchReferenceInvalidException.java similarity index 73% rename from fineract-core/src/main/java/org/apache/fineract/batch/exception/ClientDetailsNotFoundException.java rename to fineract-core/src/main/java/org/apache/fineract/batch/exception/BatchReferenceInvalidException.java index b0885f521..36c945779 100644 --- a/fineract-core/src/main/java/org/apache/fineract/batch/exception/ClientDetailsNotFoundException.java +++ b/fineract-core/src/main/java/org/apache/fineract/batch/exception/BatchReferenceInvalidException.java @@ -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); + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/batch/exception/ErrorHandler.java b/fineract-core/src/main/java/org/apache/fineract/batch/exception/ErrorHandler.java index babc57c05..6bf566fea 100644 --- a/fineract-core/src/main/java/org/apache/fineract/batch/exception/ErrorHandler.java +++ b/fineract-core/src/main/java/org/apache/fineract/batch/exception/ErrorHandler.java @@ -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 ExceptionMapper findMostSpecificExceptionHandler(T exception) { + @NotNull + public ExceptionMapper findMostSpecificExceptionHandler(T exception) { Class clazz = exception.getClass(); do { Set exceptionMappers = createSet(ctx.getBeanNamesForType(forClassWithGenerics(ExceptionMapper.class, clazz))); @@ -71,6 +86,60 @@ public final class ErrorHandler { return (ExceptionMapper) 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 exceptionMapper = findMostSpecificExceptionHandler(exception); + Response response = exceptionMapper.toResponse(exception); + MultivaluedMap headers = response.getHeaders(); + Set
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 Set 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 exceptionMapper = findMostSpecificExceptionHandler(exception); - Response response = exceptionMapper.toResponse(exception); - return new ErrorInfo(response.getStatus(), ((FineractExceptionMapper) exceptionMapper).errorCode(), - JSON_HELPER.toJson(response.getEntity())); - } } diff --git a/fineract-core/src/main/java/org/apache/fineract/batch/exception/ErrorInfo.java b/fineract-core/src/main/java/org/apache/fineract/batch/exception/ErrorInfo.java index d3a9a7f9b..8512ea1cf 100644 --- a/fineract-core/src/main/java/org/apache/fineract/batch/exception/ErrorInfo.java +++ b/fineract-core/src/main/java/org/apache/fineract/batch/exception/ErrorInfo.java @@ -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
headers; } diff --git a/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchApiServiceImpl.java b/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchApiServiceImpl.java index a14a8d9d2..c23dce152 100644 --- a/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchApiServiceImpl.java +++ b/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchApiServiceImpl.java @@ -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 handleBatchRequestsWithoutEnclosingTransaction(final List 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 handleBatchRequestsWithEnclosingTransaction(final List 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 callInTransaction(Consumer transactionConfigurator, + Supplier> request) { + List 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 handleBatchRequests(boolean enclosingTransaction, final List requestList, final UriInfo uriInfo) { - - final List responseList = new ArrayList<>(requestList.size()); - - final List 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 rootNodes; + try { + rootNodes = this.resolutionHelper.buildNodesTree(requestList); + } catch (BatchReferenceInvalidException e) { + return List.of(buildErrorResponse(e)); } - for (BatchRequestNode rootNode : batchRequestNodes) { + final ArrayList responseList = new ArrayList<>(requestList.size()); + for (BatchRequestNode rootNode : rootNodes) { if (enclosingTransaction) { this.callRequestRecursive(rootNode.getRequest(), rootNode, responseList, uriInfo, enclosingTransaction); } else { - List localResponseList = new ArrayList<>(); + ArrayList 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 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 parentRequestFailedRecursive(BatchRequest request, BatchRequestNode requestNode) { - List 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 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 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 parentRequestFailedRecursive(BatchRequest request, BatchRequestNode requestNode, BatchResponse response, + boolean root) { + List 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 handleBatchRequestsWithoutEnclosingTransaction(final List 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 handleBatchRequestsWithEnclosingTransaction(final List 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 createErrorResponse(List 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 callInTransaction(Supplier> 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 callInTransaction(Consumer transactionConfigurator, - Supplier> request) { - List 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
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 buildErrorResponses(Throwable ex, @NotNull List 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
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
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); + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchExecutionException.java b/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchExecutionException.java index d0877a14b..eb4378166 100644 --- a/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchExecutionException.java +++ b/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchExecutionException.java @@ -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; } } diff --git a/fineract-core/src/main/java/org/apache/fineract/batch/service/ResolutionHelper.java b/fineract-core/src/main/java/org/apache/fineract/batch/service/ResolutionHelper.java index 98158890a..f27e0afbf 100644 --- a/fineract-core/src/main/java/org/apache/fineract/batch/service/ResolutionHelper.java +++ b/fineract-core/src/main/java/org/apache/fineract/batch/service/ResolutionHelper.java @@ -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 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 getChildRequests() { + public List 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 getDependingRequests(final List batchRequests) { - final List rootRequests = new ArrayList<>(); - - for (BatchRequest batchRequest : batchRequests) { - if (batchRequest.getReference() == null) { - final BatchRequestNode node = new BatchRequestNode(); - node.setRequest(batchRequest); - rootRequests.add(node); + public List buildNodesTree(final List requests) { + final List 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 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 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 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 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 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 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; } diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandSource.java b/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandSource.java index e9c0d9f89..707007aa4 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandSource.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandSource.java @@ -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(); + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandWrapper.java b/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandWrapper.java index c28ac3466..99a340a8e 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandWrapper.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandWrapper.java @@ -302,6 +302,10 @@ public class CommandWrapper { return this.taskPermissionName; } + public Long getOfficeId() { + return officeId; + } + public Long getGroupId() { return this.groupId; } diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java index f58c7ef8d..3ac4e3382 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java @@ -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) { diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java index 42cc37b42..225c25f88 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java @@ -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()); diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java index 233a2639d..3eff8ba55 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java @@ -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)); diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/ApiGlobalErrorResponse.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/ApiGlobalErrorResponse.java index 95a72ab18..e2ed84968 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/ApiGlobalErrorResponse.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/ApiGlobalErrorResponse.java @@ -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 errors = new ArrayList<>(); + protected ApiGlobalErrorResponse() {} + + public static ApiGlobalErrorResponse create(int statusCode, String msgCode, String developerMessage, String defaultUserMessage, + List 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 errors = List.of(ApiParameterError.generalError(msgCode, defaultUserMessage)); - final List 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 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 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 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 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 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 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 errors = new ArrayList<>(); errors.add(ApiParameterError.generalError(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs)); - globalErrorResponse.setErrors(errors); - return globalErrorResponse; - } - - protected ApiGlobalErrorResponse() {} - - public ApiGlobalErrorResponse(final List 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 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); diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java index 70123b19b..8be8de388 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/CommandProcessingResult.java @@ -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 changes, final Long productId, final Long gsimId, final Long glimId, + final Map 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 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 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 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 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 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 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 changesOnly, final Long productId, final Long gsimId, final Long glimId, - final Map 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 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 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; } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/ConcurrencyFailureExceptionMapper.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/ConcurrencyFailureExceptionMapper.java new file mode 100644 index 000000000..41d7b6d85 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/ConcurrencyFailureExceptionMapper.java @@ -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 { + + @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; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandExceptionMapper.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandExceptionMapper.java new file mode 100644 index 000000000..5bc1846d3 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandExceptionMapper.java @@ -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 { + + @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; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessSucceedExceptionMapper.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessSucceedExceptionMapper.java deleted file mode 100644 index 77916eb54..000000000 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessSucceedExceptionMapper.java +++ /dev/null @@ -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 { - - @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(); - } -} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessUnderProcessingExceptionMapper.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessUnderProcessingExceptionMapper.java deleted file mode 100644 index dbaaea466..000000000 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessUnderProcessingExceptionMapper.java +++ /dev/null @@ -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 { - - @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(); - } -} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessFailedExceptionMapper.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/OptimisticLockExceptionMapper.java similarity index 51% rename from fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessFailedExceptionMapper.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/OptimisticLockExceptionMapper.java index 88e350b19..5b1ee011a 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/IdempotentCommandProcessFailedExceptionMapper.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/OptimisticLockExceptionMapper.java @@ -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 { +public class OptimisticLockExceptionMapper implements FineractExceptionMapper, ExceptionMapper { @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; } } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreBatchFilter.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreBatchFilter.java index b2e49222a..1af80b65b 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreBatchFilter.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreBatchFilter.java @@ -43,9 +43,9 @@ public class IdempotencyStoreBatchFilter implements BatchFilter { .setAttribute(SynchronousCommandProcessingService.IDEMPOTENCY_KEY_ATTRIBUTE, idempotentKey)); BatchResponse result = chain.serviceCall(batchRequest, uriInfo); Optional 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; } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreFilter.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreFilter.java index 2537a6b9b..daadb63b9 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreFilter.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreFilter.java @@ -56,12 +56,12 @@ public class IdempotencyStoreFilter extends OncePerRequestFilter { filterChain.doFilter(request, wrapper.getValue() != null ? wrapper.getValue() : response); Optional 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(); diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreHelper.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreHelper.java index 5ca5147a0..f2670fec4 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreHelper.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/filters/IdempotencyStoreHelper.java @@ -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 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); }); } diff --git a/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exception/IdempotencyCommandProcessFailedExceptionTest.java b/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exception/IdempotencyCommandProcessFailedExceptionTest.java index 5ab233ee7..dc7ab739a 100644 --- a/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exception/IdempotencyCommandProcessFailedExceptionTest.java +++ b/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exception/IdempotencyCommandProcessFailedExceptionTest.java @@ -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); diff --git a/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/LoanIdsHardLockedExceptionMapperTest.java b/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/LoanIdsHardLockedExceptionMapperTest.java index bc838faee..8971c199d 100644 --- a/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/LoanIdsHardLockedExceptionMapperTest.java +++ b/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/LoanIdsHardLockedExceptionMapperTest.java @@ -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()); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/ApplySavingsCommandStrategy.java b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/ApplySavingsCommandStrategy.java index 918e8a839..a8676ec42 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/ApplySavingsCommandStrategy.java +++ b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/ApplySavingsCommandStrategy.java @@ -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()); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/DeleteDatatableEntryOneToManyCommandStrategy.java b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/DeleteDatatableEntryOneToManyCommandStrategy.java new file mode 100644 index 000000000..9b0a504b3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/DeleteDatatableEntryOneToManyCommandStrategy.java @@ -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 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; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/DeleteDatatableEntryOneToOneCommandStrategy.java b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/DeleteDatatableEntryOneToOneCommandStrategy.java new file mode 100644 index 000000000..8a43fd6bf --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/DeleteDatatableEntryOneToOneCommandStrategy.java @@ -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 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; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/GetSavingsAccountByIdCommandStrategy.java b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/GetSavingsAccountByIdCommandStrategy.java index a43eafaa5..7bf56affe 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/GetSavingsAccountByIdCommandStrategy.java +++ b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/GetSavingsAccountByIdCommandStrategy.java @@ -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 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()); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/SavingsAccountAdjustTransactionCommandStrategy.java b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/SavingsAccountAdjustTransactionCommandStrategy.java index e49d3b442..3c62f14a1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/SavingsAccountAdjustTransactionCommandStrategy.java +++ b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/SavingsAccountAdjustTransactionCommandStrategy.java @@ -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 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()); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/SavingsAccountTransactionCommandStrategy.java b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/SavingsAccountTransactionCommandStrategy.java index 9c2bdb27f..5040ba10f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/SavingsAccountTransactionCommandStrategy.java +++ b/fineract-provider/src/main/java/org/apache/fineract/batch/command/internal/SavingsAccountTransactionCommandStrategy.java @@ -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 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()); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentManagementApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentManagementApiResource.java index fe1f01bee..6b1c9c288 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentManagementApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/DocumentManagementApiResource.java @@ -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 diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ImagesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ImagesApiResource.java index 2e82510ca..9c8b179c1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ImagesApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/api/ImagesApiResource.java @@ -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 **/ diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java index b84ddf714..2ceb1df73 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/DocumentWritePlatformServiceJpaRepositoryImpl.java @@ -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) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceJpaRepositoryImpl.java index 04c316e7a..21035ca3c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/service/ImageWritePlatformServiceJpaRepositoryImpl.java @@ -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) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientIdentifierWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientIdentifierWritePlatformServiceJpaRepositoryImpl.java index 33ebf3b4a..96d60f444 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientIdentifierWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientIdentifierWritePlatformServiceJpaRepositoryImpl.java @@ -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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collateral/service/CollateralWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collateral/service/CollateralWritePlatformServiceJpaRepositoryImpl.java index a25853699..ea718011c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collateral/service/CollateralWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collateral/service/CollateralWritePlatformServiceJpaRepositoryImpl.java @@ -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); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index e72be1ab3..1e623bfbd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -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 diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java index 84033b1c4..35ea15ee9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java @@ -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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/rate/service/RateWriteServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/rate/service/RateWriteServiceImpl.java index a5d816569..52565d0b0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/rate/service/RateWriteServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/rate/service/RateWriteServiceImpl.java @@ -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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java index 42e737eba..95d346487 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java @@ -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 diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java index 089e0daf4..0fd55c56b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl.java @@ -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); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index c9910a9b9..4e2e981f4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -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 existingTransactionIds, final Set 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 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 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 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) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java index eea41b942..c78a6c34a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl.java @@ -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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/registration/api/SelfServiceRegistrationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/registration/api/SelfServiceRegistrationApiResource.java index 88675a871..4f4d77713 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/registration/api/SelfServiceRegistrationApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/registration/api/SelfServiceRegistrationApiResource.java @@ -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())); } } diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index dbde0fd47..987d51228 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -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} diff --git a/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandServiceStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandServiceStepDefinitions.java index 584ce5548..818e6d95f 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandServiceStepDefinitions.java +++ b/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandServiceStepDefinitions.java @@ -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 { diff --git a/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandSourceServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandSourceServiceTest.java index 96fa7b0de..954d0bd50 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandSourceServiceTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandSourceServiceTest.java @@ -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 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 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")); diff --git a/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java index 249be3cf2..1c9e0249d 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/commands/service/SynchronousCommandProcessingServiceTest.java @@ -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); } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/exceptionmapper/InvalidInstanceTypeMethodExceptionMapperTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/exceptionmapper/InvalidInstanceTypeMethodExceptionMapperTest.java index 08bb169de..00b77665b 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/exceptionmapper/InvalidInstanceTypeMethodExceptionMapperTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/exceptionmapper/InvalidInstanceTypeMethodExceptionMapperTest.java @@ -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()); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchApiTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchApiTest.java index 252db682c..2bffb21b5 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchApiTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchApiTest.java @@ -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 diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientSavingsIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientSavingsIntegrationTest.java index 1deff5dee..f6f043094 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientSavingsIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientSavingsIntegrationTest.java @@ -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 diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionTest.java index e5661ea8f..057b19335 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountTransactionTest.java @@ -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 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 columnNames = List.of(string1, string2); + + final List> 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 columnNames; + + private TransactionExecutor(SavingsAccountHelper savingsHelper, Integer savingsId, SavingsTransactionData transactionData, + boolean batch, boolean enclosingTransaction, String datatableName, List 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 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
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 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); } - } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java index 2970ac999..0d4cfcc15 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java @@ -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 columnNames) { + public static BatchRequest createDatatableEntryRequest(final String entityId, final String datatableName, + final List 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 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; } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java index db72cf6d8..0c14c2672 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java @@ -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 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 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 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 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() { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsTransactionData.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsTransactionData.java new file mode 100644 index 000000000..d73045fc9 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsTransactionData.java @@ -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 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); + } + +}