This is an automated email from the ASF dual-hosted git repository.

adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 2afd9d35e FINERACT-1724  - Error handler is springified, exception 
mappers are retrieved as spring beans  - backward compatibility is maintained
2afd9d35e is described below

commit 2afd9d35e7d04f8c883380c27c3593695c59de36
Author: Peter Bagrij <[email protected]>
AuthorDate: Sun Jul 9 12:15:35 2023 +0200

    FINERACT-1724
     - Error handler is springified, exception mappers are retrieved as spring 
beans
     - backward compatibility is maintained
---
 .../fineract/batch/exception/ErrorHandler.java     | 114 +++++++----------
 .../batch/service/BatchApiServiceImpl.java         |  13 +-
 .../batch/service/BatchExecutionException.java     |   5 +-
 .../commands/service/CommandSourceService.java     |   3 +-
 ...tionMapper.java => DefaultExceptionMapper.java} |  29 ++---
 .../exceptionmapper/FineractExceptionMapper.java}  |  19 +--
 .../PlatformApiDataValidationExceptionMapper.java  |   8 +-
 .../PlatformDataIntegrityExceptionMapper.java      |   7 +-
 .../PlatformDomainRuleExceptionMapper.java         |   7 +-
 .../PlatformInternalServerExceptionMapper.java     |   7 +-
 .../PlatformResourceNotFoundExceptionMapper.java   |   8 +-
 .../UnsupportedParameterExceptionMapper.java       |   7 +-
 .../fineract/batch/exception/ErrorHandlerTest.java | 137 +++++++++++++++++++++
 .../DefaultExceptionMapperTest.java                |  42 +++++++
 .../LinkedAccountRequiredExceptionMapper.java      |  27 ++--
 ....java => LoanIdsHardLockedExceptionMapper.java} |  33 ++---
 ...ltiDisbursementDataRequiredExceptionMapper.java |  27 ++--
 .../LinkedAccountRequiredExceptionMapperTest.java  |  46 +++++++
 .../LoanIdsHardLockedExceptionMapperTest.java      |  41 ++++++
 ...isbursementDataRequiredExceptionMapperTest.java |  46 +++++++
 .../commands/service/CommandSourceServiceTest.java |   7 ++
 21 files changed, 470 insertions(+), 163 deletions(-)

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 ed172df98..babc57c05 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
@@ -18,29 +18,21 @@
  */
 package org.apache.fineract.batch.exception;
 
+import static org.springframework.core.ResolvableType.forClassWithGenerics;
+
 import com.google.gson.Gson;
 import jakarta.ws.rs.core.Response;
 import jakarta.ws.rs.ext.ExceptionMapper;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.function.Function;
-import 
org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
-import 
org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
-import 
org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
-import 
org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
-import 
org.apache.fineract.infrastructure.core.exception.PlatformInternalServerException;
-import 
org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException;
-import 
org.apache.fineract.infrastructure.core.exceptionmapper.PlatformApiDataValidationExceptionMapper;
-import 
org.apache.fineract.infrastructure.core.exceptionmapper.PlatformDataIntegrityExceptionMapper;
-import 
org.apache.fineract.infrastructure.core.exceptionmapper.PlatformDomainRuleExceptionMapper;
-import 
org.apache.fineract.infrastructure.core.exceptionmapper.PlatformInternalServerExceptionMapper;
-import 
org.apache.fineract.infrastructure.core.exceptionmapper.PlatformResourceNotFoundExceptionMapper;
-import 
org.apache.fineract.infrastructure.core.exceptionmapper.UnsupportedParameterExceptionMapper;
+import java.util.Set;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.SetUtils;
+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.apache.http.HttpStatus;
-import org.springframework.dao.NonTransientDataAccessException;
-import org.springframework.transaction.TransactionException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Component;
 
 /**
  * Provides an Error Handler method that returns an object of type {@link 
ErrorInfo} to the CommandStrategy which raised
@@ -50,60 +42,41 @@ import org.springframework.transaction.TransactionException;
  * @author Rishabh Shukla
  * @see org.apache.fineract.batch.command.CommandStrategy
  */
+@Component
+@Slf4j
+@AllArgsConstructor
 public final class ErrorHandler {
 
-    private static final Gson JSON_HELPER = 
GoogleGsonSerializerHelper.createGsonBuilder(true).create();
-
-    private static final Map<Class<? extends Exception>, 
Function<RuntimeException, ErrorInfo>> EXCEPTION_HANDLERS = Collections
-            .synchronizedMap(new LinkedHashMap<>());
-    private static final Map.Entry<Class<? extends Exception>, 
Function<RuntimeException, ErrorInfo>> DEFAULT_ERROR_HANDLER = Map.entry(
-            RuntimeException.class,
-            runtimeException -> new ErrorInfo(500, 9999, "{\"Exception\": 
%s}".formatted(runtimeException.getMessage())));
+    @Autowired
+    private final ApplicationContext ctx;
 
-    static {
-        EXCEPTION_HANDLERS.put(AbstractPlatformResourceNotFoundException.class,
-                runtimeException -> handleException(runtimeException, new 
PlatformResourceNotFoundExceptionMapper(), 1001));
-        EXCEPTION_HANDLERS.put(UnsupportedParameterException.class,
-                runtimeException -> handleException(runtimeException, new 
UnsupportedParameterExceptionMapper(), 2001));
-        EXCEPTION_HANDLERS.put(PlatformApiDataValidationException.class,
-                runtimeException -> handleException(runtimeException, new 
PlatformApiDataValidationExceptionMapper(), 2002));
-        EXCEPTION_HANDLERS.put(PlatformDataIntegrityException.class,
-                runtimeException -> handleException(runtimeException, new 
PlatformDataIntegrityExceptionMapper(), 3001));
-        EXCEPTION_HANDLERS.put(AbstractPlatformDomainRuleException.class,
-                runtimeException -> handleException(runtimeException, new 
PlatformDomainRuleExceptionMapper(), 9999));
-        EXCEPTION_HANDLERS.put(TransactionException.class, runtimeException -> 
new ErrorInfo(HttpStatus.SC_INTERNAL_SERVER_ERROR, 4001,
-                "{\"Exception\": 
%s}".formatted(runtimeException.getMessage())));
-        EXCEPTION_HANDLERS.put(PlatformInternalServerException.class,
-                runtimeException -> handleException(runtimeException, new 
PlatformInternalServerExceptionMapper(), 5001));
-        EXCEPTION_HANDLERS.put(NonTransientDataAccessException.class, 
runtimeException -> new ErrorInfo(HttpStatus.SC_INTERNAL_SERVER_ERROR,
-                4002, "{\"Exception\": 
%s}".formatted(runtimeException.getMessage())));
-        EXCEPTION_HANDLERS.put(DEFAULT_ERROR_HANDLER.getKey(), 
DEFAULT_ERROR_HANDLER.getValue());
-    }
+    @Autowired
+    private final DefaultExceptionMapper defaultExceptionMapper;
 
-    private ErrorHandler() {}
-
-    public static void registerNewErrorHandler(final Class<? extends 
RuntimeException> exceptionClass, final ExceptionMapper mapper,
-            int errorCode) {
-        LinkedHashMap<Class<? extends Exception>, Function<RuntimeException, 
ErrorInfo>> newHandlers = new LinkedHashMap<>();
-        newHandlers.put(exceptionClass, runtimeException -> 
handleException(runtimeException, mapper, errorCode));
-        EXCEPTION_HANDLERS.forEach(newHandlers::putIfAbsent);
-        EXCEPTION_HANDLERS.clear();
-        newHandlers.forEach(EXCEPTION_HANDLERS::putIfAbsent);
-    }
+    private static final Gson JSON_HELPER = 
GoogleGsonSerializerHelper.createGsonBuilder(true).create();
 
-    public static void registerNewErrorHandler(final Class<? extends 
RuntimeException> exceptionClass,
-            Function<RuntimeException, ErrorInfo> function) {
-        LinkedHashMap<Class<? extends Exception>, Function<RuntimeException, 
ErrorInfo>> newHandlers = new LinkedHashMap<>();
-        newHandlers.put(exceptionClass, function);
-        EXCEPTION_HANDLERS.forEach(newHandlers::putIfAbsent);
-        EXCEPTION_HANDLERS.clear();
-        newHandlers.forEach(EXCEPTION_HANDLERS::putIfAbsent);
+    private <T extends RuntimeException> ExceptionMapper<T> 
findMostSpecificExceptionHandler(T exception) {
+        Class<?> clazz = exception.getClass();
+        do {
+            Set<String> exceptionMappers = 
createSet(ctx.getBeanNamesForType(forClassWithGenerics(ExceptionMapper.class, 
clazz)));
+            Set<String> fineractErrorMappers = 
createSet(ctx.getBeanNamesForType(FineractExceptionMapper.class));
+            SetUtils.SetView<String> intersection = 
SetUtils.intersection(exceptionMappers, fineractErrorMappers);
+            if (intersection.size() > 0) {
+                // noinspection unchecked
+                return (ExceptionMapper<T>) 
ctx.getBean(intersection.iterator().next());
+            }
+            clazz = clazz.getSuperclass();
+        } while (!clazz.equals(Exception.class));
+        // noinspection unchecked
+        return (ExceptionMapper<T>) defaultExceptionMapper;
     }
 
-    private static ErrorInfo handleException(final RuntimeException exception, 
final ExceptionMapper mapper, final int errorCode) {
-        final Response response = mapper.toResponse(exception);
-        final String errorBody = JSON_HELPER.toJson(response.getEntity());
-        return new ErrorInfo(response.getStatus(), errorCode, errorBody);
+    private <T> Set<T> createSet(T[] array) {
+        if (array == null) {
+            return Set.of();
+        } else {
+            return Set.of(array);
+        }
     }
 
     /**
@@ -112,9 +85,10 @@ public final class ErrorHandler {
      * @param exception
      * @return ErrorInfo
      */
-    public static ErrorInfo handler(final RuntimeException exception) {
-        Map.Entry<Class<? extends Exception>, Function<RuntimeException, 
ErrorInfo>> errorhandler = EXCEPTION_HANDLERS.entrySet().stream()
-                .filter(e -> 
e.getKey().isAssignableFrom(exception.getClass())).findFirst().orElse(DEFAULT_ERROR_HANDLER);
-        return errorhandler.getValue().apply(exception);
+    public ErrorInfo handle(final RuntimeException exception) {
+        ExceptionMapper<RuntimeException> exceptionMapper = 
findMostSpecificExceptionHandler(exception);
+        Response response = exceptionMapper.toResponse(exception);
+        return new ErrorInfo(response.getStatus(), ((FineractExceptionMapper) 
exceptionMapper).errorCode(),
+                JSON_HELPER.toJson(response.getEntity()));
     }
 }
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 71f06ac67..dfb43a856 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
@@ -82,6 +82,7 @@ public class BatchApiServiceImpl implements BatchApiService {
     private final CommandStrategyProvider strategyProvider;
     private final ResolutionHelper resolutionHelper;
     private final PlatformTransactionManager transactionManager;
+    private final ErrorHandler errorHandler;
 
     private final List<BatchFilter> batchFilters;
 
@@ -106,7 +107,7 @@ public class BatchApiServiceImpl implements BatchApiService 
{
         final List<BatchRequestNode> batchRequestNodes = 
this.resolutionHelper.getDependingRequests(requestList);
         if (batchRequestNodes.isEmpty()) {
             final BatchResponse response = new BatchResponse();
-            ErrorInfo ex = ErrorHandler.handler(new 
ClientDetailsNotFoundException());
+            ErrorInfo ex = errorHandler.handle(new 
ClientDetailsNotFoundException());
             response.setStatusCode(500);
             response.setBody(ex.getMessage());
             responseList.add(response);
@@ -156,7 +157,7 @@ public class BatchApiServiceImpl implements BatchApiService 
{
                 try {
                     resolvedChildRequest = 
this.resolutionHelper.resoluteRequest(childNode.getRequest(), response);
                 } catch (RuntimeException ex) {
-                    throw new BatchExecutionException(childNode.getRequest(), 
ex);
+                    throw new BatchExecutionException(childNode.getRequest(), 
ex, errorHandler.handle(ex));
                 }
                 callRequestRecursive(resolvedChildRequest, childNode, 
responseList, uriInfo, enclosingTransaction);
 
@@ -206,7 +207,7 @@ public class BatchApiServiceImpl implements BatchApiService 
{
         log.debug("Batch request: method [{}], relative url [{}]", 
request.getMethod(), request.getRelativeUrl());
         Either<RuntimeException, BatchRequest> preprocessorResult = 
runPreprocessors(request);
         if (preprocessorResult.isLeft()) {
-            throw new BatchExecutionException(request, 
preprocessorResult.getLeft());
+            throw new BatchExecutionException(request, 
preprocessorResult.getLeft(), 
errorHandler.handle(preprocessorResult.getLeft()));
         } else {
             request = preprocessorResult.get();
         }
@@ -231,7 +232,7 @@ public class BatchApiServiceImpl implements BatchApiService 
{
         } catch (AbstractIdempotentCommandException idempotentException) {
             return handleIdempotentRequests(idempotentException, request);
         } catch (RuntimeException ex) {
-            throw new BatchExecutionException(request, ex);
+            throw new BatchExecutionException(request, ex, 
errorHandler.handle(ex));
         } finally {
             BatchRequestContextHolder.resetRequestAttributes();
         }
@@ -366,7 +367,7 @@ public class BatchApiServiceImpl implements BatchApiService 
{
                     return Arrays.asList(errResponse);
                 } catch (RuntimeException ex) {
                     status.setRollbackOnly();
-                    ErrorInfo e = ErrorHandler.handler(ex);
+                    ErrorInfo e = errorHandler.handle(ex);
                     BatchResponse errResponse = new BatchResponse();
                     errResponse.setStatusCode(e.getStatusCode());
                     errResponse.setBody(e.getMessage());
@@ -374,7 +375,7 @@ public class BatchApiServiceImpl implements BatchApiService 
{
                 }
             });
         } catch (TransactionException | NonTransientDataAccessException ex) {
-            ErrorInfo e = ErrorHandler.handler(ex);
+            ErrorInfo e = errorHandler.handle(ex);
             return createErrorResponse(responseList, e.getStatusCode());
         }
     }
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 4b9ad606a..d0877a14b 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,7 +20,6 @@ package org.apache.fineract.batch.service;
 
 import lombok.Getter;
 import org.apache.fineract.batch.domain.BatchRequest;
-import org.apache.fineract.batch.exception.ErrorHandler;
 import org.apache.fineract.batch.exception.ErrorInfo;
 
 @Getter
@@ -29,9 +28,9 @@ public class BatchExecutionException extends RuntimeException 
{
     private final BatchRequest request;
     private final ErrorInfo errorInfo;
 
-    public BatchExecutionException(BatchRequest request, RuntimeException ex) {
+    public BatchExecutionException(BatchRequest request, RuntimeException ex, 
ErrorInfo errorInfo) {
         super("Error executing batch request: " + request, ex);
         this.request = request;
-        this.errorInfo = ErrorHandler.handler(ex);
+        this.errorInfo = errorInfo;
     }
 }
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 39420ec1a..f58c7ef8d 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
@@ -46,6 +46,7 @@ import 
org.springframework.transaction.annotation.Transactional;
 public class CommandSourceService {
 
     private final CommandSourceRepository commandSourceRepository;
+    private final ErrorHandler errorHandler;
 
     @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = 
Isolation.REPEATABLE_READ)
     public CommandSource saveInitial(CommandWrapper wrapper, JsonCommand 
jsonCommand, AppUser maker, String idempotencyKey) {
@@ -83,7 +84,7 @@ public class CommandSourceService {
 
     public ErrorInfo generateErrorException(Throwable t) {
         if (t instanceof final RuntimeException e) {
-            return ErrorHandler.handler(e);
+            return errorHandler.handle(e);
         } else {
             return new ErrorInfo(500, 9999, "{\"Exception\": " + t.toString() 
+ "}");
         }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformInternalServerExceptionMapper.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/DefaultExceptionMapper.java
similarity index 50%
copy from 
fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformInternalServerExceptionMapper.java
copy to 
fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/DefaultExceptionMapper.java
index a7ff68c13..5d3e48b24 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformInternalServerExceptionMapper.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/DefaultExceptionMapper.java
@@ -18,34 +18,29 @@
  */
 package org.apache.fineract.infrastructure.core.exceptionmapper;
 
+import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
+
 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 java.util.Map;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
-import 
org.apache.fineract.infrastructure.core.exception.PlatformInternalServerException;
 import org.springframework.context.annotation.Scope;
 import org.springframework.stereotype.Component;
 
-/**
- * An {@link ExceptionMapper} to map {@link PlatformInternalServerException} 
thrown by platform into a HTTP API friendly
- * format.
- *
- * The {@link PlatformInternalServerException} is thrown when an api call 
results in unexpected server side exceptions.
- */
-@Provider
 @Component
 @Scope("singleton")
 @Slf4j
-public class PlatformInternalServerExceptionMapper implements 
ExceptionMapper<PlatformInternalServerException> {
+public class DefaultExceptionMapper implements FineractExceptionMapper, 
ExceptionMapper<RuntimeException> {
+
+    @Override
+    public int errorCode() {
+        return 9999;
+    }
 
     @Override
-    public Response toResponse(final PlatformInternalServerException 
exception) {
-        log.warn("Exception: {}, Message: {}", exception.getClass().getName(), 
exception.getMessage());
-        final ApiGlobalErrorResponse notFoundErrorResponse = 
ApiGlobalErrorResponse.serverSideError(exception.getGlobalisationMessageCode(),
-                exception.getDefaultUserMessage(), 
exception.getDefaultUserMessageArgs());
-        return 
Response.status(Status.INTERNAL_SERVER_ERROR).entity(notFoundErrorResponse).type(MediaType.APPLICATION_JSON).build();
+    public Response toResponse(RuntimeException runtimeException) {
+        return 
Response.status(SC_INTERNAL_SERVER_ERROR).entity(Map.of("Exception", 
runtimeException.getMessage()))
+                .type(MediaType.APPLICATION_JSON).build();
     }
 }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchExecutionException.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/FineractExceptionMapper.java
similarity index 56%
copy from 
fineract-core/src/main/java/org/apache/fineract/batch/service/BatchExecutionException.java
copy to 
fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/FineractExceptionMapper.java
index 4b9ad606a..df6f8cc5d 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchExecutionException.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/FineractExceptionMapper.java
@@ -16,22 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.batch.service;
+package org.apache.fineract.infrastructure.core.exceptionmapper;
 
-import lombok.Getter;
-import org.apache.fineract.batch.domain.BatchRequest;
-import org.apache.fineract.batch.exception.ErrorHandler;
-import org.apache.fineract.batch.exception.ErrorInfo;
+public interface FineractExceptionMapper {
 
-@Getter
-public class BatchExecutionException extends RuntimeException {
-
-    private final BatchRequest request;
-    private final ErrorInfo errorInfo;
-
-    public BatchExecutionException(BatchRequest request, RuntimeException ex) {
-        super("Error executing batch request: " + request, ex);
-        this.request = request;
-        this.errorInfo = ErrorHandler.handler(ex);
-    }
+    int errorCode();
 }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformApiDataValidationExceptionMapper.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformApiDataValidationExceptionMapper.java
index a01d0f50c..728317329 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformApiDataValidationExceptionMapper.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformApiDataValidationExceptionMapper.java
@@ -40,7 +40,8 @@ import org.springframework.stereotype.Component;
 @Component
 @Scope("singleton")
 @Slf4j
-public class PlatformApiDataValidationExceptionMapper implements 
ExceptionMapper<PlatformApiDataValidationException> {
+public class PlatformApiDataValidationExceptionMapper
+        implements FineractExceptionMapper, 
ExceptionMapper<PlatformApiDataValidationException> {
 
     @Override
     public Response toResponse(final PlatformApiDataValidationException 
exception) {
@@ -50,4 +51,9 @@ public class PlatformApiDataValidationExceptionMapper 
implements ExceptionMapper
 
         return 
Response.status(Status.BAD_REQUEST).entity(dataValidationErrorResponse).type(MediaType.APPLICATION_JSON).build();
     }
+
+    @Override
+    public int errorCode() {
+        return 2002;
+    }
 }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDataIntegrityExceptionMapper.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDataIntegrityExceptionMapper.java
index 991e092a5..ba131591f 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDataIntegrityExceptionMapper.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDataIntegrityExceptionMapper.java
@@ -40,7 +40,7 @@ import org.springframework.stereotype.Component;
 @Component
 @Scope("singleton")
 @Slf4j
-public class PlatformDataIntegrityExceptionMapper implements 
ExceptionMapper<PlatformDataIntegrityException> {
+public class PlatformDataIntegrityExceptionMapper implements 
FineractExceptionMapper, ExceptionMapper<PlatformDataIntegrityException> {
 
     @Override
     public Response toResponse(final PlatformDataIntegrityException exception) 
{
@@ -50,4 +50,9 @@ public class PlatformDataIntegrityExceptionMapper implements 
ExceptionMapper<Pla
 
         return 
Response.status(Status.FORBIDDEN).entity(dataIntegrityError).type(MediaType.APPLICATION_JSON).build();
     }
+
+    @Override
+    public int errorCode() {
+        return 3001;
+    }
 }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDomainRuleExceptionMapper.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDomainRuleExceptionMapper.java
index f88bc73a2..2b7ae0ae0 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDomainRuleExceptionMapper.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDomainRuleExceptionMapper.java
@@ -40,7 +40,7 @@ import org.springframework.stereotype.Component;
 @Component
 @Scope("singleton")
 @Slf4j
-public class PlatformDomainRuleExceptionMapper implements 
ExceptionMapper<AbstractPlatformDomainRuleException> {
+public class PlatformDomainRuleExceptionMapper implements 
FineractExceptionMapper, ExceptionMapper<AbstractPlatformDomainRuleException> {
 
     @Override
     public Response toResponse(final AbstractPlatformDomainRuleException 
exception) {
@@ -51,4 +51,9 @@ public class PlatformDomainRuleExceptionMapper implements 
ExceptionMapper<Abstra
         // domain/business logic
         return 
Response.status(Status.FORBIDDEN).entity(notFoundErrorResponse).type(MediaType.APPLICATION_JSON).build();
     }
+
+    @Override
+    public int errorCode() {
+        return 9999;
+    }
 }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformInternalServerExceptionMapper.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformInternalServerExceptionMapper.java
index a7ff68c13..84578a6f0 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformInternalServerExceptionMapper.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformInternalServerExceptionMapper.java
@@ -39,7 +39,7 @@ import org.springframework.stereotype.Component;
 @Component
 @Scope("singleton")
 @Slf4j
-public class PlatformInternalServerExceptionMapper implements 
ExceptionMapper<PlatformInternalServerException> {
+public class PlatformInternalServerExceptionMapper implements 
FineractExceptionMapper, ExceptionMapper<PlatformInternalServerException> {
 
     @Override
     public Response toResponse(final PlatformInternalServerException 
exception) {
@@ -48,4 +48,9 @@ public class PlatformInternalServerExceptionMapper implements 
ExceptionMapper<Pl
                 exception.getDefaultUserMessage(), 
exception.getDefaultUserMessageArgs());
         return 
Response.status(Status.INTERNAL_SERVER_ERROR).entity(notFoundErrorResponse).type(MediaType.APPLICATION_JSON).build();
     }
+
+    @Override
+    public int errorCode() {
+        return 5001;
+    }
 }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformResourceNotFoundExceptionMapper.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformResourceNotFoundExceptionMapper.java
index d44a5b1bf..e2f9f1b21 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformResourceNotFoundExceptionMapper.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformResourceNotFoundExceptionMapper.java
@@ -40,7 +40,8 @@ import org.springframework.stereotype.Component;
 @Component
 @Scope("singleton")
 @Slf4j
-public class PlatformResourceNotFoundExceptionMapper implements 
ExceptionMapper<AbstractPlatformResourceNotFoundException> {
+public class PlatformResourceNotFoundExceptionMapper
+        implements FineractExceptionMapper, 
ExceptionMapper<AbstractPlatformResourceNotFoundException> {
 
     @Override
     public Response toResponse(final AbstractPlatformResourceNotFoundException 
exception) {
@@ -49,4 +50,9 @@ public class PlatformResourceNotFoundExceptionMapper 
implements ExceptionMapper<
                 exception.getDefaultUserMessage(), 
exception.getDefaultUserMessageArgs());
         return 
Response.status(Status.NOT_FOUND).entity(notFoundErrorResponse).type(MediaType.APPLICATION_JSON).build();
     }
+
+    @Override
+    public int errorCode() {
+        return 1001;
+    }
 }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/UnsupportedParameterExceptionMapper.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/UnsupportedParameterExceptionMapper.java
index 926d9bc66..8abda8b0d 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/UnsupportedParameterExceptionMapper.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/UnsupportedParameterExceptionMapper.java
@@ -40,7 +40,7 @@ import org.springframework.stereotype.Component;
 @Component
 @Scope("singleton")
 @Slf4j
-public class UnsupportedParameterExceptionMapper implements 
ExceptionMapper<UnsupportedParameterException> {
+public class UnsupportedParameterExceptionMapper implements 
FineractExceptionMapper, ExceptionMapper<UnsupportedParameterException> {
 
     @Override
     public Response toResponse(final UnsupportedParameterException exception) {
@@ -62,4 +62,9 @@ public class UnsupportedParameterExceptionMapper implements 
ExceptionMapper<Unsu
 
         return 
Response.status(Status.BAD_REQUEST).entity(invalidParameterError).type(MediaType.APPLICATION_JSON).build();
     }
+
+    @Override
+    public int errorCode() {
+        return 2001;
+    }
 }
diff --git 
a/fineract-core/src/test/java/org/apache/fineract/batch/exception/ErrorHandlerTest.java
 
b/fineract-core/src/test/java/org/apache/fineract/batch/exception/ErrorHandlerTest.java
new file mode 100644
index 000000000..a06d42102
--- /dev/null
+++ 
b/fineract-core/src/test/java/org/apache/fineract/batch/exception/ErrorHandlerTest.java
@@ -0,0 +1,137 @@
+/**
+ * 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.exception;
+
+import static org.springframework.core.ResolvableType.forClassWithGenerics;
+
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.ExceptionMapper;
+import java.util.InputMismatchException;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import 
org.apache.fineract.infrastructure.core.exceptionmapper.DefaultExceptionMapper;
+import 
org.apache.fineract.infrastructure.core.exceptionmapper.FineractExceptionMapper;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.context.ApplicationContext;
+
+@ExtendWith(MockitoExtension.class)
+class ErrorHandlerTest {
+
+    @Mock
+    DefaultExceptionMapper defaultExceptionMapper;
+
+    @Mock
+    ApplicationContext applicationContext;
+
+    @Mock(extraInterfaces = FineractExceptionMapper.class)
+    ExceptionMapper<NullPointerException> exceptionMapper;
+
+    @Mock(extraInterfaces = FineractExceptionMapper.class)
+    ExceptionMapper<InputMismatchException> inputMismatchMapper;
+
+    @Mock
+    Response response;
+
+    @InjectMocks
+    ErrorHandler errorHandler;
+
+    @Test
+    public void testErrorHandlerFound() {
+        // given
+        NullPointerException npe = new NullPointerException("Div by zero");
+        
Mockito.when(applicationContext.getBeanNamesForType(forClassWithGenerics(ExceptionMapper.class,
 NullPointerException.class)))
+                .thenReturn(new String[] { "exceptionMapper1", 
"exceptionMapper2", "exceptionMapper3" });
+        
Mockito.when(applicationContext.getBeanNamesForType(FineractExceptionMapper.class))
+                .thenReturn(new String[] { "exceptionMapper2", 
"exceptionMapper4" });
+        
Mockito.when(applicationContext.getBean("exceptionMapper2")).thenReturn(exceptionMapper);
+        Mockito.when(exceptionMapper.toResponse(npe)).thenReturn(response);
+        Mockito.when(response.getStatus()).thenReturn(406);
+        Mockito.when(response.getEntity()).thenReturn(Map.of("Exception", 
"message body"));
+        Mockito.when(((FineractExceptionMapper) 
exceptionMapper).errorCode()).thenReturn(1234);
+
+        // when
+        ErrorInfo errorInfo = errorHandler.handle(npe);
+
+        // then
+        Assertions.assertEquals(1234, errorInfo.getErrorCode());
+        Assertions.assertEquals("{\n  \"Exception\": \"message body\"\n}", 
errorInfo.getMessage());
+        Assertions.assertEquals(406, errorInfo.getStatusCode());
+        Mockito.verifyNoMoreInteractions(exceptionMapper);
+        Mockito.verifyNoInteractions(defaultExceptionMapper);
+    }
+
+    @Test
+    public void testErrorHandlerChecksParentException() {
+        // given
+        InputMismatchException ime = new InputMismatchException("Input 
Mismatch");
+        
Mockito.when(applicationContext.getBeanNamesForType(forClassWithGenerics(ExceptionMapper.class,
 InputMismatchException.class)))
+                .thenReturn(new String[] {}); // no direct handler for 
InputMismatchException
+        
Mockito.when(applicationContext.getBeanNamesForType(forClassWithGenerics(ExceptionMapper.class,
 NoSuchElementException.class)))
+                .thenReturn(new String[] { "inputMismatchMapper" });
+        
Mockito.when(applicationContext.getBeanNamesForType(FineractExceptionMapper.class))
+                .thenReturn(new String[] { "inputMismatchMapper", 
"someOtherMapper", "yetAnotherMapper" });
+        
Mockito.when(applicationContext.getBean("inputMismatchMapper")).thenReturn(inputMismatchMapper);
+        Mockito.when(inputMismatchMapper.toResponse(ime)).thenReturn(response);
+        Mockito.when(response.getStatus()).thenReturn(406);
+        Mockito.when(response.getEntity()).thenReturn(Map.of("Exception", 
"message body"));
+        Mockito.when(((FineractExceptionMapper) 
inputMismatchMapper).errorCode()).thenReturn(1234);
+
+        // when
+        ErrorInfo errorInfo = errorHandler.handle(ime);
+
+        // then
+        Assertions.assertEquals(1234, errorInfo.getErrorCode());
+        Assertions.assertEquals("{\n  \"Exception\": \"message body\"\n}", 
errorInfo.getMessage());
+        Assertions.assertEquals(406, errorInfo.getStatusCode());
+        Mockito.verifyNoMoreInteractions(inputMismatchMapper);
+        Mockito.verifyNoInteractions(exceptionMapper);
+        Mockito.verifyNoInteractions(defaultExceptionMapper);
+    }
+
+    @Test
+    public void testErrorHandlerFallsBackToDefault() {
+        // given
+        NullPointerException npe = new NullPointerException("Div by zero");
+        
Mockito.when(applicationContext.getBeanNamesForType(forClassWithGenerics(ExceptionMapper.class,
 NullPointerException.class)))
+                .thenReturn(new String[] {});
+        
Mockito.when(applicationContext.getBeanNamesForType(FineractExceptionMapper.class))
+                .thenReturn(new String[] { "exceptionMapper2", 
"exceptionMapper4" });
+        
Mockito.when(defaultExceptionMapper.toResponse(npe)).thenReturn(response);
+        Mockito.when(response.getStatus()).thenReturn(406);
+        Mockito.when(response.getEntity()).thenReturn(Map.of("Exception", 
"message body"));
+        Mockito.when(((FineractExceptionMapper) 
defaultExceptionMapper).errorCode()).thenReturn(1234);
+
+        // when
+        ErrorInfo errorInfo = errorHandler.handle(npe);
+
+        // then
+        Assertions.assertEquals(1234, errorInfo.getErrorCode());
+        Assertions.assertEquals("{\n  \"Exception\": \"message body\"\n}", 
errorInfo.getMessage());
+        Assertions.assertEquals(406, errorInfo.getStatusCode());
+        Mockito.verifyNoInteractions(exceptionMapper);
+        Mockito.verifyNoMoreInteractions(defaultExceptionMapper);
+    }
+
+}
diff --git 
a/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exceptionmapper/DefaultExceptionMapperTest.java
 
b/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exceptionmapper/DefaultExceptionMapperTest.java
new file mode 100644
index 000000000..54afb5acb
--- /dev/null
+++ 
b/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exceptionmapper/DefaultExceptionMapperTest.java
@@ -0,0 +1,42 @@
+/**
+ * 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.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
+
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.util.Map;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class DefaultExceptionMapperTest {
+
+    @Test
+    public void testDefaultExceptionMapper() {
+        DefaultExceptionMapper defaultExceptionMapper = new 
DefaultExceptionMapper();
+        Response response = defaultExceptionMapper.toResponse(new 
RuntimeException("error happened"));
+        Assertions.assertEquals(9999, defaultExceptionMapper.errorCode());
+
+        Assertions.assertEquals(Map.of("Exception", "error happened"), 
response.getEntity());
+        Assertions.assertEquals(SC_INTERNAL_SERVER_ERROR, 
response.getStatus());
+        Assertions.assertEquals(MediaType.APPLICATION_JSON, 
response.getMediaType().toString());
+    }
+
+}
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDomainRuleExceptionMapper.java
 
b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/LinkedAccountRequiredExceptionMapper.java
similarity index 65%
copy from 
fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDomainRuleExceptionMapper.java
copy to 
fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/LinkedAccountRequiredExceptionMapper.java
index f88bc73a2..9e69d82ea 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDomainRuleExceptionMapper.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/LinkedAccountRequiredExceptionMapper.java
@@ -16,39 +16,36 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.infrastructure.core.exceptionmapper;
+package org.apache.fineract.infrastructure.core.exception;
 
 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.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
+import 
org.apache.fineract.infrastructure.core.exceptionmapper.FineractExceptionMapper;
+import 
org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
 import org.springframework.context.annotation.Scope;
 import org.springframework.stereotype.Component;
 
-/**
- * An {@link ExceptionMapper} to map {@link 
AbstractPlatformDomainRuleException} thrown by platform into a HTTP API
- * friendly format.
- *
- * The {@link AbstractPlatformDomainRuleException} is thrown when an api call 
results is some internal business/domain
- * logic been violated.
- */
-@Provider
 @Component
 @Scope("singleton")
 @Slf4j
-public class PlatformDomainRuleExceptionMapper implements 
ExceptionMapper<AbstractPlatformDomainRuleException> {
+public class LinkedAccountRequiredExceptionMapper implements 
FineractExceptionMapper, ExceptionMapper<LinkedAccountRequiredException> {
 
     @Override
-    public Response toResponse(final AbstractPlatformDomainRuleException 
exception) {
+    public Response toResponse(LinkedAccountRequiredException exception) {
         log.warn("Exception: {}, Message: {}", exception.getClass().getName(), 
exception.getMessage());
         final ApiGlobalErrorResponse notFoundErrorResponse = 
ApiGlobalErrorResponse.domainRuleViolation(
                 exception.getGlobalisationMessageCode(), 
exception.getDefaultUserMessage(), exception.getDefaultUserMessageArgs());
         // request understood but not carried out due to it violating some
         // domain/business logic
-        return 
Response.status(Status.FORBIDDEN).entity(notFoundErrorResponse).type(MediaType.APPLICATION_JSON).build();
+        return 
Response.status(Response.Status.FORBIDDEN).entity(notFoundErrorResponse).type(MediaType.APPLICATION_JSON).build();
     }
+
+    @Override
+    public int errorCode() {
+        return 3002;
+    }
+
 }
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/ErrorHandlerRegister.java
 
b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/LoanIdsHardLockedExceptionMapper.java
similarity index 50%
rename from 
fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/ErrorHandlerRegister.java
rename to 
fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/LoanIdsHardLockedExceptionMapper.java
index 474da1994..7f47c4056 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/ErrorHandlerRegister.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/LoanIdsHardLockedExceptionMapper.java
@@ -18,26 +18,31 @@
  */
 package org.apache.fineract.infrastructure.core.exception;
 
-import jakarta.annotation.PostConstruct;
-import org.apache.fineract.batch.exception.ErrorHandler;
-import org.apache.fineract.batch.exception.ErrorInfo;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.ExceptionMapper;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
-import 
org.apache.fineract.infrastructure.core.exceptionmapper.PlatformDomainRuleExceptionMapper;
+import 
org.apache.fineract.infrastructure.core.exceptionmapper.FineractExceptionMapper;
 import 
org.apache.fineract.infrastructure.jobs.exception.LoanIdsHardLockedException;
-import 
org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException;
-import 
org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
 import org.apache.http.HttpStatus;
+import org.springframework.context.annotation.Scope;
 import org.springframework.stereotype.Component;
 
 @Component
-public class ErrorHandlerRegister {
+@Scope("singleton")
+@Slf4j
+public class LoanIdsHardLockedExceptionMapper implements 
FineractExceptionMapper, ExceptionMapper<LoanIdsHardLockedException> {
 
-    @PostConstruct
-    public void init() {
-        ErrorHandler.registerNewErrorHandler(LoanIdsHardLockedException.class, 
runtimeException -> new ErrorInfo(HttpStatus.SC_CONFLICT,
-                4090,
-                
ApiGlobalErrorResponse.loanIsLocked(((LoanIdsHardLockedException) 
runtimeException).getLoanIdFromRequest()).toJson()));
-        
ErrorHandler.registerNewErrorHandler(LinkedAccountRequiredException.class, new 
PlatformDomainRuleExceptionMapper(), 3002);
-        
ErrorHandler.registerNewErrorHandler(MultiDisbursementDataRequiredException.class,
 new PlatformDomainRuleExceptionMapper(), 3003);
+    @Override
+    public Response toResponse(LoanIdsHardLockedException exception) {
+        return Response.status(HttpStatus.SC_CONFLICT)
+                
.entity(ApiGlobalErrorResponse.loanIsLocked(exception.getLoanIdFromRequest()).toJson()).type(MediaType.APPLICATION_JSON)
+                .build();
+    }
+
+    @Override
+    public int errorCode() {
+        return 4090;
     }
 }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDomainRuleExceptionMapper.java
 
b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/MultiDisbursementDataRequiredExceptionMapper.java
similarity index 65%
copy from 
fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDomainRuleExceptionMapper.java
copy to 
fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/MultiDisbursementDataRequiredExceptionMapper.java
index f88bc73a2..931cee3a8 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/PlatformDomainRuleExceptionMapper.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/core/exception/MultiDisbursementDataRequiredExceptionMapper.java
@@ -16,39 +16,36 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.infrastructure.core.exceptionmapper;
+package org.apache.fineract.infrastructure.core.exception;
 
 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.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
+import 
org.apache.fineract.infrastructure.core.exceptionmapper.FineractExceptionMapper;
+import 
org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException;
 import org.springframework.context.annotation.Scope;
 import org.springframework.stereotype.Component;
 
-/**
- * An {@link ExceptionMapper} to map {@link 
AbstractPlatformDomainRuleException} thrown by platform into a HTTP API
- * friendly format.
- *
- * The {@link AbstractPlatformDomainRuleException} is thrown when an api call 
results is some internal business/domain
- * logic been violated.
- */
-@Provider
 @Component
 @Scope("singleton")
 @Slf4j
-public class PlatformDomainRuleExceptionMapper implements 
ExceptionMapper<AbstractPlatformDomainRuleException> {
+public class MultiDisbursementDataRequiredExceptionMapper
+        implements FineractExceptionMapper, 
ExceptionMapper<MultiDisbursementDataRequiredException> {
 
     @Override
-    public Response toResponse(final AbstractPlatformDomainRuleException 
exception) {
+    public Response toResponse(MultiDisbursementDataRequiredException 
exception) {
         log.warn("Exception: {}, Message: {}", exception.getClass().getName(), 
exception.getMessage());
         final ApiGlobalErrorResponse notFoundErrorResponse = 
ApiGlobalErrorResponse.domainRuleViolation(
                 exception.getGlobalisationMessageCode(), 
exception.getDefaultUserMessage(), exception.getDefaultUserMessageArgs());
         // request understood but not carried out due to it violating some
         // domain/business logic
-        return 
Response.status(Status.FORBIDDEN).entity(notFoundErrorResponse).type(MediaType.APPLICATION_JSON).build();
+        return 
Response.status(Response.Status.FORBIDDEN).entity(notFoundErrorResponse).type(MediaType.APPLICATION_JSON).build();
+    }
+
+    @Override
+    public int errorCode() {
+        return 3003;
     }
 }
diff --git 
a/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/LinkedAccountRequiredExceptionMapperTest.java
 
b/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/LinkedAccountRequiredExceptionMapperTest.java
new file mode 100644
index 000000000..1cc2f4736
--- /dev/null
+++ 
b/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/LinkedAccountRequiredExceptionMapperTest.java
@@ -0,0 +1,46 @@
+/**
+ * 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.exception;
+
+import static org.apache.http.HttpStatus.SC_FORBIDDEN;
+
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
+import 
org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class LinkedAccountRequiredExceptionMapperTest {
+
+    @Test
+    public void testExceptionMapper() {
+        LinkedAccountRequiredExceptionMapper exceptionMapper = new 
LinkedAccountRequiredExceptionMapper();
+        LinkedAccountRequiredException exception = new 
LinkedAccountRequiredException("entity", "message", "args");
+        Response response = exceptionMapper.toResponse(exception);
+        Assertions.assertEquals(3002, exceptionMapper.errorCode());
+
+        Assertions.assertInstanceOf(ApiGlobalErrorResponse.class, 
response.getEntity());
+        Assertions.assertEquals(1, ((ApiGlobalErrorResponse) 
response.getEntity()).getErrors().size());
+        Assertions.assertEquals("message", ((ApiGlobalErrorResponse) 
response.getEntity()).getErrors().get(0).getDefaultUserMessage());
+        Assertions.assertEquals(SC_FORBIDDEN, response.getStatus());
+        Assertions.assertEquals(MediaType.APPLICATION_JSON, 
response.getMediaType().toString());
+    }
+
+}
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
new file mode 100644
index 000000000..bc838faee
--- /dev/null
+++ 
b/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/LoanIdsHardLockedExceptionMapperTest.java
@@ -0,0 +1,41 @@
+/**
+ * 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.exception;
+
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import 
org.apache.fineract.infrastructure.jobs.exception.LoanIdsHardLockedException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class LoanIdsHardLockedExceptionMapperTest {
+
+    @Test
+    public void testExceptionMapper() {
+        LoanIdsHardLockedExceptionMapper exceptionMapper = new 
LoanIdsHardLockedExceptionMapper();
+        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(409, response.getStatus());
+        Assertions.assertEquals(MediaType.APPLICATION_JSON, 
response.getMediaType().toString());
+    }
+}
diff --git 
a/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/MultiDisbursementDataRequiredExceptionMapperTest.java
 
b/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/MultiDisbursementDataRequiredExceptionMapperTest.java
new file mode 100644
index 000000000..75d15c2d2
--- /dev/null
+++ 
b/fineract-loan/src/test/java/org/apache/fineract/infrastructure/core/exception/MultiDisbursementDataRequiredExceptionMapperTest.java
@@ -0,0 +1,46 @@
+/**
+ * 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.exception;
+
+import static org.apache.http.HttpStatus.SC_FORBIDDEN;
+
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
+import 
org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class MultiDisbursementDataRequiredExceptionMapperTest {
+
+    @Test
+    public void testExceptionMapper() {
+        MultiDisbursementDataRequiredExceptionMapper exceptionMapper = new 
MultiDisbursementDataRequiredExceptionMapper();
+        MultiDisbursementDataRequiredException exception = new 
MultiDisbursementDataRequiredException("entity", "message", "args");
+        Response response = exceptionMapper.toResponse(exception);
+        Assertions.assertEquals(3003, exceptionMapper.errorCode());
+
+        Assertions.assertInstanceOf(ApiGlobalErrorResponse.class, 
response.getEntity());
+        Assertions.assertEquals(1, ((ApiGlobalErrorResponse) 
response.getEntity()).getErrors().size());
+        Assertions.assertEquals("message", ((ApiGlobalErrorResponse) 
response.getEntity()).getErrors().get(0).getDefaultUserMessage());
+        Assertions.assertEquals(SC_FORBIDDEN, response.getStatus());
+        Assertions.assertEquals(MediaType.APPLICATION_JSON, 
response.getMediaType().toString());
+    }
+
+}
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 b28cc7de7..96fa7b0de 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
@@ -19,9 +19,11 @@
 package org.apache.fineract.commands.service;
 
 import static 
org.apache.fineract.commands.domain.CommandProcessingResultType.UNDER_PROCESSING;
+import static org.mockito.ArgumentMatchers.any;
 
 import java.time.ZoneId;
 import java.util.Optional;
+import org.apache.fineract.batch.exception.ErrorHandler;
 import org.apache.fineract.batch.exception.ErrorInfo;
 import org.apache.fineract.commands.domain.CommandSource;
 import org.apache.fineract.commands.domain.CommandSourceRepository;
@@ -46,6 +48,9 @@ public class CommandSourceServiceTest {
     @Mock
     private CommandSourceRepository commandSourceRepository;
 
+    @Mock
+    private ErrorHandler errorHandler;
+
     @InjectMocks
     private CommandSourceService underTest;
 
@@ -103,6 +108,8 @@ public class CommandSourceServiceTest {
 
     @Test
     public void testGenerateErrorException() {
+        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"));
         Assertions.assertEquals(404, result.getStatusCode());
         Assertions.assertEquals(1001, result.getErrorCode());

Reply via email to